【问题标题】:Perl regular expression isn't greedy enoughPerl 正则表达式不够贪婪
【发布时间】:2012-03-30 06:26:13
【问题描述】:

我正在用 perl 编写一个正则表达式来匹配启动 perl 子例程定义的 perl 代码。这是我的正则表达式:

my $regex = '\s*sub\s+([a-zA-Z_]\w*)(\s*#.*\n)*\s*\{';

$regex 匹配启动子例程的代码。我还试图在 $1 中捕获子例程的名称,以及在子例程名称和 $2 中的初始左大括号之间的任何空格和 cmets。 2 美元给我带来了麻烦。

考虑以下 perl 代码:

my $x = 1;

sub zz
# This is comment 1.
# This is comment 2.
# This is comment 3.
{
    $x = 2;
    return;
}

当我将这个 perl 代码放入一个字符串并与 $regex 匹配时,$2 是“# This is comment 3.\n”,而不是我想要的三行 cmets。我以为正则表达式会贪婪地将所有三行 cmets 放入 $2,但似乎并非如此。

我想了解为什么 $regex 不起作用并设计一个简单的替代品。正如下面的程序所示,我有一个更复杂的替换($re3)可以工作。但我认为了解 $regex 为什么不起作用对我来说很重要。

use strict;
use English;

my $code_string = <<END_CODE;
my \$x = 1;

sub zz
# This is comment 1.
# This is comment 2.
# This is comment 3.
{
    \$x = 2;
    return;
}
END_CODE

my $re1 = '\s*sub\s+([a-zA-Z_]\w*)(\s*#.*\n)*\s*\{';
my $re2 = '\s*sub\s+([a-zA-Z_]\w*)(\s*#.*\n){0,}\s*\{';
my $re3 = '\s*sub\s+([a-zA-Z_]\w*)((\s*#.*\n)+)?\s*\{';

print "\$code_string is '$code_string'\n";
if  ($code_string =~ /$re1/) {print "For '$re1', \$2 is '$2'\n";}
if  ($code_string =~ /$re2/) {print "For '$re2', \$2 is '$2'\n";}
if  ($code_string =~ /$re3/) {print "For '$re3', \$2 is '$2'\n";}
exit 0;

__END__

上面perl脚本的输出如下:

$code_string is 'my $x = 1;

sub zz
# This is comment 1.
# This is comment 2.
# This is comment 3.
{
    $x = 2;
    return;
} # sub zz
'
For '\s*sub\s+([a-zA-Z_]\w*)(\s*#.*\n)*\s*\{', $2 is '# This is comment 3.
'
For '\s*sub\s+([a-zA-Z_]\w*)(\s*#.*\n){0,}\s*\{', $2 is '# This is comment 3.
'
For '\s*sub\s+([a-zA-Z_]\w*)((\s*#.*\n)+)?\s*\{', $2 is '
# This is comment 1.
# This is comment 2.
# This is comment 3.
'

【问题讨论】:

  • 另见PPI。例如,$subs=PPI::Document-&gt;new(\$code_string)-&gt;find('PPI::Statement::Sub');...

标签: regex perl regex-greedy


【解决方案1】:

只查看正则表达式中捕获$2 的部分。它是(\s*#.*\n)。就其本身而言,它只能捕获 single 注释行。您在它后面有一个星号以捕获多个注释行,这很好用。它捕获多个注释行并将它们中的每一个逐个放入$2,每次替换之前的$2 值。因此,当正则表达式完成匹配时,$2 的最终值是捕获组匹配的 last 事物,即最后的注释行。仅有的。要修复它,您需要将星号放在捕获组中。但是随后您需要添加另一组括号(这次是非捕获),以确保星号适用于整个事物。所以你需要((?:\s*#.*\n)*),而不是(\s*#.*\n)*

您的第三个正则表达式有效,因为您无意中将整个表达式括在括号中,以便您可以在它后面加上一个问号。这导致$2 一次捕获所有cmets,而$3 仅捕获最终评论。

当您调试您的正则表达式时,请确保您打印出 所有 您正在使用的匹配变量的值:$1$2$3 等。您将拥有看到$1 只是子例程的名称,$2 只是第三条评论。这可能会让您想知道,当第一个和第二个捕获组之间没有任何内容时,您的正则表达式到底是如何跳过前两个 cmets 的,这最终会引导您发现当一个捕获组多次匹配时会发生什么。

顺便说一句,您似乎还在将子例程名称后的任何空格捕获到$1 中。这是故意的吗?(糟糕,我弄乱了我的助记符,并认为\w 是“w 代表空白”。)

【讨论】:

  • 谢谢。我想你解决了这个问题。事实上,我在调试时打印了 $1, $2, ... 的值。我最小化了我在这里发布的测试代码。||||关于 $1,与它匹配的正则表达式部分是 '([a-zA-Z_]\w*)',一个字母字符或下划线后跟零或更多字母字符、下划线和数字。这些都不匹配空白。我已经测试过了。
【解决方案2】:

如果您向捕获组添加重复,它将仅捕获该组的最终匹配项。这就是为什么$regex 只匹配最后的注释行。

这是我将如何重写你的正则表达式:

my $regex = '\s*sub\s+([a-zA-Z_]\w*)((?:\s*#.*\n)*)\s*\{';

这与您的$re3 非常相似,除了以下更改:

  • 空白和评论匹配部分现在位于非捕获组中
  • 我将正则表达式的那部分从 ((...)+)? 更改为 ((...)*),这是等效的。

【讨论】:

  • 谢谢。我现在看到了。我想做的事情似乎需要额外的括号。
【解决方案3】:

问题在于默认情况下\n 不是字符串的一部分。正则表达式在 \n 处停止匹配。

您需要使用s 修饰符进行多行匹配:

if  ($code_string =~ /$re1/s) {print "For '$re1', \$2 is '$2'\n";}

注意正则表达式后面的s

【讨论】:

  • 这是不正确的,\n 是字符串的一部分,并且正则表达式确实会继续匹配,否则 OP 的任何表达式都不会匹配。
  • 是的,尽管使用 s 和可能的 m 修饰符可以更好地编写此正则表达式,但它在没有它们的情况下匹配得很好。这不是问题。
猜你喜欢
  • 2011-03-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-25
相关资源
最近更新 更多