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

20 投票
3 回答
5197 浏览
提问于 2025-04-16 18:17

我在理解负向前瞻正则表达式的细节时遇到了一些困难。在阅读了正则表达式的前瞻、后顾和原子组后,我觉得我对负向前瞻有了一个不错的总结,看到这个描述时我觉得很有帮助:

(?!REGEX_1)REGEX_2

只有在REGEX_1不匹配的情况下才会匹配;在检查完REGEX_1后,REGEX_2的搜索从同一个位置开始。

我希望我理解了这个算法,于是我想出了一个两句话的测试侮辱句;我想找出没有某个词的句子。具体来说...

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

要求

  • 测试1:返回一个没有'丑'的句子。
  • 测试2:返回一个没有'看起来'的句子。
  • 测试3:返回一个没有'闻'的句子。

我把测试词赋值给$arg,然后用(?:(?![A-Z].*?$arg.*?\.))([A-Z].*?\.)来实现测试。

  • (?![A-Z].*?$arg.*?\.)是一个负向前瞻,用来拒绝包含测试词的句子。
  • ([A-Z].*?\.)至少匹配一个句子。

关键在于理解正则表达式引擎在处理完负向前瞻后,从哪里开始匹配。

预期结果

  • 测试1 ($arg = "丑"): "而且,她闻起来像只湿狗。"
  • 测试2 ($arg = "看起来"): "Yomama很丑。"
  • 测试3 ($arg = "闻"): "Yomama很丑。"

实际结果

  • 测试1 ($arg = "丑"): "而且,她闻起来像只湿狗。" (成功)
  • 测试2 ($arg = "看起来"): "Yomama很丑。" (成功)
  • 测试3 ($arg = "闻"): 失败,没有匹配

起初我以为测试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,我会在工作后再考虑一下。到目前为止,Seth的描述看起来是准确的。我学到的一个重要点是,负向前瞻表达式会尽可能匹配,即使我在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.'

3 个回答

2

你的问题在于,正则表达式引擎会尽量匹配 (?![A-Z].*?$arg.*?\.),所以在“smells”的情况下,它最终会匹配整个字符串。(中间的句号会被包含在 .*? 的某个部分里。)你应该限制负向前瞻的匹配范围,让它只匹配和其他部分一样多的内容:

不要使用:

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

而是使用:

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

现在,负向前瞻的匹配不会超过其他部分的匹配,因为它必须在第一个句号处停止。

3

如果你想知道Perl在处理正则表达式时到底在干什么,可以使用正则表达式调试器来查看:

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

这个调试器会给你很多输出信息,让你可以仔细思考。你需要使用带有-DDEBUGGING选项的Perl版本。

3
#!/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.'

结果:

撰写回答