以下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'的数量并不会显着改变执行速度。
答案 0 :(得分:13)
我认为Perl足够聪明,可以将两个+
合并为一个,而Python则不然。现在让我们想象一下引擎的功能,如果没有优化的话。请记住,捕获通常很昂贵。另请注意,两个+
都是贪婪的,因此引擎将尝试在一个回溯步骤中尽可能多地使用重复。每个项目符号代表一个回溯步骤:
[a]
,并消耗所有30 a
秒。然后它不能再进一步,所以它留下了第一次重复和捕获 30 a
s。现在下一个重复开始,它尝试用另一个([a]+)
消耗更多,但这当然不起作用。然后c
无法匹配b
。a
。在此之后我们再次离开内部重复,因此引擎将捕获 29 a
s。然后另一个+
开始,内部重复再次尝试(消耗第30 a
)。然后我们再次离开内部重复,这也会离开捕获组,因此第一次捕获被丢弃,引擎捕获最后a
。 c
无法匹配b
。a
。我们捕获 28 a
。捕获组的第二个(外部重复)消耗最后2个a
捕获。 c
无法匹配b
。a
中的第二个。剩下的那个将是捕获。然后我们第三次进入捕获组,捕获最后一次a
。 c
无法匹配b
。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' )