我需要匹配网络应用程序中的某些网址,即/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/')
这是一个错误吗?
答案 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
为避免此问题,您可以使用以下方法之一:
答案 2 :(得分:30)
为了避免灾难性的回溯我建议
r'\d+(,\d+)*/$'