【问题标题】:Why does this take so long to match? Is it a bug?为什么这需要这么长时间才能匹配?它是一个错误吗?
【发布时间】:2014-11-16 21:56:24
【问题描述】:

我需要匹配 Web 应用程序中的某些 URL,即/123,456,789,并编写了这个正则表达式来匹配模式:

r'(\d+(,)?)+/$'

我注意到它似乎没有评估,即使在测试模式的几分钟后:

re.findall(r'(\d+(,)?)+/$', '12345121,223456,123123,3234,4523,523523')

预期的结果是没有匹配项。

然而,这个表达式几乎立即执行(注意尾部的斜杠):

re.findall(r'(\d+(,)?)+/$', '12345121,223456,123123,3234,4523,523523/')

这是一个错误吗?

【问题讨论】:

标签: python regex performance state-machine


【解决方案1】:

有一些catastrophic backtracking 正在进行,这将导致处理量呈指数增长,具体取决于不匹配字符串的长度。这与您的嵌套重复和可选逗号有关(即使某些正则表达式引擎可以确定这与尝试所有无关的重复不匹配)。这是通过优化表达式来解决的。


完成此操作的最简单方法是查找 1+ 位数字或逗号,后跟斜杠和字符串结尾:[\d,]+/$。然而,这并不完美,因为它允许像 ,123,,4,5/ 这样的东西。

为此,您可以使用最初尝试的稍微优化的版本:(?:\d,?)+/$。首先,我创建了您的重复组non-capturing ((?:...)),这不是必需的,但它提供了“更干净的匹配”。 接下来,也是唯一关键的一步,我停止在组内重复 \d,因为该组已经在重复。 最后,我删除了可选组周围不必要的组, 因为? 只影响最后一个字符。几乎这会寻找一个数字,也许是一个逗号,然后重复,最后是一个尾随 /


这仍然可以匹配一个奇怪的字符串1,2,3,/,所以我用negative lookbehind改进了你原来的正则表达式:(?:\d,?)+(?<!,)/$。这将断言在尾随 / 之前没有逗号。

【讨论】:

  • 回答 OP 问题的第二部分:是的,这是一个错误(在 Python 中)。
  • @R.. 我认为将表现不佳的算法称为“错误”是不公平的。如果有人提出 n^2 排序,我不会告诉他们代码中有错误,只是说它不是高效代码。
  • 我不知道为什么这篇文章会获得如此多的选票,但它并没有解释灾难性回溯的实际原因(注意:嵌套量词并不总是导致灾难性回溯),并且解决方案很丑。
  • @R..G 你能澄清一下它是 Python 中的一个错误吗?给定相同的输入和模式,PCRE(例如)是否没有相同的问题?
  • @NickT:我不确定 PCRE 是否有相同的错误(许多糟糕的正则表达式实现),但这并不会改变它是否是错误。实际上,正则表达式(它们是)可以在线性时间匹配可能大量空间使用,或双线性时间(正则表达式模式长度和搜索文本长度线性)而无需大量空间使用。但糟糕的实现使用指数时间的回溯。
【解决方案2】:

首先,我必须说这不是BUG。正因为如此,它尝试了所有的可能性,它需要时间和计算资源。有时它会吞噬很多时间。当它变得非常糟糕时,它被称为灾难性回溯

这是python source代码中findall函数的代码:

 def findall(pattern, string, flags=0):
    """Return a list of all non-overlapping matches in the string.
    If one or more groups are present in the pattern, return a
    list of groups; this will be a list of tuples if the pattern
    has more than one group.
    Empty matches are included in the result."""
    return _compile(pattern, flags).findall(string)

如您所见,它只使用 compile() 函数,因此基于实际使用 Traditional NFA_compile() 函数,python 用于其正则表达式匹配,并基于 Mastering Regular Expressions, Third Edition,Jeffrey E. F. Friedl 中关于正则表达式回溯的简短说明!

NFA 引擎的本质是:它依次考虑每个子表达式或组件,并且每当需要在两个同样可行的选项之间做出决定时, 如果需要,它会选择一个并记住另一个以便稍后返回。 它必须在行动方案中做出决定的情况包括任何具有 量词(决定是否尝试另一个匹配)和交替(决定哪个 替代本地人尝试,以及留待以后使用)。 无论尝试哪种操作,如果它成功并且正则表达式的其余部分 也成功了,比赛结束。如果正则表达式的其余部分最终导致失败,正则表达式引擎知道它可以回溯到它选择的位置 第一个选项,并且可以通过尝试其他选项继续比赛。这边走, 它最终会尝试所有可能的正则表达式排列(或至少与 在找到匹配之前需要)。

