【问题标题】:Regular Expressions in Python unexpectedly slowPython中的正则表达式出乎意料地慢
【发布时间】:2012-06-26 19:07:46
【问题描述】:

考虑一下这个 Python 代码:

import timeit
import re

def one():
        any(s in mystring for s in ('foo', 'bar', 'hello'))

r = re.compile('(foo|bar|hello)')
def two():
        r.search(mystring)


mystring="hello"*1000
print([timeit.timeit(k, number=10000) for k in (one, two)])
mystring="goodbye"*1000
print([timeit.timeit(k, number=10000) for k in (one, two)])

基本上,我正在对两种方法进行基准测试,以检查大字符串中是否存在多个子字符串之一。

我在这里得到的(Python 3.2.3)是这个输出:

[0.36678314208984375, 0.03450202941894531]
[0.6672089099884033, 3.7519450187683105]

在第一种情况下,正则表达式很容易击败 any 表达式 - 正则表达式会立即找到子字符串,而 any 必须检查整个字符串几次才能找到正确的子字符串。

但是在第二个例子中发生了什么?在子字符串不存在的情况下,正则表达式的速度非常慢!这让我感到惊讶,因为理论上正则表达式只需要遍历字符串一次,而 any 表达式必须遍历字符串 3 次。这里有什么问题?我的正则表达式有问题,还是在这种情况下 Python 正则表达式很慢?

【问题讨论】:

  • 至少我可以确认时间,但我不知道为什么。 +1 用于奇怪和可复制粘贴的示例。
  • @JBernardo:你的意思是代替one() 中的元组?实际上,这没有什么区别。使用“foo|bar|hello”删除正则表达式中的组会使正则表达式花费 3.5 秒而不是 3.7 秒,但这仍然很慢。
  • 如果您使用较长的字符串 ('foo'*1000) 并排除简单的长度测试(将 hello*1000 更改为 hello*200),您会发现 regexp 通常较慢,不仅仅是在第二个示例中.
  • 猜测,也许简单的字符串搜索已经过优化,因此它比较每个汇编指令的多个字节(使用一些 SIMD 指令?),而正则表达式不是。
  • @hamster: 'foo'*1000 不是一个很有趣的案例,因为any 表达式会立即短路。并且在这种情况下,正则表达式仍然更快,至少在这里 - 不过,正如您所期望的那样,它们大致相同。

标签: python regex python-3.x


【解决方案1】:

