Как реализовать собственную escape-последовательность при использовании split () в perl? - PullRequest
3 голосов
/ 28 августа 2010

Я пытаюсь написать синтаксический анализатор для формата данных EDI, который представляет собой просто текст с разделителями, но в котором разделители определены в верхней части файла.

По сути, это набор split (), основанный на значениях, которые я прочитал вверху моего кода. Проблема в том, что есть также пользовательский «escape-символ», который указывает, что мне нужно игнорировать следующий разделитель.

Например, если предположить, * является разделителем и? это побег, я делаю что-то вроде

use Data::Dumper;
my $delim = "*";
my $escape = "?";
my $edi = "foo*bar*baz*aster?*isk";

my @split = split("\\" . $delim, $edi);
print Dumper(\@split);

Мне нужно вернуть "aster * isk" в качестве последнего элемента.

Моя первоначальная идея заключалась в том, чтобы сделать что-то, где я заменяю каждый экземпляр escape-символа и следующего символа какой-нибудь нестандартной отображаемой непечатаемой последовательностью ascii, прежде чем вызывать свои функции split (), а затем другое регулярное выражение, чтобы переключить их обратно вправо значения.

Это выполнимо, но похоже на хак и станет довольно уродливым, как только я сделаю это для всех 5 различных разделителей. Каждый разделитель также потенциально является специальным символом регулярного выражения, что приводит к значительному экранированию в моих собственных регулярных выражениях.

Есть ли способ избежать этого, возможно, с помощью специального регулярного выражения, переданного моим вызовам split ()?

Ответы [ 4 ]

7 голосов
/ 28 августа 2010
my @split = split( /(?<!\Q$escape\E)\Q$delim\E/, $edi);

сделает разделение за вас, но вы должны удалить escape-символы отдельно:

s/\Q$escape$delim\E/$delim/g for @split;

Обновление: чтобы разрешить экранирующему символу экранировать любой символ, включая самого себя, а не только разделитель, требуется другой подход. Вот один из способов:

my @split = $edi =~ /(?:\Q$delim\E|^)((?:\Q$escape\E.|(?!\Q$delim\E).)*+)/gs;
s/\Q$escape$delim\E/$delim/g for @split;

*+ требуется Perl 5.10+. До этого было бы:

/(?:\Q$delim\E|^)((?>(?:\Q$escape\E.|(?!\Q$delim\E).)*))/gs
2 голосов
/ 28 августа 2010

Попробуйте Text::CSV.

1 голос
/ 28 августа 2010

Вот пользовательская функция - она ​​длиннее, чем ответ ysth, но, на мой взгляд, легче разбить на полезные части (не являясь единым регулярным выражением), а также имеет возможность справляться с несколькими разделителями, которые вы запрашивали .

sub split_edi {
  my ($in, %args) = @_;
  die q/Usage: split_edi($input, escape => "#", delims => [ ... ]) /
    unless defined $in and defined $args{escape} and defined $args{delims};

  my $escape = quotemeta $args{escape};
  my $delims = join '|', map quotemeta, @{ $args{delims} };

  my ($cur, @ret);

  while ($in !~ /\G\z/cg) {
    if ($in =~ /\G$escape(.)/mcg) {
      $cur .= $1;
    } elsif ($in =~ /\G(?:$delims)/cg) {
      push @ret, $cur; 
      $cur = '';
    } elsif ($in =~ /\G((?:(?!$delims|$escape).)+)/mcg) {
      $cur .= $1;
    } else {
      die "hobbs can't write parsers";
    }
  }
  push @ret, $cur if defined $cur;
  @ret;
}

Первая строка - это синтаксический анализ аргументов, обратная косая черта по мере необходимости и создание фрагмента регулярного выражения, соответствующего любому из разделителей.

Затем следует соответствующий цикл:

  • Если мы найдем выход, пропустим его и запишем следующий символ в качестве буквального бита вывода вместо того, чтобы относиться к нему специально.
  • Если мы найдем какой-либо из разделителей, начните новую запись.
  • В противном случае захватывайте символы до следующего перехода или разделителя.
  • Остановитесь, когда мы достигнем конца строки.

, который довольно прост и все еще имеет довольно солидную производительность. Как и решения ysth для регулярных выражений, они неуместны - они не будут пытаться вернуться без необходимости. Корректность не гарантируется, если escape или любой из разделителей является многосимвольным, хотя я на самом деле думаю это в значительной степени правильно:)

say for split_edi("foo*bar;baz*aster?*isk", delims => [qw(* ;)], escape => "?");
foo
bar
baz
aster*isk
1 голос
/ 28 августа 2010

Это немного сложно, если вы хотите правильно обработать случай, когда escape-символ является последним символом поля. Вот один из способов:

# Process escapes to hide the following character:
$edi =~ s/\Q$escape\E(.)/sprintf '%s%d%s', $escape, ord $1, $escape/esg;

my @split = split( /\Q$delim\E/, $edi);

# Convert escape sequences into the escaped character:
s/\Q$escape\E(\d+)\Q$escape\E/chr $1/eg for @split;

Обратите внимание, что это предполагает, что ни escape-символ, ни разделитель не будут цифрами, но он поддерживает весь диапазон символов Unicode.

...