为什么python正则表达式如此缓慢?

为什么python正则表达式如此缓慢?,python,regex,Python,Regex,经过长时间的调试,我发现了为什么我使用python正则表达式的应用程序速度慢。以下是我感到惊讶的事情: 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:

经过长时间的调试,我发现了为什么我使用python正则表达式的应用程序速度慢。以下是我感到惊讶的事情:

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秒

我知道这个报告说,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
。。。但我认为这并不能解决这个问题,因为regexp将匹配整个输入,因此findall应该在第一次匹配时停止

不管怎样,我的问题更多的是关于一个regexp新手陷入这个问题有多容易。。。我认为理解
(.*)sol(.*)
是解决方案并不是那么简单(例如
(.*)sol(.*)
不是)

您可以尝试此操作。这可以减少回溯并更快地失败。请在此处尝试您的字符串


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

有人已经为Python构建了NFA正则表达式解析器:

对于病理情况,它确实优于默认的Python正则表达式解析器。但是,它在下执行以下所有操作:

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

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

另一个原因是某些特性(如反向引用)很难或不可能使用NFA方法实现。另请参阅

因此,如果您使至少一种模式非贪婪以避免灾难性的回溯,您的测试可以执行得更好:

pattern = re.compile('(.*?)sol(.*)')
或者根本不使用正则表达式;您可以使用
str.partition()
来获取前缀和后缀:

before, sol, after = s.partition('sol')
e、 不是所有的文本问题都是正则表达式形式的,所以放下锤子,看看工具箱的其他部分

此外,您还可以查看可选的
re
模块。该模块实现了一些病理病例的基本检查,并灵活地避免了这些检查,而不必求助于Thompson NFA实现。引述:

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

此引擎可以使您的病理病例运行速度提高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秒的时间内运行了10万次。

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

问题是由
(.*)sol(.*)
中的第一个
(.*)
以及regex未锚定在任何位置这一事实引起的

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(n2),其中n是字符串的长度

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)
这个问题可以通过锚定您的模式来解决,因此它会在您的模式没有机会匹配的位置立即失败
(.*)sol(.*)
将在一行文本(由行终止符分隔)中搜索
sol
,因此,如果它在行的开头找不到匹配项,它将找不到该行其余部分的匹配项

因此,您可以使用:

^(.*)sol(.*)
有选择权

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

(请注意,通过和未通过均为220004个字符)

给出以下结果:

string len 220004
time spent 0:00:00.002000

string len 220004
time spent 0:00:00.005000

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

不,原因不是实施。原因是这两个
*
过于宽松,会导致灾难性的回溯。你到底想做什么?@casimir,灾难性的回溯是实现问题。阅读由Emanuele链接的文章。@alexis:不,本例中的灾难性回溯是由于模式概念。您将获得与其他NFA引擎大致相同的结果。“其他NFA引擎”具有相同的实现。真正的FSA不需要任何回溯。请注意:问题不是由双
(.*)
引起的。搜索
(*)sol
具有完全相同的时间配置文件。事实上,如果字符串包含
sol
,并且您使用
findall()
进行搜索,则
(.*)sol
实际上更糟糕,因为它会在
sol
后面的子字符串上触发失败的回溯搜索。(成功后,原始RE将消耗整个字符串)。或者
str.split()
,因为他使用
findall()
表明需要多个位置。@alexis:我的意思是更多地表明工具箱中有更多的工具!:-)在我的用例中,我想将一些文本拆分为两部分,但是拆分是由许多不同的正则表达式(我用|连接)实现的
string len 220004
time spent 0:00:00.002000

string len 220004
time spent 0:00:00.005000