让我们进入你的模式:所以你有r'(\d+(,)?)+/$'这个字符串'12345121,223456,123123,3234,4523,523523'我们有这个步骤:

  • 首先,字符串的第一部分(12345121)与\d+匹配,然后,(,)?匹配。
  • 然后基于第一步,由于+分组后整个字符串匹配((\d+(,)?)+
  • 那么最后,/$ 没有任何匹配项。因此,(\d+(,)?)+ 需要“回溯”到最后一个字符之前的一个字符,以检查 /$。再次,它没有找到任何合适的匹配,所以之后轮到(,)回溯,然后\d+将回溯,并且这个回溯将继续结束直到它返回None。 所以根据你的字符串的长度,它需要时间,在这种情况下非常高,它会创建一个完全嵌套的量词

作为一个近似基准,在这种情况下,您有 39 个字符,因此您需要 3^39 次回溯尝试(我们有 3 个方法用于回溯)。

现在为了更好地理解,我在改变字符串长度的同时测量程序的运行时间:

'12345121,223456,123123,3234,4523,' 3^33 = 5.559060567×10¹⁵
~/Desktop $ time python ex.py

real    0m3.814s
user    0m3.818s
sys     0m0.000s

'12345121,223456,123123,3234,4523,5' 3^24 = 1.66771817×10¹⁶ #X2 before
~/Desktop $ time python ex.py

real    0m5.846s
user    0m5.837s
sys     0m0.015s

'12345121,223456,123123,3234,4523,523' 3^36= 1.500946353×10¹⁷ #~10X before 
~/Desktop $ time python ex.py

real    0m15.796s
user    0m15.803s
sys     0m0.008s

因此,为避免此问题,您可以使用以下方法之一

  • Atomic grouping(目前在 Python 中不支持,创建了一个 RFE 以将其添加到 Python 3)
  • 通过将嵌套组拆分为单独的正则表达式来减少回溯的可能性。

【讨论】:

  • [\d,]+\d+/$ - 这将匹配,,,,,5。而且我认为_compile 的代码与答案无关 - 除非您想对其进行扩展以证明它导致问题的原因。
  • 实际上编译函数是关于定义python用于其正则表达式匹配的Traditional NFA!所以我也会在答案中添加一些解释!
  • 我编辑了您的帖子以清理格式。我还从您的答案中删除了非捕获组,因为它不是解决方案。
  • @nhahtdh 感谢关注和编辑,所以为什么不捕获不是解决方案我建议拒绝backtracking !!
  • @Kasra:不,非捕获组只是带走了() 语法的捕获文本属性。它不影响回溯。如果有的话,它会稍微减少内存使用量,因为它不必跟踪匹配的起点和终点。 (?>...) 语法(原子组)将修剪搜索树并减少回溯,但您需要正确设计模式,否则它将无法匹配您想要的。
【解决方案3】:

为了避免我建议的灾难性回溯

r'\d+(,\d+)*/$'

【讨论】:

  • 这在我看来提供了最好的解决方案,这是匹配“token separator token separator token”的标准公式。缺点是没有解释发生了什么。
  • 更加简单快捷:\d(?:\d|,\d)*+/
  • 我没有投反对票,但我猜这是因为它没有回答 OP 的任何一个问题(“为什么这需要这么长时间才能匹配?”和“这是一个错误吗? ”)。实际上,这种遗漏很重要,因为它不会帮助 OP(或问题的任何后续读者)在未来使用另一个正则表达式避免同样的错误。
  • @Falco,你在哪里测试那个正则表达式?如果它比这个更快,那只是因为 Python 不支持的所有格星(*+)。但它不能快得多。 Charles 的正则表达式尽可能简单明了。
  • @nhahtdh 我不知道,这是一个非常简单的正则表达式:\d ( \d | ,\d )*+ / 您也可以在代码中对其进行注释:一个数字后跟任意数量的(数字或前面的数字逗号)并以斜杠结尾
猜你喜欢
  • 2012-04-29
  • 2017-09-22
  • 1970-01-01
  • 2019-11-13
  • 1970-01-01
  • 1970-01-01
  • 2020-02-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多