为什么这需要很长时间才能匹配?这是一个错误吗?

时间:2014-09-22 20:14:52

标签: python regex performance state-machine

我需要匹配网络应用程序中的某些网址,即/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/')

这是一个错误吗?

3 个答案:

答案 0 :(得分:55)

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


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

为此,您可以使用初始尝试的略微优化版本:(?:\d,?)+/$。首先,我制作了重复组non-capturing(?:...)),这不是必需的,但它提供了“更清晰的匹配”。 接下来,唯一关键的一步,我已经停止重复组中的\d,因为该组已在重复。 最后,我删除了不必要的组可选,,因为?仅影响最后一个字符。几乎这将寻找一个数字,可能是一个逗号,然后重复,最后跟着一个尾随/


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

答案 1 :(得分:32)

首先,我必须说它不是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()函数,因此基于_compile()函数实际使用python用于其正则表达式匹配的Traditional NFA,并基于 这篇简短的解释是关于Jeffrey E. F. Friedl 掌握正则表达式第三版的正则表达式的回溯!

  

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

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

  • 首先,字符串的第一部分(12345121)与\d+匹配,然后,(,)?匹配。
  • 然后根据第一步,整个字符串在分组后由+匹配((\d+(,)?)+
  • 然后最后,/$没有任何内容可供匹配。因此,(\d+(,)?)+需要在最后一个字符之前“回溯”到一个字符,以便检查/$。同样,它没有找到任何正确的匹配,所以在那之后(,)转向回溯,然后\d+将回溯,并且这种回溯将继续结束,直到它返回{{ 1}}。 因此,基于字符串的长度需要时间,在这种情况下非常高,并且它会完全创建嵌套量词

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

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

None

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

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

答案 2 :(得分:30)

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

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