3

我正在尝试为 EDI 数据格式编写一个解析器,它只是分隔文本,但分隔符是在文件顶部定义的。

本质上,它是一堆基于我在代码顶部读取的值的 splits() 。问题是还有一个自定义的“转义字符”,表示我需要忽略以下分隔符。

例如,假设 * 是分隔符,而 ? 是逃跑,我正在做类似的事情

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

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

我需要它返回“aster*isk”作为最后一个元素。

我最初的想法是做一些事情,在我调用我的 split() 函数之前,用一些自定义映射的不可打印的 ascii 序列替换转义字符的每个实例和以下字符,然后使用另一个正则表达式将它们切换回正确的值。

这是可行的,但感觉就像一个 hack,一旦我为所有 5 个不同的潜在分隔符做了这件事,它就会变得非常难看。每个定界符也可能是一个正则表达式特殊字符,导致我自己的正则表达式中有很多转义。

有什么方法可以避免这种情况,可能是通过将特殊的正则表达式传递给我的 split() 调用?

4

4 回答 4

7
my @split = split( /(?<!\Q$escape\E)\Q$delim\E/, $edi);

将为您进行拆分,但您必须单独删除转义字符:

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
于 2010-08-27T21:49:37.733 回答
2

试试Text::CSV

于 2010-08-27T21:50:19.117 回答
1

如果要正确处理转义字符是字段的最后一个字符的情况,这有点棘手。这是一种方法:

# 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;

请注意,这假定转义字符和分隔符都不是数字,但它确实支持所有 Unicode 字符。

于 2010-08-27T22:39:17.970 回答
1

这是一个自定义函数——它比 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 的正则表达式解决方案一样,它是棘轮式的——它不会试图不必要地回溯。如果转义符或任何分隔符是多字符,则不能保证正确性,尽管我实际上认为这非常正确:)

say for split_edi("foo*bar;baz*aster?*isk", delims => [qw(* ;)], escape => "?");
foo
bar
baz
aster*isk
于 2010-08-28T03:05:31.783 回答