【问题标题】:why python regex is so slow?为什么python正则表达式这么慢?
【发布时间】:2014-10-06 10:28:31
【问题描述】:

经过长时间的调试,我发现为什么我的应用程序使用 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秒!

我知道这份报告http://swtch.com/~rsc/regexp/regexp1.html 说python、perl、ruby 中的正则表达式实现在某种程度上不是最佳的……这是原因吗?没想到这么简单的表达方式会发生这种事。

添加 我最初的任务是拆分一个字符串,依次尝试不同的正则表达式。比如:

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... 但我认为这不会解决问题,因为正则表达式将匹配整个输入,因此 findall 应该在第一次匹配时停止。

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

【问题讨论】:

  • 不,原因不是执行。原因是这两个.*过于放纵,造成灾难性的回溯。你到底想做什么?
  • @casimir,灾难性的回溯的实现问题。阅读 Emanuele 链接的文章。
  • @alexis:不,这种情况下的灾难性回溯是由于模式概念造成的。使用其他 NFA 引擎,您将获得或多或少相同的结果。
  • 阅读文章。 “其他 NFA 引擎”具有相同的实现。真正的 FSA 不需要任何回溯。
  • 再注意:问题不是由双(.*)引起的。搜索(.*)sol 具有完全相同的时间配置文件。事实上,如果字符串包含sol 并且您使用findall() 搜索,(.*)sol 实际上更糟糕,因为它会触发对sol 后面的子字符串的回溯搜索失败。 (原始的 RE 将在成功时消耗整个字符串)。

标签: python regex


【解决方案1】:

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

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

对于病理情况,它确实优于默认的 Python 正则表达式解析器。 然而,它-执行所有其他

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

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

另一个原因是使用 NFA 方法很难或不可能实现某些功能(例如反向引用)。另请参阅response on the Python Ideas mailing list

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

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

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

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

例如并非所有文本问题都是正则表达式,所以放下锤子,看看你工具箱的其余部分!

此外,您或许可以查看替代的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 秒内运行 10 万次。

【讨论】:

  • str.split(),因为他使用findall() 表明需要多个职位。
  • @alexis:我的意思是更多地表明工具箱中有更多工具! :-)
  • 在我的用例中,我想将一些文本分成两部分,但拆分是通过许多不同的正则表达式(我用 | 连接)实现的,其中一些比这更复杂(但不是那么慢...)。因此,最好将此匹配保留在正则表达式中。
  • @EmanuelePaolini:对于 splitting,你可以使用re.split(),然后你就不需要在@之前和之后的部分使用.* 987654341@文字。
  • the upcoming regex module 有什么问题?对于大多数病理性问题来说,这速度更快。对于这个例子,它已经足够快了。无论如何,我投了反对票,因为我认为通过将高度优化的 C 实现与仓促制作的纯 Python 实现进行比较得出性能结论是我见过任何人做过的最糟糕的基准测试。
【解决方案2】:

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

问题是由(.*)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(n2),其中 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

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

【讨论】:

  • 这很有趣……事实上,问题在于re.search 很慢,而re.match 很快。但是,我尝试使用 awk 进行相同的搜索(但我不是 100% 确定我使用了等效模式),并且似乎对于 awk,搜索和匹配需要相同的时间。也许关键是,使用 NFA 方法,我可以在线性时间实现搜索,其中重复匹配需要二次时间。
  • @EmanuelePaolini:awk 不使用回溯引擎。这就是为什么它很快。
  • 这不是病态的回溯吗?
  • @Veedrac:这更多是顶层的行为(在当前索引的所有可能性都用尽后,引擎前进到下一个索引)。从技术上讲,您可以将其定义为“灾难性回溯”,但它与 (a*)* 的情况不同,在这种情况下,由于引擎允许 aa 扩展而导致问题发生。两种情况的解决方案也不同。但我同意归根结底,任何效率低下都是由于过度回溯造成的。
【解决方案3】:
^(?=(.*?sol))\1(.*)$

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

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

【讨论】:

  • 是什么让您认为它失败得更快并且回溯更少?
  • 因此您正在捕获并立即反向引用捕获的组,并声称通过额外的捕获更快地失败;是什么赋予了?嗯,这确实需要更长的时间才能成功,而且并没有真正更快地失败。
  • @Unihedron 好吧,我在 regexhero.net 上进行了检查...在一个小字符串中失败时速度提高了 132%,成功时速度提高了 50%。我想这是一个重大改进
  • @Jerry 它也需要更少的步骤
  • @vks 您已经在自己的问题中被告知,步数并不是影响速度的最终因素。唯一比 OP 的正则表达式稍快的是锚点。在 OP 的正则表达式上使用锚点,这比 OP 当前的正则表达式慢得多。
猜你喜欢
  • 1970-01-01
  • 2019-03-12
  • 2014-01-23
  • 2020-11-27
  • 2012-06-30
  • 2012-10-08
  • 2018-05-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多