为什么python正则表达式这么慢?

时间:2014-10-06 10:28:31

标签: python regex

经过长时间的调试后,我发现为什么使用python regexps的应用程序运行缓慢。这是令我惊讶的事情:

import datetime
import re

pattern = re.compile('(.*)sol(.*)')

lst = ["ciao mandi "*10000 + "sol " + "ciao mandi "*10000,
       "ciao mandi "*1000 + "sal " + "ciao mandi "*1000]
for s in lst:
    print "string len", len(s)
    start = datetime.datetime.now()
    re.findall(pattern,s)
    print "time spent", datetime.datetime.now() - start
    print

我机器上的输出是:

string len 220004
time spent 0:00:00.002844

string len 22004
time spent 0:00:05.339580

第一个测试字符串长220K,匹配,匹配非常快。第二个测试字符串是20K长,不匹配,计算需要5秒!

我知道这个报告http://swtch.com/~rsc/regexp/regexp1.html,它说python,perl,ruby中的regexp实现有点不理想......这是什么原因?我没想到会发生这么简单的表达。

加入 我的原始任务是依次拆分一个字符串尝试不同的正则表达式。类似的东西:

for regex in ['(.*)sol(.*)', '\emph{([^{}])*)}(.*)', .... ]:
    lst = re.findall(regex, text) 
    if lst:
        assert len(lst) == 1
        assert len(lst[0]) == 2
        return lst[0]

这是为了解释为什么我不能使用split。我按照Martijn的建议将(.*)sol(.*)替换为(.*?)sol(.*)解决了我的问题。

可能我应该使用match代替findall ...但我不认为这会解决问题,因为正则表达式将匹配整个输入,因此找到应该在第一场比赛中停止。

无论如何,我的问题更多的是关于一个正则表达式新手有多容易陷入这个问题...我认为理解(.*?)sol(.*)是解决方案并不是那么简单(例如{{1}不是)。

3 个答案:

答案 0 :(得分:18)

Thompson NFA方法将正则表达式从默认贪婪更改为默认非贪婪。普通正则表达式引擎可以做同样的事情;只需将.*更改为.*?即可。当非贪婪时,你不应该使用贪婪的表达。

有人已经为Python构建了一个NFA正则表达式解析器:https://github.com/xysun/regex

它确实胜过病态案例的默认Python正则表达式解析器。 然而

  

这个正则表达式引擎在正常输入上表现不如Python的模块(使用Glenn Fowler的测试套件 - 见下文)

以牺牲典型为代价来修复病理情况可能是不使用NFA方法作为默认引擎的一个很好的理由,而不是在可以简单地避免病理情况时。

另一个原因是某些功能(例如反向引用)要么非常难以实现,要么无法使用NFA方法实现。另请参阅response on the Python Ideas mailing list

因此,如果您至少使其中一个模式不贪婪,以避免灾难性的回溯,那么您的测试可以表现得更好:

pattern = re.compile('(.*?)sol(.*)')

或根本不使用正则表达式;您可以使用str.partition()来获取前缀和后缀:

before, sol, after = s.partition('sol')

e.g。并非所有的文本问题都是正则表达形式的,所以放下锤子并查看工具箱的其余部分!

此外,您可以查看替代re模块regex。该模块对病理案例进行了一些基本检查,并且可以灵活地避免它们,而无需诉诸Thompson NFA实现。引用an entry to a Python bug report tracking regex

  

内部引擎不再解释字节码的形式,而是解释   跟随一组链接的节点,它既可以广度也可以   深度优先,这使得它在面对其中一个时表现得更好   那些“病态的”正则表达。

这个引擎可以让您的病理情况快10万倍:

>>> import re, regex
>>> import timeit
>>> p_re = re.compile('(.*)sol(.*)')
>>> p_regex = regex.compile('(.*)sol(.*)')
>>> s = "ciao mandi "*1000 + "sal " + "ciao mandi "*1000
>>> timeit.timeit("p.findall(s)", 'from __main__ import s, p_re as p', number=1)
2.4578459990007104
>>> timeit.timeit("p.findall(s)", 'from __main__ import s, p_regex as p', number=100000)
1.955532732012216

注意数字;我将re测试限制为1次运行并且花了2.46秒,而regex测试在2秒内运行了100k次。

答案 1 :(得分:6)

我认为这与灾难性的回溯无关(或者至少是我对它的理解)。

问题是由(.*)中的第一个(.*)sol(.*)引起的,以及正则表达式没有锚定在任何地方的事实。

re.findall(),在索引0失败后,将在索引1,2等处重试,直到字符串结束。

badbadbadbad...bad
^                   Attempt to match (.*)sol(.*) from index 0. Fail
 ^                  Attempt to match (.*)sol(.*) from index 1. Fail
  ^                 Attempt to match (.*)sol(.*) from index 2. Fail (and so on)

它实际上具有二次复杂度O(n 2 ),其中n是字符串的长度。

问题可以通过锚定您的模式来解决,因此它会在您的模式无法匹配的位置立即失败。 (.*)sol(.*)将在一行文本中搜索sol(由行终止符分隔),因此如果它无法在该行的开头找到匹配项,则无法找到任何其余部分。

因此,您可以使用:

^(.*)sol(.*)

re.MULTILINE选项。

运行此测试代码(从您的代码修改):

import datetime
import re

pattern = re.compile('^(.*)sol(.*)', re.MULTILINE)

lst = ["ciao mandi "*10000 + "sol " + "ciao mandi "*10000,
       "ciao mandi "*10000 + "sal " + "ciao mandi "*10000]
for s in lst:
    print "string len", len(s)
    start = datetime.datetime.now()
    re.findall(pattern,s)
    print "time spent", datetime.datetime.now() - start
    print

(请注意,传递和失败都是220004个字符)

给出以下结果:

string len 220004
time spent 0:00:00.002000

string len 220004
time spent 0:00:00.005000

这清楚地表明两种情况现在具有相同的数量级。

答案 2 :(得分:0)

^(?=(.*?sol))\1(.*)$

你可以尝试这个。这可以减少回溯并且更快地失败。在这里尝试你的字符串。

http://regex101.com/r/hQ1rP0/22