【问题标题】:Regular expression that never finishes running永远不会结束运行的正则表达式
【发布时间】:2012-10-05 15:14:20
【问题描述】:

我写了一个小而朴素的正则表达式,它应该在括号内找到文本:

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

注意:我不是在寻找更好的正则表达式或其他解决方案来解决这个问题。我们不再使用它。

【问题讨论】:

  • 不知道为什么这是-1?!?

标签: python regex


【解决方案1】:

灾难性的回溯

正如 CaffGeek 的回答正确暗示的那样,问题出在catastrophic backtracking 的一种形式上。这两种选择都匹配空格(或制表符),并且贪婪地无限次应用。此外,点匹配右括号,因此一旦匹配左括号,这个表达式总是匹配到字符串的最后,然后它必须煞费苦心地回溯以找到右括号。并且在这个回溯过程中,在每个位置都尝试了另一种替代方案(对于空格或制表符也是成功的)。因此,在引擎可以回溯一个位置之前,必须尝试所有可能的匹配组合序列。在结束括号后有很多空格,这很快就会增加。可以通过简单地使星形量词变得惰性(即r'\((.|\s)*?\)')来解决存在匹配闭括号的情况的具体问题,但是对于存在开括号的非匹配情况,仍然存在失控的正则表达式问题主题字符串中没有匹配的右括号。

原来的正则表达式真的,真的很糟糕! (当有一对以上时,也不能正确匹配右括号)。

匹配最内层括号的正确表达式(对于匹配和不匹配的情况都非常快),当然是:

re_innermostparens = re.compile(r"""
    \(        # Literal open paren.
    [^()]*    # Zero or more non-parens.
    \)        # Literal close paren.
    """, re.VERBOSE)

所有正则表达式作者都应该阅读 MRE3!

这一切都在 Jeffrey Friedl 的 must-read-for-regex-authorsMastering Regular Expressions (3rd Edition) 中进行了非常详细的解释(带有详尽的示例和推荐的最佳实践)。老实说,这是我读过的最有用的书。正则表达式是一个非常强大的工具,但就像装载的武器一样,必须非常小心和精确地使用(否则你会在脚上开枪!)

【讨论】:

  • 我第二个MRE3;这很棒。通读一遍并开始为以下内容编写正则表达式生成器:r'[\s,]*("[^"]+"|\'[^\']+\'|[^\s,]+)[ \t,]*'(可选引用,空格或逗号分隔的标签拆分器)和r'[\s]*([+-]?"[^"]+"|\'[^\']+\'|[^\s]+)[\s]*'(谷歌风格的搜索词 globber,用于可选的前缀和引号)跨度>
  • 所以你的意思是它试图让你的空格的例子匹配...,..\s,.\s.,.\s\s,等等?这就是我真正要寻找的解释,因为它直接解释了执行时间的 n^2 增长。
  • @fletom - 是的。 "...""..\s"".\s."".\s\s""\s..""\s.\s""\s\s.""\s\s\s"
【解决方案2】:

可能是重做问题。

见:http://en.wikipedia.org/wiki/ReDoS

可以对构造不佳的正则表达式创建正则表达式拒绝服务。

例如,从这里:https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS

这个正则表达式^(a+)+$

对于输入 aaaaX,上图中有 16 条可能的路径。但是对于 aaaaaaaaaaaaaaaX,有 65536 条可能的路径,并且每增加一个 a,这个数字就会翻倍。这是朴素算法存在问题的极端情况,因为它必须通过许多路径,然后失败。

我怀疑您的正则表达式的很大一部分问题是这个(.|\s),这有点令人困惑,因为\s 已经包含在. 中。但是会创建一个选项点。

编辑:我认为这是对我读过的问题的更好解释之一。

http://msdn.microsoft.com/en-us/magazine/ff646973.aspx

【讨论】:

  • 在原始答案更新后编辑:很好的答案。为什么转义的右括号不限制重组?
  • @GothAlice,不确定,但如果我只使用\((.*)\),它是一样的,但要快得多。这是解析器的工作方式,我试图找到我几年前读过的链接,它有一个很好的解释。
猜你喜欢
  • 1970-01-01
  • 2021-08-05
  • 1970-01-01
  • 1970-01-01
  • 2021-05-17
  • 2013-04-09
  • 2010-12-27
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多