非常慢的正则表达式搜索

时间:2014-03-26 02:02:16

标签: python regex string performance

我不确定我是否完全理解以下正则表达式搜索的内容:

>>> 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()会立即返回。

发生了什么事?

3 个答案:

答案 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中重复运行它不会在第一次运行后打印出解析树。