我写了一个小的,天真的正则表达式,应该在括号内找到文本:
re.search(r'\((.|\s)*\)', name)
我知道这不是出于某些原因这样做的最佳方式,但它工作得很好。我正在寻找的只是一个解释,为什么对于某些字符串,这个表达式开始呈指数级增长然后永远不会完成。昨晚,在运行此代码数月之后,我们的一台服务器突然卡住了匹配类似于以下内容的字符串:
x (y) z
我已经对它进行了实验,并确定在“y”和“z”之间的每个空间所花费的时间都会翻倍:
In [62]: %timeit re.search(r'\((.|\s)*\)', 'x (y)' + (22 * ' ') + 'z')
1 loops, best of 3: 1.23 s per loop
In [63]: %timeit re.search(r'\((.|\s)*\)', 'x (y)' + (23 * ' ') + 'z')
1 loops, best of 3: 2.46 s per loop
In [64]: %timeit re.search(r'\((.|\s)*\)', 'x (y)' + (24 * ' ') + 'z')
1 loops, best of 3: 4.91 s per loop
但是除了空格之外的其他字符也没有相同的效果:
In [65]: %timeit re.search(r'\((.|\s)*\)', 'x (y)' + (24 * 'a') + 'z')
100000 loops, best of 3: 5.23 us per loop
注意:我不是在寻找更好的正则表达式或此问题的其他解决方案。我们不再使用它了。
答案 0 :(得分:6)
正如CaffGeek的答案正确暗示的那样,问题是由catastrophic backtracking的一种形式引起的。这两个选项都匹配一个空格(或制表符),这是无限次地贪婪地应用。此外,点匹配右括号,因此一旦打开paren匹配,此表达式始终匹配字符串的最末端,然后必须艰苦地回溯以找到结束括号。在此回溯过程中,在每个位置尝试另一种替代方案(对于空格或制表符也是成功的)。因此,必须在引擎可以回溯一个位置之前尝试每个可能的匹配组合序列。在收盘后有很多空间,这很快就会增加。通过简单地使星形量化器变得懒惰(即r'\((.|\s)*?\)'
),可以解决存在匹配关闭paren的情况下的具体问题,但是对于存在开口的非匹配情况,仍然存在失控的正则表达式问题。 paren在主题字符串中没有匹配的关闭paren。
原始正则表达式确实是,真的糟糕! (如果有多对,也不能正确匹配关闭的parens。)
匹配最里面括号的正确表达式(对于匹配和非匹配情况都非常快),当然是:
re_innermostparens = re.compile(r"""
\( # Literal open paren.
[^()]* # Zero or more non-parens.
\) # Literal close paren.
""", re.VERBOSE)
这一切都在Jeffrey Friedl的必须阅读的正则表达式作者:Mastering Regular Expressions (3rd Edition)中进行了详细解释(详尽的例子和推荐的最佳实践)。我可以诚实地说,这是我读过的最有用的书。正则表达式是一个非常强大的工具,但是像装载的武器一样必须非常小心和精确地应用(或者你会用脚射击自己!)
答案 1 :(得分:5)
可能是ReDos问题。
请参阅:http://en.wikipedia.org/wiki/ReDoS
可以在构造不良的正则表达式上创建正则表达式拒绝服务。
例如,从这里开始:https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS
此正则表达式^(a+)+$
对于输入aaaaX,上图中有16条可能的路径。但是对于aaaaaaaaaaaaaaaaX,有65536条可能的路径,每增加一条路径,这个数字就是两倍。这是一个极端的情况,其中天真的算法是有问题的,因为它必须传递许多路径,然后失败。
我怀疑你的正则表达式的很大一部分问题是(.|\s)
这有点令人困惑,因为\s
已经包含.
。但是创造了一个选择点。