在 Perl 正则表达式中处理 "Unescaped braces in regex" 的最佳方法

Best way to deal with "Unescaped braces in regex" inside Perl regex

我最近开始学习 Perl 来自动执行一些无意识的数据任务。我在 windows 机器上工作,但更喜欢使用 Cygwin。编写了一个 Perl 脚本,它在 Cygwin 中完成了我想要的一切,但是当我尝试通过 CMD 在 Windows 上使用 Strawberry Perl 运行 它时,我得到了 "Unescaped left brace in regex is illegal here in regex," 错误。

经过一些阅读,我猜我的 Cygwin 有早期版本的 Perl,而 Strawberry 使用的现代版本的 Perl 不允许这样做。我熟悉正则表达式中的转义字符,但是在使用先前正则表达式匹配中的捕获组进行替换时出现此错误。

open(my $fh, '<:encoding(UTF-8)', $file)
    or die "Could not open file '$file' $!";
my $fileContents = do { local $/; <$fh> };

my $i = 0;
while ($fileContents =~ /(.*Part[^\}]*\})/) {
    $defParts[$i] = ;
    $i = $i + 1;
    $fileContents =~ s///;
}

基本上我在文件中搜索如下所示的匹配项:

Part
{
    Somedata
}

然后将这些匹配项存储在一个数组中。然后从 $fileContents 中清除匹配项,以避免重复。

我确信有更好、更有效的方法来完成这些事情,但令我惊讶的是,当使用捕获组时,它会抱怨未转义的字符。

我可以想象存储捕获组,手动转义大括号,然后使用它进行替换,但是有没有更快或更有效的方法来避免这个错误而不重写整个块? (我想尽可能避免使用特殊包,以便该脚本易于移植。)

我找到的所有与此错误相关的答案都是针对使用花括号编辑源代码更直接或更实用的特定情况。

谢谢!

至于转义的问题,就是quotemeta的意思,

my $needs_escaping = q(some { data } here);
say quotemeta $needs_escaping;

打印什么(在 v5.16 上)

some\ \{\ data\ \}\ here

并且也在 </code> 上工作。有关详细信息,请参阅链接文档。另请参阅 <a href="https://perldoc.perl.org/perlre.html#Regular-Expressions" rel="nofollow noreferrer"><code>\Q in perlre(搜索 \Q),这是在正则表达式中的用法,例如 s/\Q//;\E 停止转义(你不需要的)。

一些评论。

依靠删除让正则表达式不断寻找更多这样的模式可能是一个有风险的设计。如果不是并且您确实使用了它,则不需要索引,因为我们有 push

my @defParts;
while ($fileContents =~ /($pattern)/) {
    push @defParts, ;
    $fileContents =~ s/\Q//;
}

其中 \Q 添加到正则表达式中。更好的是,如 中所述,替换可以在 while 条件本身

中完成
push @defParts,   while $fileContents =~ s/($pattern)//;

为了简洁起见,我使用了 statement modifier 形式(后缀语法)。

对于标量上下文中的 /g modifier,如在 while (/($pattern)/g) { .. } 中,搜索从每次迭代中上一个匹配项的位置继续,这是迭代所有实例的常用方法字符串中的模式。请仔细阅读 /g 在标量上下文中的使用,因为它的行为中有一些细节值得我们注意。

然而,这在这里很棘手(即使它有效),因为字符串在正则表达式下发生变化。如果效率不是问题,您可以在列表上下文中捕获所有匹配 /g 的内容,然后删除它们

my @all_matches = $fileContents =~ /$patt/g;
$fileContents =~ s/$patt//g;

虽然效率低下,因为它进行了两次传递,但更简单明了。

我预计 Somedata 永远不可能包含 },例如嵌套的 { ... },对吗?如果确实如此,您会遇到 平衡定界符 的问题,这要圆润得多。一种方法是使用核心 Text::Balanced 模块。搜索包含示例的 SO 帖子。

我会绕过整个问题,同时简化代码:

my $i = 0;
while ($fileContents =~ s/(.*Part[^\}]*\})//) {
    $defParts[$i] = ;
    $i = $i + 1;
}

这里我们简单的先做代入。如果它成功了,它仍然会设置 </code> 和 return 为真(就像普通的 <code>/.../ 一样),所以以后不需要再乱用 s///

使用 </code>(或任何变量)作为模式意味着您必须转义所有正则表达式元字符(例如 <code>*+{(, |, 等等)如果你想让它按字面意思匹配。您可以使用 quotemeta 或内联 (s/\Q//) 非常轻松地做到这一点,但这仍然是一个额外的步骤,因此容易出错。

或者,您可以保留原始代码而不使用 s///。我的意思是,你已经找到匹配项了。为什么要用s///重新搜索呢?

while ($fileContents =~ /(.*Part[^\}]*\})/) {
    ...
    substr($fileContents, $-[0], $+[0] - $-[0], "");
}

我们已经知道匹配项在字符串中的位置。 $-[0] is the position of the start and $+[0] the position of the end of the last regex match (thus $+[0] - $-[0] is the length of the matched string). We can then use substr"" 替换那个块。

但是让我们继续 s///:

my $i = 0;
while ($fileContents =~ s/(.*Part[^\}]*\})//) {
    $defParts[$i] = ;
    $i++;
}

$i = $i + 1; 可以简化为 $i++; ("increment $i").

my @defParts;
while ($fileContents =~ s/(.*Part[^\}]*\})//) {
    push @defParts, ;
}

我们需要 $i 的唯一原因是向 @defParts 数组添加元素。我们可以通过使用 push 来做到这一点,因此不需要维护额外的变量。这为我们节省了另一行。

现在我们可能不需要销毁$fileContents。如果替换只是为了这个循环的好处而存在(所以我没有重新匹配已经提取的内容),我们可以做得更好:

my @defParts;
while ($fileContents =~ /(.*Part[^\}]*\})/g) {
    push @defParts, ;
}

在标量上下文中使用 /g 将 "current position" 附加到 $fileContents,因此下一次匹配尝试从上一次匹配停止的地方开始。这可能更有效,因为它不必继续重写 $fileContents.

my @defParts = $fileContents =~ /(.*Part[^\}]*\})/g;

... 或者我们可以在列表上下文中使用 //g,其中 return 是所有匹配项的所有捕获组的列表,并将其分配给 @defParts

my @defParts = $fileContents =~ /.*Part[^\}]*\}/g;

如果正则表达式中没有捕获组,//g 在列表上下文中 returns 是所有匹配字符串的列表(就好像有 ( ) 围绕整个正则表达式)。

请随意选择其中的任何一个。 :-)