【问题标题】:Very slow regular expression search非常慢的正则表达式搜索
【发布时间】:2014-05-04 05:05:08
【问题描述】:

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

>>> 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() 很快就会返回。

发生了什么?

【问题讨论】:

标签: python regex string performance


【解决方案1】:

了解这个问题需要了解 NFA 在 RegExp 下的工作原理。

详细说明 NFA 的定义对我来说可能是一项太重的任务。在 wiki 上搜索 NFA,它会给你一个更好的解释。这里只是认为 NFA 是一个寻找你给出的模式的机器人。

粗略实现的 NFA 有点愚蠢,它只是向前看你给出的一两个令牌。因此,在您给出的综合示例中,NFA 一开始只是看起来\w+(不是用于分组的括号)。

因为+是一个贪心量词,也就是匹配尽可能多的字符,所以NFA傻傻地继续消费target中的字符。在 30 as 之后,NFA 遇到字符串结尾。之后 NFA 是否意识到 he 需要匹配template 中的其他令牌。 下一个令牌是+。 NFA 已经匹配了它,所以它继续到\.。这次失败了。

NFA 下一步要做的是退后一个字符,尝试通过截断\w+ 的子匹配来匹配模式。所以 NFA 将target 分成两组,29 个as 一组\w+,一组尾随a。 NFA 首先尝试通过将尾随 a 与第二个 + 匹配来消耗它,但当 NFA 遇到 \. 时它仍然失败。 NFA 继续上述过程,直到获得完全匹配,否则它将尝试所有可能的分区。

所以(\w+)+\. 指示 NFA 以这样的方式对target 进行分组:目标被分成一个或多个组,每组至少一个字符,目标以句点“.”结尾。只要期间不匹配。 NFA 尝试所有可能的分区。那么有多少个分区呢? 2^n,2 的指数。(想想在a 之间插入分隔符)。如下所示

aaaaaaa a
aaaaaa aa
aaaaaa a a
.....
.......
aa a a ... a
a a a a a .... a

如果 NFA 匹配 \.,它不会受到太大伤害。但是当匹配失败时,这个表达式就注定是永无止境的指数了。

我不是打广告,但Mastering Regular Expression是一本了解RegExp机制的好书。

【讨论】:

  • 顺便说一句,RegExp 引擎有很多优化,它们比我上面描述的更智能。因此,在高级 RegExp 引擎中不太可能发生指数级噩梦。
【解决方案2】:

缓慢是由于引擎回溯造成的:

(\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++)\.

【讨论】:

    【解决方案3】:

    第二个加号导致问题:

    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 会记住传入的值,因此它不会重新编译相同的正则表达式两次,例如在 ipython 中重复运行它不会在第一次运行后打印出解析树。

    【讨论】:

      猜你喜欢
      • 2020-06-21
      • 2021-11-25
      • 2018-11-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多