负向前瞻正则表达式贪婪(为什么.*?太贪婪)
我在理解负向前瞻正则表达式的细节时遇到了一些困难。在阅读了正则表达式的前瞻、后顾和原子组后,我觉得我对负向前瞻有了一个不错的总结,看到这个描述时我觉得很有帮助:
(?!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 个回答
你的问题在于,正则表达式引擎会尽量匹配 (?![A-Z].*?$arg.*?\.)
,所以在“smells”的情况下,它最终会匹配整个字符串。(中间的句号会被包含在 .*?
的某个部分里。)你应该限制负向前瞻的匹配范围,让它只匹配和其他部分一样多的内容:
不要使用:
(?:(?![A-Z].*?$arg.*?\.))([A-Z].*?\.)
而是使用:
(?:(?![A-Z][^.]*$arg[^.]*\.))([A-Z].*?\.)
现在,负向前瞻的匹配不会超过其他部分的匹配,因为它必须在第一个句号处停止。
如果你想知道Perl在处理正则表达式时到底在干什么,可以使用正则表达式调试器来查看:
perl -Dr -e '"A two. A one." =~ /(?![A-Z][^\.]*(?:two)[^\.]*\.)([A-Z][^\.]+\.)/; print ">$1<\n"'
这个调试器会给你很多输出信息,让你可以仔细思考。你需要使用带有-DDEBUGGING选项的Perl版本。
#!/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.'
结果: