我不确定我是否完全理解以下正则表达式搜索的内容:
>>> import re
>>> template = re.compile("(\w+)+\.")
>>> target = "a" * 30
>>> template.search(target)
search()
调用需要几分钟才能完成,CPU使用率达到100%。对于2.7.5和3.3.3 Python版本,该行为都是可重现的。
有趣的是,如果字符串的长度小于20-25个字符,search()
会立即返回。
发生了什么事?
答案 0 :(得分:12)
了解此问题需要了解NFA在RegExp下的工作方式。
阐述NFA的定义可能对我来说太过沉重。在维基上搜索NFA,它会为您提供更好的解释。这里只是认为NFA是一个机器人发现你给出的模式。
粗暴地实施NFA有些愚蠢,它只是向前看你给的一两个代币。因此,在您给出的合成示例中,NFA首先看起来\w+
(不是用于分组的括号)。
因为+
是一个贪婪的量词,也就是说,匹配尽可能多的字符,所以NFA勉强继续使用target
中的字符。在30 a
之后,NFA遇到字符串的结尾。之后,NFA意识到他需要匹配template
中的其他令牌。
下一个标记是+
。 NFA已匹配,因此会转到\.
。这次失败了。
NFA接下来要做的是先退一步,尝试通过截断\w+
的子匹配来匹配模式。因此,NFA将target
拆分为两个组,一个a
为29个\w+
,另一个为a
。 NFA首先尝试通过将其与第二个+
匹配来使用尾随a,但在NFA会议\.
时仍然会失败。 NFA继续上述过程,直到获得完全匹配,否则它将尝试所有可能的分区。
因此(\w+)+\.
指示NFA以这种方式对target
进行分组:将目标划分为一个或多个组,每个组至少一个字符,目标以句点结束'。 &#39 ;.只要期间不匹配。 NFA尝试所有分区。那么有多少分区? 2 ^ n,指数为2.(JUst认为在a
之间插入分隔符)。如下所示
aaaaaaa a
aaaaaa aa
aaaaaa a a
.....
.......
aa a a ... a
a a a a a .... a
如果NFA与\.
匹配,则不会造成太大伤害。但是当它无法匹配时,这个表达式注定永无止境的指数。
我不是广告,但掌握正则表达式是理解RegExp机制的好书。
答案 1 :(得分:5)
缓慢是由引擎的回溯引起的:
(\w+)+\.
如果字符串末尾没有.
,则此模式会自然发生回溯。引擎将首先尝试匹配尽可能多的\w
并在发现在字符串结束之前需要匹配更多字符时进行回溯。
(a x 59) .
(a x 58) .
...
(a) .
最后它将无法匹配。但是,模式中的第二个+
会导致引擎检查(n-1)!
个可能的路径,因此:
(a x 58) (a) .
(a x 57) (a) (a) .
(a x 57) (a x 2) .
...
(a) (a) (a) (a) (a) (a) (a) ...
删除+
可以防止异常的回溯:
(\w+)\.
某些实现还将支持占有量词,在这种特定场景中可能更为理想:
(\w++)\.
答案 2 :(得分:1)
第二个问题是导致问题:
template = re.compile("(\w+)\.")
对我来说很好。要查看正则表达式的解析树,请将re.DEBUG作为第二个参数传递给:
import re
re.compile("(\w+)+\.", re.DEBUG)
print "\n\n"
re.compile("(\w+)\.", re.DEBUG)
max_repeat 1 65535
subpattern 1
max_repeat 1 65535
in
category category_word
literal 46
subpattern 1
max_repeat 1 65535
in
category category_word
literal 46
处理完成,退出代码为0
这证明了第二个加号是添加一个循环,python正则表达式解析器必须在65535上限。这有点证明了我的理论。
请注意,要运行它,您需要为每次执行提供一个新的python解释器。 re.compile memoizes传入的值,因此它不会重新编译相同的正则表达式两次,例如在ipython中重复运行它不会在第一次运行后打印出解析树。