Perl非贪婪模式

5

我遇到了一个非贪婪正则表达式(regex)的问题。我看到有关非贪婪正则表达式的问题,但它们并没有回答我的问题。

问题: 我正在尝试匹配“lol”锚点的href。

注意: 我知道可以使用Perl HTML解析模块来完成此操作,但我的问题与在Perl中解析HTML无关。我的问题是关于正则表达式本身的,而HTML只是一个例子。

测试用例: 我对.*?[^"]进行了四个测试。前两个产生了预期的结果。然而第三个测试失败了,第四个测试成功了,但我不明白为什么。

  1. 为什么第三个测试在.*?[^"]的两个测试中都失败了?非贪婪运算符不应该起作用吗?
  2. 为什么第四个测试在.*?[^"]的两个测试中都成功了?我不明白为什么在前面包含一个.*会改变正则表达式(第三个和第四个测试相同,除了前面的.*)。

我可能不完全理解这些正则表达式的工作原理。一个Perl Cookbook的配方提到了一些内容,但我认为它没有回答我的问题。

use strict;

my $content=<<EOF;
<a href="/hoh/hoh/hoh/hoh/hoh" class="hoh">hoh</a>
<a href="/foo/foo/foo/foo/foo" class="foo">foo </a>
<a href="/bar/bar/bar/bar/bar" class="bar">bar</a>
<a href="/lol/lol/lol/lol/lol" class="lol">lol</a>
<a href="/koo/koo/koo/koo/koo" class="koo">koo</a>
EOF

print "| $1 | \n\nThat's ok\n" if $content =~ m~href="(.*?)"~s ;

print "\n---------------------------------------------------\n";

print "| $1 | \n\nThat's ok\n" if $content =~ m~href="(.*?)".*>lol~s ;

print "\n---------------------------------------------------\n";

print "| $1 | \n\nWhy does not the 2nd non-greedy '?' work?\n"
  if $content =~ m~href="(.*?)".*?>lol~s ;

print "\n---------------------------------------------------\n";

print "| $1 | \n\nIt now works if I put the '.*' in the front?\n"
  if $content =~ m~.*href="(.*?)".*?>lol~s ;

print "\n###################################################\n";
print "Let's try now with [^]";
print "\n###################################################\n\n";


print "| $1 | \n\nThat's ok\n" if $content =~ m~href="([^"]+?)"~s ;

print "\n---------------------------------------------------\n";

print "| $1 | \n\nThat's ok.\n" if $content =~ m~href="([^"]+?)".*>lol~s ;

print "\n---------------------------------------------------\n";

print "| $1 | \n\nThe 2nd greedy still doesn't work?\n"
  if $content =~ m~href="([^"]+?)".*?>lol~s ;

print "\n---------------------------------------------------\n";

print "| $1 | \n\nNow with the '.*' in front it does.\n"
  if $content =~ m~.*href="([^"]+?)".*?>lol~s ;

你陈述了一个问题,并说有一个解决方案可以产生预期的结果。我不确定问题是什么。 - musiKk
你说得对,我没有表达得够清楚。我进行了编辑并更明确地陈述了问题。 - vkats
4个回答

6
尝试打印出整个正则表达式匹配的文本$&$1。这可以让你更好地了解发生了什么。
你所遇到的问题是,.*?并不意味着“在所有可能的匹配中找到使用最少字符的匹配”。它只表示“首先尝试在此处匹配0个字符,并继续匹配剩余的正则表达式。如果失败,则尝试在此处匹配1个字符。如果剩余的正则表达式无法匹配,则尝试在此处匹配2个字符等”。
Perl将始终找到距离字符串开头最近的匹配。由于大多数模式都以href=开头,它会找到字符串中第一个href=并查看是否有任何方法可以扩展重复项以从那里开始匹配。如果它无法获得匹配,它将尝试从下一个href=开始,依此类推。
当您在正则表达式的开头添加贪婪的.*时,匹配会从.*抓取尽可能多的字符开始。然后,Perl回溯以找到href=。本质上,这导致它首先尝试字符串中的最后一个href=,然后向字符串开头工作。

谢谢,这似乎就是问题所在。它很好地解释了第一个匹配和回溯。 - vkats
记住的一件好事是,贪婪/非贪婪永远不会改变匹配成功或失败的结果。如果它以贪婪方式成功,那么以非贪婪方式也会成功。如果它以贪婪方式失败,那么以非贪婪方式也会失败。当在当前位置(从左到右)有多种匹配方式时,贪婪性才会发挥作用。在这种情况下,贪婪匹配当前点可能的最长匹配,而非贪婪匹配当前点可能的最短匹配。 - tadmc
1
@cjm:谢谢,这是我看到的第一个关于为什么它不起作用以及如何使其起作用的实际答案。在其他有相同问题的问题和答案中,人们只提供了不同的解决方案,而不是真正的答案。 - Francisco Zarabozo

0

让我试着解释一下这里发生了什么(参见其他答案为什么会发生这种情况):

href="(.*?)"

匹配:href="/hoh/hoh/hoh/hoh/hoh"
分组:/hoh/hoh/hoh/hoh/hoh

