对于分隔符(例如<
和>
)之间匹配文本的常见问题,有两种常见模式:
- 使用贪心
*
或+
量词的形式START [^END]* END
,例如<[^>]*>
,或 - 在表格中使用惰性
*?
或+?
量词START .*? END
,例如<.*?>
.
有什么特别的理由偏爱其中一个吗?
对于分隔符(例如<
和>
)之间匹配文本的常见问题,有两种常见模式:
*
或+
量词的形式START [^END]* END
,例如<[^>]*>
,或*?
或+?
量词START .*? END
,例如<.*?>
.有什么特别的理由偏爱其中一个吗?
一些优点:
[^>]*
:
/s
标志如何,都捕获换行符。[^>]
引擎不会做出选择 - 我们只给它一种将模式与字符串匹配的方法)。.*?
(?:(?!END).)*
. 如果 END 分隔符是另一种模式,情况会更糟。第一个更明确,即它明确地将结束分隔符排除在匹配文本的一部分之外。在第二种情况下不能保证这一点(如果正则表达式被扩展为匹配的不仅仅是这个标签)。
示例:如果您尝试与 匹配<tag1><tag2>Hello!
,<.*?>Hello!
则正则表达式将匹配
<tag1><tag2>Hello!
而<[^>]*>Hello!
将匹配
<tag2>Hello!
大多数人在处理此类问题时没有考虑的是,当正则表达式无法找到匹配项时会发生什么。 那是最有可能出现杀手级性能漏洞的时候。例如,以 Tim 为例,您正在寻找类似<tag>Hello!
. 考虑会发生什么:
<.*?>Hello!
正则表达式引擎找到 a<
并很快找到关闭>
,但不是>Hello!
。所以.*?
继续寻找后面跟着的a >
。如果没有,它将在放弃之前一直走到文档的末尾。然后正则表达式引擎继续扫描,直到找到另一个,然后再试一次。 我们已经知道结果会如何,但是正则表达式引擎通常不知道;它与文档中的每个都经历了相同的繁琐。现在考虑另一个正则表达式:Hello!
<
<
<[^>]*>Hello!
<
和以前一样,它从到快速匹配>
,但无法匹配Hello!
。它将回溯到<
,然后退出并开始扫描另一个<
. 它仍然会像第一个正则表达式一样检查每<
一个,但它不会在每次找到一个时一直搜索到文档的末尾。
但它甚至比这更糟糕。如果你仔细想想,.*?
实际上相当于一个负前瞻。它的意思是“在使用下一个字符之前,确保正则表达式的其余部分不能在这个位置匹配。” 换句话说,
/<.*?>Hello!/
...相当于:
/<(?:(?!>Hello!).)*(?:>Hello!|\z(*FAIL))/
因此,在您执行的每个位置上,不仅仅是正常的匹配尝试,而是更昂贵的前瞻。(这至少是两倍的成本,因为前瞻必须扫描至少一个字符,然后.
继续并消耗一个字符。)
((*FAIL)
是 Perl 的回溯控制动词之一(PHP 也支持)。 |\z(*FAIL)
意思是“或到达文档末尾并放弃”。)
最后,否定字符类方法还有另一个优点。虽然它(正如@Bart 指出的那样)不像量词是所有格一样,但如果你的风格支持它,没有什么可以阻止你让它成为所有格:
/<[^>]*+>Hello!/
...或将其包装在一个原子组中:
/(?><[^>]*>)Hello!/
这些正则表达式不仅不会不必要地回溯,而且它们不必保存使回溯成为可能的状态信息。