比较非匹配正则表达式的速度

时间:2012-11-01 14:19:28

标签: python regex perl

以下Python代码非常慢:

import re
re.match( '([a]+)+c', 'a' * 30 + 'b' )

如果用更大的常数替换30,情况会变得更糟。

我怀疑由于连续+导致的解析歧义是罪魁祸首,但我在regexp解析和匹配方面不是很专业。这是Python regexp引擎的错误,还是任何合理的实现都会这样做?

我不是Perl专家,但以下回复非常快

perl -e '$s="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"; print "ok\n" if $s =~ m/([a]+)+c/;'

并且增加'a'的数量并不会显着改变执行速度。

2 个答案:

答案 0 :(得分:13)

我认为Perl足够聪明,可以将两个+合并为一个,而Python则不然。现在让我们想象一下引擎的功能,如果没有优化的话。请记住,捕获通常很昂贵。另请注意,两个+都是贪婪的,因此引擎将尝试在一个回溯步骤中尽可能多地使用重复。每个项目符号代表一个回溯步骤:

  • 引擎使用尽可能多的[a],并消耗所有30 a秒。然后它不能再进一步,所以它留下了第一次重复和捕获 30 a s。现在下一个重复开始,它尝试用另一个([a]+)消耗更多,但这当然不起作用。然后c无法匹配b
  • 原路返回!扔掉内心重复消耗的最后a。在此之后我们再次离开内部重复,因此引擎将捕获 29 a s。然后另一个+开始,内部重复再次尝试(消耗第30 a)。然后我们再次离开内部重复,这也会离开捕获组,因此第一次捕获被丢弃,引擎捕获最后ac无法匹配b
  • 原路返回!扔掉里面的另一个a。我们捕获 28 a。捕获组的第二个(外部重复)消耗最后2个a 捕获c无法匹配b
  • 原路返回!现在我们可以在第二个其他重复中回溯并扔掉两个a中的第二个。剩下的那个将是捕获。然后我们第三次进入捕获组,捕获最后一次ac无法匹配b
  • 原路返回!在第一次重复中低至27 a

这是一个简单的可视化。每一行代表一个回溯步骤,每组括号显示一次内部重复的消耗。大括号表示为回溯步骤捕获的那些,而在此特定回溯步骤中不重新访问正常括号。我遗漏了b / c,因为它永远不会匹配:

{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}
{aaaaaaaaaaaaaaaaaaaaaaaaaaaaa}{a}
{aaaaaaaaaaaaaaaaaaaaaaaaaaaa}{aa}
(aaaaaaaaaaaaaaaaaaaaaaaaaaaa){a}{a}
{aaaaaaaaaaaaaaaaaaaaaaaaaaa}{aaa}
(aaaaaaaaaaaaaaaaaaaaaaaaaaa){aa}{a}
(aaaaaaaaaaaaaaaaaaaaaaaaaaa){a}{aa}
(aaaaaaaaaaaaaaaaaaaaaaaaaaa)(a){a}{a}
{aaaaaaaaaaaaaaaaaaaaaaaaaa}{aaaa}
(aaaaaaaaaaaaaaaaaaaaaaaaaa){aaa}{a}
(aaaaaaaaaaaaaaaaaaaaaaaaaa){aa}{aa}
(aaaaaaaaaaaaaaaaaaaaaaaaaa)(aa){a}{a}
(aaaaaaaaaaaaaaaaaaaaaaaaaa){a}{aaa}
(aaaaaaaaaaaaaaaaaaaaaaaaaa)(a){aa}{a}
(aaaaaaaaaaaaaaaaaaaaaaaaaa)(a){a}{aa}
(aaaaaaaaaaaaaaaaaaaaaaaaaa)(a)(a){a}{a}

和。所以。上。

请注意,最后引擎还会针对a的子集尝试所有组合(仅通过前29个a然后通过前28个a回溯)发现,c也与a不匹配。

正则表达式引擎内部的解释基于散布在regular-expressions.info周围的信息。

解决这个问题。只需删除其中一个+r'a+c'执行想要捕获a使用r'(a+)s'的数量。

最后,回答你的问题。我不认为这是Python的正则表达式引擎中的错误,但只是(如果有的话)缺乏优化逻辑。这个问题通常是不可解决的,所以对于一个引擎来说,假设你必须自己处理灾难性的回溯并不是太不合理。如果Perl足够聪明,能够识别出足够简单的案例,那就更好了。

答案 1 :(得分:4)

通过删除嵌套量词来重写正则表达式以消除"catastrophic backtracking"(请参阅this question):

re.match( '([a]+)+c', 'a' * 30 + 'b' )
# becomes
re.match( 'a+c', 'a' * 30 + 'b' )