在perl中使用split()时,如何实现自己的转义序列?

在perl中使用split()时,如何实现自己的转义序列?,perl,split,delimiter,escaping,Perl,Split,Delimiter,Escaping,我正在尝试为EDI数据格式编写一个解析器,它只是分隔文本,但分隔符是在文件顶部定义的 本质上,它是一组基于我在代码顶部读取的值的splits()。 问题是还有一个自定义的“转义字符”,表示我需要忽略以下分隔符 例如,假设*是分隔符,而?是逃跑,我在做类似的事情 use Data::Dumper; my $delim = "*"; my $escape = "?"; my $edi = "foo*bar*baz*aster?*isk"; my @split = split("\\" . $del

我正在尝试为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序列替换转义字符和后面字符的每个实例,然后使用另一个regexp将它们切换回正确的值

这是可行的,但感觉像一个黑客,并会变得相当丑陋,一旦我做了所有5个不同的潜在定界符。每个分隔符也可能是一个regexp特殊字符,导致在我自己的正则表达式中进行大量转义

有没有办法避免这种情况,可能是通过向我的split()调用传递一个特殊的regexp

更新:要允许转义字符转义任何字符,包括其本身,而不仅仅是分隔符,需要不同的方法。这里有一个方法:

my @split = $edi =~ /(?:\Q$delim\E|^)((?:\Q$escape\E.|(?!\Q$delim\E).)*+)/gs;
s/\Q$escape$delim\E/$delim/g for @split;
# 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;
*+
需要perl 5.10+。在此之前,它将是:

/(?:\Q$delim\E|^)((?>(?:\Q$escape\E.|(?!\Q$delim\E).)*))/gs

试试。

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

my @split = $edi =~ /(?:\Q$delim\E|^)((?:\Q$escape\E.|(?!\Q$delim\E).)*+)/gs;
s/\Q$escape$delim\E/$delim/g for @split;
# 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字符。

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


如果输入包含
“??*”
,该怎么办?这会使星号转义还是不转义?据我所知,这应该只是作为一个文字来阅读?一个非转义星号猜测转义将只在分隔符上使用,而不会在其他字符上使用,并且分隔符将始终不同于转义。您将文本分隔到字段中,基本上是一个xSV文件。在我的问题中,使用Text::CSV和与转义符和分隔符匹配的选项charsI可能没有足够清楚地说明这一点,但问题是有多个嵌套分隔符,我认为Text::CSV一次只能处理一个。例如:abc | foo~bar^ alpha~beta | def将是['abc'、['foo'、'bar'、['alpha'、'beta']、'def'];这无法正确处理转义转义字符。e、 例如,它不拆分
'foo???*bar'
,但应该拆分为
'foo?'
'bar'
;我的$csv=Text::csv->new({escape\u char=>'?',sep\u char=>'*',allow\u loose\u escapes=>1})$csv->parse($edi);打印转储程序($csv->字段);
say for split_edi("foo*bar;baz*aster?*isk", delims => [qw(* ;)], escape => "?");
foo
bar
baz
aster*isk