我的同事找到了 re2 库 (https://code.google.com/p/re2/)?有一个 python 包装器。在某些系统上安装有点麻烦。

我在处理一些复杂的正则表达式和长字符串时遇到了同样的问题——re2 显着加快了处理时间——从几秒到几毫秒。

【讨论】:

    【解决方案2】:

    未来读者须知

    我认为正确的答案实际上是 Python 的字符串处理算法真的针对这种情况进行了优化,而 re 模块实际上要慢一些。我在下面写的是真的,但可能与我在问题中的简单正则表达式无关。

    原答案

    显然这不是偶然的——Python 的re 模块确实比较慢。看起来它在找不到匹配项时使用递归回溯方法,而不是构建 DFA 并对其进行模拟。

    即使正则表达式中没有反向引用,它也使用回溯方法!

    这意味着在最坏的情况下,Python 正则表达式需要指数而非线性时间!

    这是一篇描述该问题的非常详细的论文: http://swtch.com/~rsc/regexp/regexp1.html

    我认为这张接近结尾的图表简洁地总结了它:

    【讨论】:

    • 非常正确。我已经在 SO 上多次看到朴素的字符串搜索算法优于等效的正则表达式超过一个数量级。这不仅是 Python 问题,而且很遗憾,因为优化引擎可以使这类正则表达式变得异常快。
    • 请注意,指数运行时间只发生在非常非常特殊的情况下(论文称它们为病态)
    • @Niklas:论文认为优化器是问题的一部分,而不是解决方案。另外,当然,您认为这是一个病态案例是正确的。我已经编辑了答案,提到我说的是最坏的情况。
    • 在这种情况下,正则表达式匹配算法的回溯行为并不重要,我们从不备份搜索的字符串(忽略b)。比较使用字符串函数搜索xy 的字符串与[xy] 正则表达式。每个字节被比较两次,正则表达式从不回溯,但仍然慢得多。
    • @cha0site:问题在于现代正则表达式实现比经典的正式正则语言表达式更强大。如果涉及反向引用,我们就不能再为正则表达式构造 NFA,因此我们不能使用基于 DFA 的极快的方法。我认为逐案优化是要走的路。当然,如果可能的话,一个好的优化可能是构建一个最小的 DFA。 编辑: 刚刚看到论文的相关部分:“一个特别聪明的实现可以将两者结合起来,仅采用回溯来适应反向引用。”
    【解决方案3】:

    你有一个由三个正则表达式组成的正则表达式。如果正则表达式不检查这三遍,您认为这究竟是如何工作的? :-) 计算没有魔法,你仍然需要做三项检查。

    但是正则表达式将逐个字符地进行每三个测试,而“one()”方法将检查整个字符串是否有一个匹配项,然后再进行下一个匹配项。

    在第一种情况下,正则表达式要快得多,因为您检查最后匹配的字符串。这意味着one() 需要首先查看整个字符串以查找“foo”,然后查找“bar”,然后查找匹配的“hello”。先移动“hello”,one() 和 two() 的速度几乎一样,因为在这两种情况下完成的第一个匹配都成功了。

    正则表达式是比“in”复杂得多的测试,所以我预计它会更慢。我怀疑当您使用“|”时这种复杂性会增加很多,但是我还没有阅读 regexp 库的源代码,所以我知道什么。 :-)

    【讨论】:

    • 计算中有很多魔法。比如Aho-Corasik Algorithm会更快地解决这个问题,正则表达式算法可以合并这个东西。
    • 其实,没有。正则表达式基本上是一种描述确定性有限自动机 (DFA) 的领域特定语言——好吧,python 正则表达式可以有反向引用,这会破坏它,但我的正则表达式没有任何反向引用。根据 DFA 检查字符串应该是 O(m*n),其中 m 是 DFA 的长度,n 是字符串的长度。这不是魔术,这是理论。此外,正则表达式非常流行且易于理解。我希望它们得到优化——Python 的 re 模块是用 C 语言编写的。
    • @cha0site:优化并没有消除检查三个不同事物时进行三个检查的必要性。
    • @unkulunkulu:Aho-Corasik 算法在这里没有多大帮助,因为三个字符串之间的重叠非常小。因此,您仍然需要检查三个不同的东西。但是,是的,在这种情况下,一个更好优化的引擎应该或多或少一样快,而在这种情况下,Python 的重新引擎要慢得多。并称其为“魔术”是愚蠢的。
    • @LennartRegebro 如果您使用基于 DFA 的正则表达式算法,您实际上只需要对输入字符串中的每个字符执行一次操作。您一次只能进入一个可能的 DFA 节点,并且您对每个输入字符执行一个节点转换。
    【解决方案4】:

    正则表达式之所以这么慢,是因为它不仅要遍历整个字符串,而且还要对每个字符进行多次计算。

    第一个只是这样做:

    Does f match h? No.
    Does b match h? No.
    Does h match h? Yes.
    Does e match e? Yes.
    Does l match l? Yes.
    Does l match l? Yes.
    Does o match o? Yes.
    Done. Match found.
    

    第二个这样做:

    Does f match g? No.
    Does b match g? No.
    Does h match g? No.
    Does f match o? No.
    Does b match o? No.
    Does h match o? No.
    Does f match o? No.
    Does b match o? No.
    Does h match o? No.
    Does f match d? No.
    Does b match d? No.
    Does h match d? No.
    Does f match b? No.
    Does b match b? Yes.
    Does a match y? No.
    Does h match b? No.
    Does f match y? No.
    Does b match y? No.
    Does h match y? No.
    Does f match e? No.
    Does b match e? No.
    Does h match e? No.
    ... 999 more times ...
    Done. No match found.
    

    我只能推测any 和正则表达式之间的区别,但我猜正则表达式速度较慢主要是因为它运行在一个高度复杂的引擎中,并且有状态机的东西和所有东西,它只是不是与特定实现一样高效 (in)。

    在第一个字符串中,正则表达式几乎会立即找到匹配项,而any 必须在字符串中循环两次才能找到任何内容。

    然而,在第二个字符串中,any 执行与正则表达式基本相同的步骤,但顺序不同。这似乎表明any 解决方案更快,可能是因为它更简单。

    特定代码比通用代码更有效。任何关于问题的知识都可以用于优化解决方案。简单代码优于复杂代码。本质上,当模式接近字符串开头时,正则表达式更快,但in 在模式接近字符串结尾或未找到时更快完全没有。

    免责声明:我不了解 Python。我知道算法。

    【讨论】:

    • 当然,但这是字符串中每个字符 3 次操作。 any 遍历字符串 3 次。在这种情况下,我希望它们大致相同,但正则表达式要慢一个数量级......
    • @cha0site:查看更新后的答案。差异似乎很大,我将其归因于正则表达式引擎的低效率。
    • 我想我找到了答案 - 这不是随机的低效率,Python 正则表达式使用慢速算法。我正在写它以获得答案。
    猜你喜欢
    • 1970-01-01
    • 2020-10-27
    • 1970-01-01
    • 2014-07-01
    • 2019-12-28
    • 2023-03-17
    • 2012-12-21
    • 2015-09-18
    • 2020-06-21
    相关资源
    最近更新 更多