Perl正则表达式不够贪婪

3

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

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

$regex可以匹配启动子程序的代码。我还试图在$1中捕获子程序的名称,在子程序名称和初始大括号之间捕获任何空格和注释并存储在$2中。问题出在$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",而不是我想要的三行注释。我原以为正则表达式会贪婪地将所有三行注释都放入$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.
'

2
参见PPI。例如, $subs=PPI::Document->new(\$code_string)->find('PPI::Statement::Sub');... - mob
3个回答

7
只看捕获$2部分的正则表达式:(\s*#.*\n)。它只能捕获一行注释。在它后面加上一个星号可以匹配多行注释,并将每行注释放入$2中,每次都会替换前一个$2的值。因此,当匹配完成时,$2的最终值是捕获组匹配的最后一行注释。为了解决这个问题,需要将星号放在捕获组内部。但是,需要再放置另一组括号(这次不进行捕获),以确保星号适用于整个表达式。因此,你需要将(\s*#.*\n)*改为((?:\s*#.*\n)*)
第三个正则表达式之所以有效,是因为无意中将整个表达式括在括号中,以便在其后面加上一个问号。这使得$2一次性捕获了所有注释,并且$3只捕获了最后一行注释。
调试正则表达式时,请确保打印出您使用的所有匹配变量:$1$2$3等。您会发现$1只是子例程的名称,而$2只是第三个注释。这可能会让你想知道,在第一个和第二个捕获组之间没有任何东西时,正则表达式如何跳过前两个注释,最终会导致你发现当捕获组多次匹配时会发生什么。

1
谢谢。我认为你解决了这个问题。实际上,在调试时,我正在打印$1、$2等的值。我缩小了我在这里发布的测试代码。关于 $1 ,正则表达式匹配它的部分是 '([a-zA-Z_]\w*)',即字母字符或下划线后跟零个或多个字母字符、下划线和数字。其中没有匹配空格。我已经测试过了。 - David Levner

4

如果将重复操作添加到捕获组,它只会捕获该组的最终匹配。这就是为什么$regex 只匹配最后一行评论。

以下是我如何重新编写正则表达式:

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

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

  • 白空格和注释匹配部分现在位于非捕获组中
  • 我将正则表达式的该部分从((...)+)?更改为等效的((...)*)

谢谢。我现在明白了。看起来我想要做的需要额外的括号。 - David Levner

1
问题在于默认情况下,\n 不是字符串的一部分。正则表达式在 \n 处停止匹配。
您需要使用 s 修饰符进行多行匹配:
if  ($code_string =~ /$re1/s) {print "For '$re1', \$2 is '$2'\n";}

请注意正则表达式后面的 s

这是不正确的,\n 是字符串的一部分,正则表达式仍然会继续匹配,否则 OP 的任何表达式都不会匹配。 - Andrew Clark
是的,尽管使用s和可能的m修饰符可以更好地编写此正则表达式,但即使没有它们,它也可以很好地匹配。这不是问题所在。 - Ryan C. Thompson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接