href="(.*?)".*>lol

匹配: href="/hoh/hoh/hoh/hoh/hoh" class="hoh">hoh</a> <a href="/foo/foo/foo/foo/foo" class="foo">foo </a> <a href="/bar/bar/bar/bar/bar" class="bar">bar</a> <a href="/lol/lol/lol/lol/lol" class="lol">lol

分组: /hoh/hoh/hoh/hoh/hoh

href="([^"]+?)".*?>lol

匹配: href="/hoh/hoh/hoh/hoh/hoh" class="hoh">hoh</a> <a href="/foo/foo/foo/foo/foo" class="foo">foo </a> <a href="/bar/bar/bar/bar/bar" class="bar">bar</a> <a href="/lol/lol/lol/lol/lol" class="lol">lol

组: /hoh/hoh/hoh/hoh/hoh

.*href="(.*?)".*?>lol

匹配: <a href="/hoh/hoh/hoh/hoh/hoh" class="hoh">hoh</a> <a href="/foo/foo/foo/foo/foo" class="foo">foo </a> <a href="/bar/bar/bar/bar/bar" class="bar">bar</a> <a href="/lol/lol/lol/lol/lol" class="lol">lol

组: /lol/lol/lol/lol/lol

编写所需的正则表达式的一种方法是使用:href="[^"]*"[^>]*>lol


确实,您的建议 href="[^"]*"[^>]*>lol 是有效的。使用 + 替换 * 的正则表达式 href="[^"]+"[^>]+>lol 是否会改变其含义? - vkats
@vkats,对我来说它很好用。我使用 * 而不是 + 是因为 href="">lol - gangabass

0

只有第四个测试用例有效。

第一个:m〜href =“(.*?)”〜s

这将匹配您字符串中的第一个href,并捕获引号之间的内容,因此:/hoh/hoh/hoh/hoh/hoh

第二个:m〜href =“(.*?)”。*&gt;lol〜s

这将匹配您字符串中的第一个href,并捕获引号之间的内容。然后它匹配任意数量的任何字符,直到找到>lol,因此:/hoh/hoh/hoh/hoh/hoh

尝试使用m〜href =“(.*?)”(.*)&gt;lol〜s捕获.*

$1 contains:
/hoh/hoh/hoh/hoh/hoh
$2 contains: 
class="hoh">hoh</a>
<a href="/foo/foo/foo/foo/foo" class="foo">foo </a>
<a href="/bar/bar/bar/bar/bar" class="bar">bar</a>
<a href="/lol/lol/lol/lol/lol" class="lol" 

第三个:m〜href =“(.*?)”。*?>lol〜s

与上一个测试用例相同的结果。

第四个:m〜。* href =“(.*?)”。*?>lol〜s

这将匹配任意数量的任何字符,然后是 href =“,然后捕获任意数量的任何非贪婪字符直到引号,然后匹配任意数量的任何字符,直到找到> lol ,因此:/ lol / lol / lol / lol / lol

尝试使用 m〜(.*)href =“(.*?)”(.*?)>lol〜s 捕获所有 .*

$1 contains:
<a href="/hoh/hoh/hoh/hoh/hoh" class="hoh">hoh</a>
<a href="/foo/foo/foo/foo/foo" class="foo">foo </a>
<a href="/bar/bar/bar/bar/bar" class="bar">bar</a>
<a
$2 contains: 
/lol/lol/lol/lol/lol
$3 contains:
class="lol"

看看这个网站,它解释了你的正则表达式是如何工作的。


谢谢您的回答。您提到了发生了什么(我已经理解了),但没有提到为什么会发生这种情况。也许我的问题没有表述清楚,所以我进行了编辑。 - vkats
@vkats:我会说是因为正则表达式是这样工作的 :-)。它试图匹配你正在搜索的第一个出现的内容。 - Toto
我知道它试图匹配我告诉它要匹配的内容。显然,我不理解我告诉它要匹配什么,这就是我尝试做的事情。 - vkats

0
主要问题在于你在不应该使用非贪婪正则表达式。第二个问题是使用 *.,这可能会意外匹配更多的内容。你正在使用的 s 标志使得 . 更加匹配。
建议使用:
m~href="([^"]+)"[^>]*>lol~

对于您的情况。关于非贪婪正则表达式,请考虑以下代码:

$_ = "xaaaaab xaaac xbbc";
m~^x.+?c~;

它不会像你期望的那样匹配'xaaac'。它将从字符串的开头开始匹配'xaaaaab xaaac'。贪婪变体将匹配整个字符串。

关键是,尽管非贪婪正则表达式不会尽可能地抓取尽可能多的内容,但它们仍然会像贪婪正则表达式一样急切地尝试进行匹配。并且它们将抓取字符串的任何部分来完成匹配。

您还可以考虑“占有量词”,它关闭回溯。

此外,食谱是一个很好的开始,但如果您想了解事物的真实运作方式,您应该阅读这篇文章 - perlre


感谢回答(它与几秒钟前给出的另一个答案一致)。我忘记了匹配从左侧开始。 - vkats

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