为什么Python会在这个正则表达式上窒息?

时间:2014-10-06 05:27:42

标签: python regex

这看起来像一个简单的正则表达式,没有反向引用,没有“任何”字符,我甚至敢说Thomson DFA和所有人都可以解析它。它甚至可以工作,但是非常简单的不匹配就会窒息。

{\s*?
ngx_string\("(?P<name>[a-z0-9_]+)"\)\s*?,\s*?
(?P<where>(([A-Z0-9_]+)\s*\|?)+?)\s*?,\s*?
(?P<bla>[^\n}]+?)\s*?,\s*?
(?P<bla2>[^\n}]+?)\s*?,\s*?
(?P<bla3>[^\n}]+?)\s*?,\s*?
(?P<bla4>[^\n}]+?)\s*?
}

+ re.MULTILINE | re.VERBOSE 

runnable gist here

我目前正在Python 2.7.8上尝试这个(但链接的要点也在py3.4上失败;而且linux,x86-64,Ubuntu,PCRE静态链接在[至少/ proc // maps不会显示任何有趣的东西)。

这解析得很好:

{ ngx_string("daemon"),
  NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG,
  ngx_conf_set_flag_slot,
  0,
  offsetof(ngx_core_conf_t, daemon),
  NULL },

这就是有趣的地方:

 { ngx_string("off"), NGX_HTTP_REQUEST_BODY_FILE_OFF },
 { ngx_string("on"), NGX_HTTP_REQUEST_BODY_FILE_ON },

此外,还有更多数据:

将第二行更改为此

(?P<where>(([A-Z0-9_]{1,20})\s*\|?){1,6}?)\s{0,10}?,\s{0,10}?

,它最终在合理的时间内完成,但指数爆炸仍然存在,只是可以忍受:

trying      { ngx_string("off"), NGX_HTTP_REQUEST_BODY_FILE
Took 0.033483 s
trying      { ngx_string("off"), NGX_HTTP_REQUEST_BODY_FILE_
Took 0.038528 s
trying      { ngx_string("off"), NGX_HTTP_REQUEST_BODY_FILE_O
Took 0.044108 s
trying      { ngx_string("off"), NGX_HTTP_REQUEST_BODY_FILE_OF
Took 0.053547 s

另外,有趣的是,基于JS的Python正则表达式(模拟器?)解析器可以像PCRE冠军的早餐一样吃它:https://www.debuggex.com/r/S__vSvp8-LGLuCLQ

哦,也许有人应该创建pathological-regex标签:|

2 个答案:

答案 0 :(得分:1)

(?P<where>(([A-Z0-9_]+)\s*\|?)+?)
                     ^        ^

这是你的正则表达式爆炸的地方。阅读http://www.regular-expressions.info/catastrophic.html

在每次失败时,它会返回一步以检查是否存在匹配。这会为正则表达式引擎创建一系列步骤和可能性。

答案 1 :(得分:1)

问题在于原始正则表达式中的(([A-Z0-9_]+)\s*\|?)+?或测试正则表达式中的(([A-Z0-9_]{1,20})\s*\|?){1,6}?{1,20}{1,6}仅用于抑制指数回溯。

由于\s*\|?是可选的,当输入仅包含(([A-Z0-9_]+))+?不带空格或条[A-Z0-9_]时,正则表达式可以扩展为指数回溯|的经典示例1}},但与正则表达式的其余部分不匹配。

例如,匹配(?P<where>(([A-Z0-9_]+)\s*\|?)+?)\s*?,\s*?AAAAAAAAAAAAAA,缺失)会导致引擎检查所有分割字符串的可能性,每个标记在不同的迭代次数下匹配外部重复:

AAAAAAAAAAAAAA
AAAAAAAAAAAAA A
AAAAAAAAAAAA AA
AAAAAAAAAAAA A A
AAAAAAAAAAA AAA
...
A AAAAAAAAAAAAA
A AAAAAAAAAAAA A
A AAAAAAAAAAA AA
...
A A A A A A A A A A A A A A

仔细看看,你的正则表达式的其余部分也有过多的回溯问题。

(?P<bla2>[^\n}]+?)\s*?,\s*?为例。 [^\n}]可以匹配空格(或制表符或其他一些空格字符),\s也可以。如果您的不匹配输入包含一长串空格,这可能会导致过多的回溯。可能还存在正确性问题,因为,也可以与[^\n}]匹配。

另外,Python re是一个NFA引擎,没有任何优化来缓解指数回溯问题。

减轻指数回溯:

{\s*
ngx_string\("(?P<name>[a-z0-9_]+)"\)\s*,\s*
(?P<where>[A-Z0-9_]+(?:\s*\|\s*[A-Z0-9_]+)*)\s*,\s*
(?P<bla>[^\n}]+?)\s*,\s*
(?P<bla2>[^\n}]+?)\s*,\s*
(?P<bla3>[^\n}]+?)\s*,\s*
(?P<bla4>[^\n}]+?)\s*
}

[^\n}]的过度回溯和正确性问题仍然没有解决。如果参数不在不同的行上,则函数调用中的,可能被错误地识别为外部块{}的一部分。

一般解决方案需要递归正则表达式,因为您可以调用函数将其结果作为参数传递,例如offsetof(ngx_core_conf_t, daemon),但re包中没有递归正则表达式功能。一般性较低的解决方案是匹配嵌套括号的某些级别,例如1级嵌套:

(?P<bla>(?:\([^)]*\)|[^,()])+),\s*

整个正则表达式是:

{\s*?
ngx_string\("(?P<name>[a-z0-9_]+)"\)\s*,\s*
(?P<where>[A-Z0-9_]+(?:\s*\|\s*[A-Z0-9_]+)*)\s*,
(?P<bla>(?:\([^)]*\)|[^,()])+),\s*
(?P<bla2>(?:\([^)]*\)|[^,()])+),\s*
(?P<bla3>(?:\([^)]*\)|[^,()])+),\s*
(?P<bla4>(?:\([^)]*\)|[^,()])+)
}

DEMO

有两个警告:

  • <bla*>捕获组最后会包含空格。如果你想删除空格,正则表达式会更长一些,可以防止可能的过度回溯。您可以尝试在\s*之前将,添加回this demo here
  • 它假定()不是任何字符串文字的一部分。