为什么这需要很长时间才能匹配? 这是一个错误?
我需要匹配的Web应用程序中的某些url,即/123,456,789
,并写了这个正则expression式匹配的模式:
r'(\d+(,)?)+/$'
我注意到它似乎没有评估,即使在testing模式几分钟后:
re.findall(r'(\d+(,)?)+/$', '12345121,223456,123123,3234,4523,523523')
预期的结果是没有匹配。
然而,这个expression式几乎立即执行(注意结尾的斜线):
re.findall(r'(\d+(,)?)+/$', '12345121,223456,123123,3234,4523,523523/')
这是一个错误?
有一些灾难性的回溯正在进行,这将导致指数的处理量取决于不匹配string的长度。 这与你的嵌套重复和可选的逗号(即使一些正则expression式引擎可以确定这不会是尝试所有多余的重复匹配)。 这是通过优化expression式来解决的。
最简单的方法是只查找1+数字或逗号,后跟斜杠和string的结尾: [\d,]+/$
。 然而,这不是完美的,因为它会允许像,123,,4,5/
。
为此,您可以使用初始尝试的稍微优化的版本: (?:\d,?)+/$
。 首先,我让你的重复组不捕获 ( (?:...)
),这是不必要的,但它提供了一个“更清洁的匹配”。 接下来,也是唯一关键的一步,因为小组已经在重复,所以我不再重复小组内部的工作。 最后,我删除了可选的不必要的组,
因为?
只影响最后一个字符。 几乎这将会寻找一个数字,也许一个逗号,然后重复,最后跟着一个尾随/
。
这仍然可以匹配一个奇怪的string1,2,3,/
,所以对于它的heck,我改善了你的原始正则expression式负面向后看 : (?:\d,?)+(?<!,)/$
。 这将断言在尾部之前没有直接的逗号。
首先,我必须说这不是一个BUG 。 正因为如此,它尝试了所有的可能性,这需要时间和计算资源。 有时它可以吞噬很多时间。 当它变得非常糟糕时,它被称为灾难性的回溯 。
这是python源代码中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的正则expression式匹配使用Traditional NFA
,并基于这个简短的解释在Mastering正则expression式,正规expression式回溯第三版,由杰弗里EF弗里德尔 !
NFA
引擎的本质是这样的:它依次考虑每个子expression式或组件,并且每当需要在两个同样可行的选项之间做出决定时,就select一个并记住另一个,以便在需要时返回。 必须在行动过程中作出决定的情况包括任何有量词的人(决定是否尝试另一场比赛)和轮换(决定哪一个本来的尝试,以及哪个稍后离开)。 无论采取什么样的行动,如果成功,正则expression式的其余部分也是成功的,则匹配结束。 如果正则expression式的其余部分最终导致失败,则正则expression式引擎知道它可以回溯到它select第一个选项的位置,并且可以通过尝试另一个选项继续匹配。 这样,它最终尝试正则expression式的所有可能的排列(或者至lessfind所需的直到find一个匹配)。
让我们进入你的模式 :所以你有r'(\d+(,)?)+/$'
这个string'12345121,223456,123123,3234,4523,523523'
我们有这个步骤:
- 首先,string的第一部分(
12345121
)与\d+
匹配,然后与(,)?
匹配(,)?
。 - 然后基于第一步,整个string在分组之后由于
+
而匹配((\d+(,)?)+
) - 那么最后,没有什么可以匹配
/$
。 因此,(\d+(,)?)+
需要在最后检查/$
之前“回溯”到一个字符。 再次,它没有find任何适合的匹配,所以之后(())回到原点,那么\d+
将会回溯,并且这个回溯将继续结束直到它返回None
。 所以根据string的长度,需要花费很多时间,在这种情况下,这个时间非常长,并且它会完全创build一个嵌套的量词 !
作为一个近似的基准testing,在这种情况下,你有39个字符,所以你需要3 ^ 39回溯尝试 (我们有3 个回溯方法)。
现在为了更好的理解,我在测量程序的运行时间的同时改变了string的长度:
'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
所以为了避免这个问题,你可以使用下面的方法之一 :
- primefaces分组 (目前不支持Python,创build一个RFE将其添加到Python 3)
- 通过打破嵌套组来分开正则expression式来减less回溯的可能性。
为了避免灾难性的回溯,我build议
r'\d+(,\d+)*/$'