负前瞻正则表达式贪婪(为啥.*?太贪婪)

Posted

技术标签:

【中文标题】负前瞻正则表达式贪婪(为啥.*?太贪婪)【英文标题】:Negative Lookahead Regex greed (why is .*? too greedy)负前瞻正则表达式贪婪(为什么.*?太贪婪) 【发布时间】:2011-09-01 10:13:06 【问题描述】:

我无法理解负前瞻正则表达式的细节。在阅读Regex lookahead, lookbehind and atomic groups 之后,当我发现这个描述时,我以为我对负前瞻有一个很好的总结:

(?!REGEX_1)REGEX_2

仅当REGEX_1 不匹配时才匹配;检查REGEX_1 后,REGEX_2 的搜索从同一位置开始。

希望我理解算法,我编造了一个两句测试侮辱;我想找到没有某个词的句子。具体...

侮辱: 'Yomama 很丑。而且,她闻起来像一条湿狗。'

要求

测试 1:返回一个不带“ugly”的句子。 测试 2:返回一个不带“looks”的句子。 测试 3:返回没有“气味”的句子。

我将测试词分配给$arg,并使用(?:(?![A-Z].*?$arg.*?\.))([A-Z].*?\.)来实现测试。

(?![A-Z].*?$arg.*?\.) 是否定前瞻来拒绝带有测试词的句子 ([A-Z].*?\.) 匹配至少一句话。

关键部分似乎在于了解正则表达式引擎在处理否定前瞻后开始匹配的位置。

预期结果

测试 1 ($arg = "ugly"):“而且,她闻起来像一条湿狗。” 测试 2($arg = “looks”):“Yomama 很丑。” 测试 3 ($arg = "smells"):“Yomama 很丑。”

实际结果

测试 1 ($arg = "ugly"):“而且,她闻起来像一条湿狗。” (成功) 测试 2 ($arg = "looks"): "Yomama 很丑。" (成功) 测试 3 ($arg = "smells"):失败,不匹配

一开始我以为Test 3失败是因为([A-Z].*?\.)太贪心,两个句子都匹配;但是,(?:(?![A-Z].*?$arg.*?\.))([A-Z][^\.]*?\.) 也不起作用。接下来我想知道python的负前瞻实现是否有问题,但是perl给了我完全相同的结果。

最后我找到了解决方案,我不得不使用[^\.]*? 来拒绝表达式的.*? 部分中的句点;所以这个正则表达式有效:(?:(?![A-Z][^\.]*?$arg[^\.]*?\.))([A-Z][^\.]*?\.)

问题

但是,我还有另一个担心; “Yomama很丑。”里面没有“气味”。那么,如果 .*? 应该是非贪婪匹配,为什么我不能用 (?:(?![A-Z].*?$arg.*?\.))([A-Z].*?\.) 完成测试 3?

编辑

鉴于@bvr 提出的使用-Mre=debug 的极好建议,我会在下班后考虑更多。看来赛斯的描述在这一点上是准确的。到目前为止,我了解到的是,即使我在 NLA 中放置了非贪婪的 .*? 运算符,负前瞻表达式也会尽可能匹配。


Python 实现

import re

def test_re(arg, INSULTSTR):
    mm = re.search(r'''
        (?:                  # No grouping
        (?![A-Z].*?%s.*?\.)) # Negative zero-width
                             #     assertion: arg, followed by a period
        ([A-Z].*?\.)         # Match a capital letter followed by a period
        ''' % arg, INSULTSTR, re.VERBOSE)
    if mm is not None:
        print "neg-lookahead(%s) MATCHED: '%s'" % (arg, mm.group(1))
    else:
        print "Unable to match: neg-lookahead(%s) in '%s'" % (arg, INSULTSTR)


INSULT = 'Yomama is ugly.  And, she smells like a wet dog.'
test_re('ugly', INSULT)
test_re('looks', INSULT)
test_re('smells', INSULT)

Perl 实现

#!/usr/bin/perl

sub test_re 
    $arg    = $_[0];
    $INSULTSTR = $_[1];
    $INSULTSTR =~ /(?:(?![A-Z].*?$arg.*?\.))([A-Z].*?\.)/;
    if ($1) 
        print "neg-lookahead($arg) MATCHED: '$1'\n";
     else 
        print "Unable to match: neg-lookahead($arg) in '$INSULTSTR'\n";
    


$INSULT = 'Yomama is ugly.  And, she smells like a wet dog.';
test_re('ugly', $INSULT);
test_re('looks', $INSULT);
test_re('smells', $INSULT);

输出

neg-lookahead(ugly) MATCHED: 'And, she smells like a wet dog.'
neg-lookahead(looks) MATCHED: 'Yomama is ugly.'
Unable to match: neg-lookahead(smells) in 'Yomama is ugly.  And, she smells like a wet dog.'

【问题讨论】:

