6

我正在尝试使用从http://daringfireball.net/2010/07/improved_regex_for_matching_urls获得的 URL 匹配正则表达式

(?xi)
\b
(                       # Capture 1: entire matched URL
  (?:
    https?://               # http or https protocol
    |                       #   or
    www\d{0,3}[.]           # "www.", "www1.", "www2." … "www999."
    |                           #   or
    [a-z0-9.\-]+[.][a-z]{2,4}/  # looks like domain name followed by a slash
  )
  (?:                       # One or more:
    [^\s()<>]+                  # Run of non-space, non-()<>
    |                           #   or
    \(([^\s()<>]+|(\([^\s()<>]+\)))*\)  # balanced parens, up to 2 levels
  )+
  (?:                       # End with:
    \(([^\s()<>]+|(\([^\s()<>]+\)))*\)  # balanced parens, up to 2 levels
    |                               #   or
    [^\s`!()\[\]{};:'".,<>?«»“”‘’]        # not a space or one of these punct chars
  )
)

根据对另一个问题的回答,似乎有些情况会导致此正则表达式灾难性地回溯。例如:

var re = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i;
re.test("http://google.com/?q=(AAAAAAAAAAAAAAAAAAAAAAAAAAAAA)")

... 可能需要很长时间才能执行(例如在 Chrome 中)

在我看来,问题在于这部分代码:

(?:                       # One or more:
    [^\s()<>]+                  # Run of non-space, non-()<>
    |                           #   or
    \(([^\s()<>]+|(\([^\s()<>]+\)))*\)  # balanced parens, up to 2 levels
  )+

...这似乎大致相当于(.+|\((.+|(\(.+\)))*\))+, 看起来它包含(.+)+

我可以做出改变来避免这种情况吗?

4

1 回答 1

9

将其更改为以下内容应该可以防止灾难性的回溯:

(?xi)
\b
(                       # Capture 1: entire matched URL
  (?:
    https?://               # http or https protocol
    |                       #   or
    www\d{0,3}[.]           # "www.", "www1.", "www2." … "www999."
    |                           #   or
    [a-z0-9.\-]+[.][a-z]{2,4}/  # looks like domain name followed by a slash
  )
  (?:                       # One or more:
    [^\s()<>]+                  # Run of non-space, non-()<>
    |                           #   or
    \(([^\s()<>]|(\([^\s()<>]+\)))*\)  # balanced parens, up to 2 levels
  )+
  (?:                       # End with:
    \(([^\s()<>]|(\([^\s()<>]+\)))*\)  # balanced parens, up to 2 levels
    |                               #   or
    [^\s`!()\[\]{};:'".,<>?«»“”‘’]        # not a space or one of these punct chars
  )
)

所做的唯一更改是删除正则表达式的每个“平衡括号”部分中的+第一个之后。[^\s()<>]

这是用 JS 测试的单行版本:

var re = /\b((?:https?:\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/i;
re.test("http://google.com/?q=(AAAAAAAAAAAAAAAAAAAAAAAAAAAAA")

原始正则表达式的问题部分是平衡括号部分,为了简化回溯发生原因的解释,我将完全删除其中的嵌套括号部分,因为它在这里不相关:

\(([^\s()<>]+|(\([^\s()<>]+\)))*\)    # original
\(([^\s()<>]+)*\)                     # expanded below

\(                # literal '('
(                 # start group, repeat zero or more times
    [^\s()<>]+        # one or more non-special characters
)*                # end group
\)                # literal ')'

考虑一下 string 在这里发生了什么'(AAAAA',文字(会匹配,然后AAAAA会被组消耗,而)将无法匹配。在这一点上,该组将放弃一个A,留下AAAA被俘并试图在这一点上继续比赛。因为该组有一个追随者,所以该组可以匹配*多次,所以现在您将拥有match ,然后在第二次通过。当这失败时,原始捕获将放弃额外的捕获并由第二个捕获消耗。([^\s()<>]+)*AAAAAA

这将持续很长时间,导致以下尝试匹配,其中每个逗号分隔的组表示该组匹配的不同时间,以及该实例匹配的字符数:

AAAAA
AAAA, A
AAA, AA
AAA, A, A
AA, AAA
AA, AA, A
AA, A, AA
AA, A, A, A
....

我可能算错了,但我很确定在确定正则表达式无法匹配之前,它总共需要 16 个步骤。随着您继续向字符串中添加其他字符,解决此问题的步骤数呈指数增长。

通过删除 并将其+更改为\(([^\s()<>])*\),您将避免这种回溯情况。

重新添加交替以检查嵌套括号不会导致任何问题。

请注意,您可能希望在字符串的末尾添加某种锚点,因为当前"http://google.com/?q=(AAAAAAAAAAAAAAAAAAAAAAAAAAAAA"将匹配到 之前(,所以re.test(...)会返回true因为http://google.com/?q=匹配。

于 2012-04-18T22:20:03.893 回答