其他故障:test_re('Yomama',$INSULT);test_re('And',$INSULT); @Mike:是的,你得到了匹配,但它们是糟糕的匹配。它正在返回一个包含坏词的句子。 关于您的负面预测,$arg 之后的所有内容有什么意义?在我看来,(?![A-Z][^\.]*?$arg) 会像遇到 $arg 时一样失败(失败是这里的预期行为)。但我不知道 Perl 或 Python。 @harpo,我在否定前瞻中使用了(?![A-Z].*?$arg.*?\.),因为我想拒绝一个带有$arg 的句子;但是,我想 100% 确定我尽可能避免与第二句话匹配。因此,我明确匹配了$arg 之后的第一个句点 我想我的意思是,(?![^\.]*?$arg) 应该这样做。 【参考方案1】:
#!/usr/bin/perl

sub test_re 
    $arg    = $_[0];
    $INSULTSTR = $_[1];
    $INSULTSTR =~ /(?:^|\.\s*)(?:(?![^.]*?$arg[^.]*\.))([^.]*\.)/;
    if ($1) 
        print "neg-lookahead($arg) MATCHED: '$1'\n";
     else 
        print "Unable to match: neg-lookahead($arg) in '$INSULTSTR'\n";
    


$INSULT = 'Yomama is ugly.  And, she smells like an wet dog.';
test_re('Yomama', $INSULT);
test_re('ugly', $INSULT);
test_re('looks', $INSULT);
test_re('And', $INSULT);
test_re('And,', $INSULT);
test_re('smells', $INSULT);
test_re('dog', $INSULT);

结果:

neg-lookahead(Yomama) MATCHED: 'And, she smells like an wet dog.'
neg-lookahead(ugly) MATCHED: 'And, she smells like an wet dog.'
neg-lookahead(looks) MATCHED: 'Yomama is ugly.'
neg-lookahead(And) MATCHED: 'Yomama is ugly.'
neg-lookahead(And,) MATCHED: 'Yomama is ugly.'
neg-lookahead(smells) MATCHED: 'Yomama is ugly.'
neg-lookahead(dog) MATCHED: 'Yomama is ugly.'

【讨论】:

Seth,你当然似乎有更好的实现;但是,我试图理解为什么我的原始代码会破坏 WRT 贪婪。 我怀疑问题是您通过移动模式开始位置来要求第二句匹配,同时仍然进行非贪婪匹配。允许.*? 匹配.,因此重新引擎没有特别的理由尝试移动模式开始空间。如果您将.*? 更改为[^.]*(是否贪心),系统将正常工作(模第一个单词匹配问题)。 为了帮助解释问题,请尝试使用模式[A-Z].*?smells.*?\.。它匹配整个字符串。它应该只适用于((?![A-Z][^\.]*?smells.*?\.)[A-Z].*?\.) @Mike:这可能会让正在发生的事情更清楚:`` perl -e '$_="Yomama 很丑。而且,她闻起来像条湿狗。"; $arg="气味";打印“匹配 \n” if /([A-Z].*?$arg.*?\.)/;' `` 模式开始没有改变,..*? 匹配。【参考方案2】:

如果您对 Perl 对正则表达式的作用感到好奇,您可以使用正则表达式调试器运行:

perl -Dr -e '"A two. A one." =~ /(?![A-Z][^\.]*(?:two)[^\.]*\.)([A-Z][^\.]+\.)/; print ">$1<\n"'

这将产生很多输出供您思考。您将需要使用 -DDEBUGGING 构建的 Perl。

【讨论】:

即使没有 DEBUGGING 的 perl 也可以使用 perl -Mre=debug -e ... @bvr,谢谢-Mre=debug;该选项还允许我在没有-e 的情况下从磁盘运行脚本,这就是我在这种情况下想要的。【参考方案3】:

您的问题是正则表达式引擎将尽可能匹配(?![A-Z].*?$arg.*?\.),因此对于“气味”的情况,它最终会匹配整个字符串。 (然后中间的句点包含在 .*? 构造之一中。)您应该限制负前瞻情况,使其仅匹配其他情况:

代替:

(?:(?![A-Z].*?$arg.*?\.))([A-Z].*?\.)

用途:

(?:(?![A-Z][^.]*$arg[^.]*\.))([A-Z].*?\.)

现在,负前瞻不能比其他部分匹配更多的字符串,因为它必须在第一个句点停止。

【讨论】:

以上是关于负前瞻正则表达式贪婪(为啥.*?太贪婪)的主要内容,如果未能解决你的问题,请参考以下文章

非贪婪的前瞻正则表达式

正则表达式第三回--模式分组与前瞻

15.python正则匹配 元字符转义重复或捕获分组断言:零度断言负向零宽断言贪婪非贪婪引擎选项

为啥正则表达式默认是贪婪的?

js正则匹配总结

正则表达式贪婪与非贪婪模式