这个正则表达式不应该发生灾难性的回溯

Posted

技术标签:

【中文标题】这个正则表达式不应该发生灾难性的回溯【英文标题】:Catastrophic backtracking shouldn't be happening on this regex 【发布时间】:2011-09-11 13:46:51 【问题描述】:

有人能解释一下为什么 Java 的正则表达式引擎会在这个正则表达式上进入灾难性的回溯模式吗?据我所知,每个交替都与其他交替互斥。

^(?:[^'\"\\s~:/@#\\|\\^\\&\\[\\]\\(\\)\\\\\\\\][^\"\\s~:/@#\\|\\^\\&\\[\\]\\(\\)\\\\\\\\]*|
\"(?:[^\"]+|\"\")+\"|
'(?:[^']+|'')+')

文字:'pão de açúcar itaucard mastercard platinum SUSTENTABILIDADE])

向一些交替添加所有格匹配可以解决问题,但我不知道为什么 - Java 的正则表达式库必须非常错误才能在互斥分支上回溯。

 ^(?:[^'\"\\s~:/@#\\|\\^\\&\\[\\]\\(\\)\\\\\\\\][^\"\\s~:/@#\\|\\^\\&\\[\\]\\(\\)\\\\\\\\]*|
 \"(?:[^\"]++|\"\")++\"|
 '(?:[^']++|'')++')

【问题讨论】:

您是否检查过使用不同的正则表达式引擎(Perl、Python 或 php)?该实现是否表现出这种行为? 一方面,你的模式搞砸了。你在一些地方有太多的反斜杠。如果\" 是文字双引号,那么\\s 是文字反斜杠后跟一个s。此外,您还有字符类中不需要的额外反斜杠。 @tchrist: \"\\s 在 Java 字符串文字中是有意义的,但我同意第一行的大多数反斜杠根本不需要在那里,更不用说翻倍了起来。 @Alan:呃,那是字符串文字而不是模式?多么可怕的混乱!我希望人们会发布模式。他说这是一个正则表达式,但不是。那是邪恶的。任何让我想连续处理四个反斜杠的事情都出其不意了。 @tchrist:一般来说,我认为最好发布正则表达式,因为它们出现在实际源代码中——例如,String regex = "[\"\\s\\\\]";。使用任何其他方法某人必然会走错路,至少我观察到了。 【参考方案1】:

编辑:最后添加了 Java 版本——尽管它本身就很笨拙、不可读和不可维护。


¡不再有丑陋的图案!

您需要做的第一件事以一种能够承受任何可能的人类可读性并因此可维护的希望的方式编写正则表达式。 您需要做的第二件事是分析它以查看它实际在做什么。

这意味着您至少需要以Pattern.COMMENTS 模式(或前缀"(?x)")编译它,然后添加空格以提供一些视觉空间。据我所知,您实际尝试匹配的模式是这个:

^ 
(?: [^'"\s~:/@\#|^&\[\]()\\]    # alt 1: unquoted
    [^"\s~:/@\#|^&\[\]()\\] *
  | " (?: [^"]+ | "" )+ "        # alt 2: double quoted
  | ' (?: [^']+ | '' )+ '        # alt 3: single quoted
)

如您所见,我在可能的地方引入了垂直和水平空白,以便将眼睛和思想作为一种认知块来引导。我还删除了所有多余的反斜杠。这些要么是彻头彻尾的错误,要么是只会让读者感到困惑的混淆器。

请注意,在应用垂直空白时,我如何让从一行到下一行相同的部分出现在同一列中,这样您就可以立即看到哪些部分相同,哪些部分不同。

完成之后,我终于可以看到,你在这里是一个锚定到开始的匹配,然后是三个选项的选择。因此,我用描述性注释标记了这三个备选方案,这样人们就不必猜测了。

我还注意到您的第一个替代方案有两个微妙不同的(否定的)方括号字符类。第二个缺少第一个中看到的单引号排除。这是故意的吗?即使是这样,我也觉得这对我的口味来说太过重复了。部分或全部应该在一个变量中,这样您就不会冒更新一致性问题的风险。


分析

您必须做的两件事中的第二件也是更重要的是对此进行分析。您需要准确查看该模式正在编译到哪个正则表达式程序中,并且您需要在它在您的数据上运行时跟踪它的执行情况。

Java 的 Pattern 类目前无法做到这一点,尽管我已经与 OraSun 的当前代码管理员详细讨论过,他都热衷于将这种能力添加到 Java 并认为他确切地知道如何去做吧。他甚至给我发了一个原型,它完成了第一部分:编译。所以我希望它可以在这些日子里使用。

同时,让我们转而使用一种工具,其中正则表达式是编程语言本身不可或缺的一部分,而不是作为一个尴尬的事后思考的东西。尽管有几种语言符合该标准,但模式匹配技术还没有达到 Perl 中所见的复杂程度。

这是一个等效的程序。

#!/usr/bin/env perl
use v5.10;      # first release with possessive matches
use utf8;       # we have utf8 literals
use strict;     # require variable declarations, etc
use warnings;   # check for boneheadedness

my $match = qr
    ^ (?: [^'"\s~:/@\#|^&\[\]()\\]
          [^"\s~:/@\#|^&\[\]()\\] *
        | " (?: [^"]+ | "" )+ "
        | ' (?: [^']+ | '' )+ '
    )
x;

my $text = "'pão de açúcar itaucard mastercard platinum SUSTENTABILIDAD])";

my $count = 0;

while ($text =~ /$match/g) 
    print "Got it: $&\n";
    $count++;


if ($count == 0) 
    print "Match failed.\n";

如果我们运行该程序,我们会得到匹配失败的预期答案。问题是为什么以及如何。

我们现在想看两件事:我们想看看该模式编译成什么正则表达式程序,然后我们想跟踪该正则表达式程序的执行。

这两个都由

控制
use re "debug";

pragma,也可以通过-Mre=debug在命令行中指定。这是我们将在这里做的,以避免对源代码进行黑客攻击。

正则表达式编译

re 调试编译指示通常会显示模式的编译及其执行。为了将它们分开,我们可以使用 Perl 的“仅编译”开关-c,它不会尝试执行已编译的程序。这样,我们看到的只是编译的模式。执行这些操作会产生以下 36 行输出:

$ perl -c -Mre=debug /tmp/bt
Compiling REx "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"...
Final program:
   1: BOL (2)
   2: BRANCH (26)
   3:   ANYOF[^\x09\x0a\x0c\x0d "#&-)/:@[-\^-~][^unicode+utf8::IsSpacePerl] (14)
  14:   STAR (79)
  15:     ANYOF[^\x09\x0a\x0c\x0d "#&()/:@[-\^-~][^unicode+utf8::IsSpacePerl] (0)
  26: BRANCH (FAIL)
  27:   TRIE-EXACT["'] (79)
        <"> (29)
  29:     CURLYX[0] 1,32767 (49)
  31:       BRANCH (44)
  32:         PLUS (48)
  33:           ANYOF[\x00-!#-\xff][unicode_all] (0)
  44:       BRANCH (FAIL)
  45:         EXACT <""> (48)
  47:       TAIL (48)
  48:     WHILEM[1/2] (0)
  49:     NOTHING (50)
  50:     EXACT <"> (79)
        <'> (55)
  55:     CURLYX[0] 1,32767 (75)
  57:       BRANCH (70)
  58:         PLUS (74)
  59:           ANYOF[\x00-&(-\xff][unicode_all] (0)
  70:       BRANCH (FAIL)
  71:         EXACT <''> (74)
  73:       TAIL (74)
  74:     WHILEM[2/2] (0)
  75:     NOTHING (76)
  76:     EXACT <'> (79)
  78: TAIL (79)
  79: END (0)
anchored(BOL) minlen 1 
/tmp/bt syntax OK
Freeing REx: "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"...

如您所见,编译后的正则表达式程序本身就是一种“正则表达式汇编语言”。 (它也恰好看起来很像向我展示的 Java 原型,所以我想你有一天也会在 Java 中看到这种东西。)所有细节都不是必不可少的,但我要指出,节点 2 处的指令是一个 BRANCH,如果它失败则继续执行分支 26,另一个 BRANCH。第二个 BRANCH 是正则表达式程序的唯一其他部分,由单个 TRIE-EXACT 节点组成,因为它知道备选方案具有不同的起始文字字符串。会发生什么 我们将在这两个 trie 分支中讨论。

正则表达式执行

现在是时候看看它运行时会发生什么。您正在使用的文本字符串会导致相当多的回溯,这意味着在它最终失败之前您将有大量的输出需要处理。输出多少?好吧,就这么多:

$ perl -Mre=debug /tmp/bt 2>&1 | wc -l
9987

我假设 10,000 步就是您所说的“灾难性回溯模式”。让我们看看我们不能把它精简成更容易理解的东西。您的输入字符串长度为 61 个字符。为了更好地了解发生了什么,我们可以将其缩减为仅 'pão,即只有 4 个字符。 (嗯,在 NFC 中,也就是说;它是 NFD 中的五个代码点,但这里没有任何改变)。结果是 167 行输出:

$ perl -Mre=debug /tmp/bt 2>&1 | wc -l
167

事实上,当你的字符串有这么多字符长时,你得到的正则表达式(编译加)执行分析的行:

chars   lines   string
    1     63   ‹'›
    2     78   ‹'p›  
    3    109   ‹'pã›
    4    167   ‹'pão› 
    5    290   ‹'pão ›
    6    389   ‹'pão d›
    7    487   ‹'pão de›
    8    546   ‹'pão de ›
    9    615   ‹'pão de a›
   10    722   ‹'pão de aç›
  ....
   61   9987   ‹'pão de açúcar itaucard mastercard platinum SUSTENTABILIDAD])›

我们看一下字符串为'pão这四个字符时的调试输出。这次我省略了编译部分,只显示执行部分:

$ perl -Mre=debug /tmp/bt
Matching REx "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"... against "'p%xe3o"
UTF-8 string...
   0 <> <'p%xe3o>  |  1:BOL(2)
   0 <> <'p%xe3o>  |  2:BRANCH(26)
   0 <> <'p%xe3o>  |  3:  ANYOF[^\x09\x0a\x0c\x0d "#&-)/:@[-\^-~][^unicode+utf8::IsSpacePerl](14)
                            failed...
   0 <> <'p%xe3o>  | 26:BRANCH(78)
   0 <> <'p%xe3o>  | 27:  TRIE-EXACT["'](79)
   0 <> <'p%xe3o>  |      State:    1 Accepted: N Charid:  2 CP:  27 After State:    3
   1 <'> <p%xe3o>  |      State:    3 Accepted: Y Charid:  0 CP:   0 After State:    0
                            got 1 possible matches
                            TRIE matched word #2, continuing
                            only one match left, short-circuiting: #2 <'>
   1 <'> <p%xe3o>  | 55:  CURLYX[0] 1,32767(75)
   1 <'> <p%xe3o>  | 74:    WHILEM[2/2](0)
                              whilem: matched 0 out of 1..32767
   1 <'> <p%xe3o>  | 57:      BRANCH(70)   1 <'> <p%xe3o>          | 58:        PLUS(74)
                                  ANYOF[\x00-&(-\xff][unicode_all] can match 3 times out of 2147483647...
   5 <'p%xe3o> <>  | 74:          WHILEM[2/2](0)
                                    whilem: matched 1 out of 1..32767
   5 <'p%xe3o> <>  | 57:            BRANCH(70)
   5 <'p%xe3o> <>  | 58:              PLUS(74)
                                        ANYOF[\x00-&(-\xff][unicode_all] can match 0 times out of 2147483647...
                                        failed...
   5 <'p%xe3o> <>  | 70:            BRANCH(73)
   5 <'p%xe3o> <>  | 71:              EXACT <''>(74)
                                        failed...
                                      BRANCH failed...
                                    whilem: failed, trying continuation...
   5 <'p%xe3o> <>  | 75:            NOTHING(76)
   5 <'p%xe3o> <>  | 76:            EXACT <'>(79)
                                      failed...
                                    failed...
   4 <'p%xe3> <o>  | 74:          WHILEM[2/2](0)
                                    whilem: matched 1 out of 1..32767
   4 <'p%xe3> <o>  | 57:            BRANCH(70)
   4 <'p%xe3> <o>  | 58:              PLUS(74)
                                        ANYOF[\x00-&(-\xff][unicode_all] can match 1 times out of 2147483647...
   5 <'p%xe3o> <>  | 74:                WHILEM[2/2](0)
                                          whilem: matched 2 out of 1..32767
   5 <'p%xe3o> <>  | 57:                  BRANCH(70)
   5 <'p%xe3o> <>  | 58:                    PLUS(74)
                                              ANYOF[\x00-&(-\xff][unicode_all] can match 0 times out of 2147483647...
                                              failed...
   5 <'p%xe3o> <>  | 70:                  BRANCH(73)
   5 <'p%xe3o> <>  | 71:                    EXACT <''>(74)
                                              failed...
                                            BRANCH failed...
                                          whilem: failed, trying continuation...
   5 <'p%xe3o> <>  | 75:                  NOTHING(76)
   5 <'p%xe3o> <>  | 76:                  EXACT <'>(79)
                                            failed...
                                          failed...
                                        failed...
   4 <'p%xe3> <o>  | 70:            BRANCH(73)
   4 <'p%xe3> <o>  | 71:              EXACT <''>(74)
                                        failed...
                                      BRANCH failed...
                                    whilem: failed, trying continuation...
   4 <'p%xe3> <o>  | 75:            NOTHING(76)
   4 <'p%xe3> <o>  | 76:            EXACT <'>(79)
                                      failed...
                                    failed...
   2 <'p> <%xe3o>  | 74:          WHILEM[2/2](0)
                                    whilem: matched 1 out of 1..32767
   2 <'p> <%xe3o>  | 57:            BRANCH(70)
   2 <'p> <%xe3o>  | 58:              PLUS(74)
                                        ANYOF[\x00-&(-\xff][unicode_all] can match 2 times out of 2147483647...
   5 <'p%xe3o> <>  | 74:                WHILEM[2/2](0)
                                          whilem: matched 2 out of 1..32767
   5 <'p%xe3o> <>  | 57:                  BRANCH(70)
   5 <'p%xe3o> <>  | 58:                    PLUS(74)
                                              ANYOF[\x00-&(-\xff][unicode_all] can match 0 times out of 2147483647...
                                              failed...
   5 <'p%xe3o> <>  | 70:                  BRANCH(73)
   5 <'p%xe3o> <>  | 71:                    EXACT <''>(74)
                                              failed...
                                            BRANCH failed...
                                          whilem: failed, trying continuation...
   5 <'p%xe3o> <>  | 75:                  NOTHING(76)
   5 <'p%xe3o> <>  | 76:                  EXACT <'>(79)
                                            failed...
                                          failed...
   4 <'p%xe3> <o>  | 74:                WHILEM[2/2](0)
                                          whilem: matched 2 out of 1..32767
   4 <'p%xe3> <o>  | 57:                  BRANCH(70)
   4 <'p%xe3> <o>  | 58:                    PLUS(74)
                                              ANYOF[\x00-&(-\xff][unicode_all] can match 1 times out of 2147483647...
   5 <'p%xe3o> <>  | 74:                      WHILEM[2/2](0)
                                                whilem: matched 3 out of 1..32767
   5 <'p%xe3o> <>  | 57:                        BRANCH(70)
   5 <'p%xe3o> <>  | 58:                          PLUS(74)
                                                    ANYOF[\x00-&(-\xff][unicode_all] can match 0 times out of 2147483647.
..
                                                    failed...
   5 <'p%xe3o> <>  | 70:                        BRANCH(73)
   5 <'p%xe3o> <>  | 71:                          EXACT <''>(74)
                                                    failed...
                                                  BRANCH failed...
                                                whilem: failed, trying continuation...
   5 <'p%xe3o> <>  | 75:                        NOTHING(76)
   5 <'p%xe3o> <>  | 76:                        EXACT <'>(79)
                                                  failed...
                                                failed...
                                              failed...
   4 <'p%xe3> <o>  | 70:                  BRANCH(73)
   4 <'p%xe3> <o>  | 71:                    EXACT <''>(74)
                                              failed...
                                            BRANCH failed...
                                          whilem: failed, trying continuation...
   4 <'p%xe3> <o>  | 75:                  NOTHING(76)
   4 <'p%xe3> <o>  | 76:                  EXACT <'>(79)
                                            failed...
                                          failed...
                                        failed...
   2 <'p> <%xe3o>  | 70:            BRANCH(73)
   2 <'p> <%xe3o>  | 71:              EXACT <''>(74)
                                        failed...
                                      BRANCH failed...
                                    whilem: failed, trying continuation...
   2 <'p> <%xe3o>  | 75:            NOTHING(76)
   2 <'p> <%xe3o>  | 76:            EXACT <'>(79)
                                      failed...
                                    failed...
                                  failed...
   1 <'> <p%xe3o>  | 70:      BRANCH(73)
   1 <'> <p%xe3o>  | 71:        EXACT <''>(74)
                                  failed...
                                BRANCH failed...
                              failed...
                            failed...
                          BRANCH failed...
Match failed
Match failed.
Freeing REx: "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"...

您所看到的情况是,trie 快速分支到节点 55,这是您的三个备选方案中的最后一个, 它匹配单引号,因为您的目标字符串以单引号开头引用。就是这个:

  | ' (?: [^']+ | '' )+ '        # alt 3: single quoted

节点 55 是这个 trie 分支:

        <'> (55)
  55:     CURLYX[0] 1,32767 (75)
  57:       BRANCH (70)
  58:         PLUS (74)
  59:           ANYOF[\x00-&(-\xff][unicode_all] (0)
  70:       BRANCH (FAIL)
  71:         EXACT <''> (74)

这里是显示灾难性退避发生位置的执行跟踪:

   1 <'> <p%xe3o>  | 74:    WHILEM[2/2](0)
                              whilem: matched 0 out of 1..32767
   1 <'> <p%xe3o>  | 57:      BRANCH(70)
   1 <'> <p%xe3o>  | 58:        PLUS(74)
                                  ANYOF[\x00-&(-\xff][unicode_all] can match 3 times out of 2147483647...
   5 <'p%xe3o> <>  | 74:          WHILEM[2/2](0)
                                    whilem: matched 1 out of 1..32767
   5 <'p%xe3o> <>  | 57:            BRANCH(70)
   5 <'p%xe3o> <>  | 58:              PLUS(74)
                                        ANYOF[\x00-&(-\xff][unicode_all] can match 0 times out of 2147483647...
                                        failed...
   5 <'p%xe3o> <>  | 70:            BRANCH(73)
   5 <'p%xe3o> <>  | 71:              EXACT <''>(74)
                                        failed...
                                      BRANCH failed...
                                    whilem: failed, trying continuation...
   5 <'p%xe3o> <>  | 75:            NOTHING(76)
   5 <'p%xe3o> <>  | 76:            EXACT <'>(79)
                                      failed...
                                    failed...
   4 <'p%xe3> <o>  | 74:          WHILEM[2/2](0)
                                    whilem: matched 1 out of 1..32767
   4 <'p%xe3> <o>  | 57:            BRANCH(70)
   4 <'p%xe3> <o>  | 58:              PLUS(74)
                                        ANYOF[\x00-&(-\xff][unicode_all] can match 1 times out of 2147483647...
   5 <'p%xe3o> <>  | 74:                WHILEM[2/2](0)
                                          whilem: matched 2 out of 1..32767

节点 58 吞噬了字符串 pão 中所有剩余的 3 个字符。这导致单引号的终止精确匹配失败。因此,它会尝试您的替代方案,即一对单引号,但这也失败了。

在这一点上,我不得不质疑你的模式。不应该

' (?: [^']+ | '' )+ '

真的是这样吗?

' [^']* '

所以发生的事情是,它有很多方法可以回溯寻找在逻辑上永远不会发生的事情。你有一个嵌套的量词,这导致了各种无望和无脑的忙碌工作。

如果我们把模式简化成这样:

^ (?: [^'"\s~:/@\#|^&\[\]()\\] +
    | " [^"]* "
    | ' [^']* '
  )

无论输入字符串的大小如何,它现在都提供相同数量的跟踪输出行:只有 40 行,这包括编译。见证完整字符串的编译和执行:

Compiling REx "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"...
Final program:
   1: BOL (2)
   2: BRANCH (26)
   3:   ANYOF[^\x09\x0a\x0c\x0d "#&-)/:@[-\^-~][^unicode+utf8::IsSpacePerl] (14)
  14:   STAR (61)
  15:     ANYOF[^\x09\x0a\x0c\x0d "#&()/:@[-\^-~][^unicode+utf8::IsSpacePerl] (0)
  26: BRANCH (FAIL)
  27:   TRIE-EXACT["'] (61)
        <"> (29)
  29:     STAR (41)
  30:       ANYOF[\x00-!#-\xff][unicode_all] (0)
  41:     EXACT <"> (61)
        <'> (46)
  46:     STAR (58)
  47:       ANYOF[\x00-&(-\xff][unicode_all] (0)
  58:     EXACT <'> (61)
  60: TAIL (61)
  61: END (0)
anchored(BOL) minlen 1 
Matching REx "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"... against "'p%xe3o de a%xe7%xfacar itaucard mast
ercard platinum S"...
UTF-8 string...
   0 <> <'p%xe3o >  |  1:BOL(2)
   0 <> <'p%xe3o >  |  2:BRANCH(26)
   0 <> <'p%xe3o >  |  3:  ANYOF[^\x09\x0a\x0c\x0d "#&-)/:@[-\^-~][^unicode+utf8::IsSpacePerl](14)
                             failed...
   0 <> <'p%xe3o >  | 26:BRANCH(60)
   0 <> <'p%xe3o >  | 27:  TRIE-EXACT["'](61)
   0 <> <'p%xe3o >  |      State:    1 Accepted: N Charid:  2 CP:  27 After State:    3
   1 <'> <p%xe3o d> |      State:    3 Accepted: Y Charid:  0 CP:   0 After State:    0
                             got 1 possible matches
                             TRIE matched word #2, continuing
                             only one match left, short-circuiting: #2 <'>
   1 <'> <p%xe3o d> | 46:  STAR(58)
                             ANYOF[\x00-&(-\xff][unicode_all] can match 60 times out of 2147483647...
                             failed...
                           BRANCH failed...
Match failed
Match failed.
Freeing REx: "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"...

我知道您认为所有格匹配可能是这里的答案,但我认为真正的问题是原始模式中的错误逻辑。看看它现在运行得有多稳健?

如果我们用你的所有格在旧模式上运行它,即使我认为这没有意义,我们仍然可以获得恒定的运行时间,但它需要更多的步骤。用这种模式

   ^ (?: [^'"\s~:/@\#|^&\[\]()\\] +    # alt 1: unquoted
       | " (?: [^"]++ | "" )++ "        # alt 2: double quoted
       | ' (?: [^']++ | '' )++ '        # alt 3: single quoted
     )

编译加执行配置文件如下:

Compiling REx "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"...
Final program:
   1: BOL (2)
   2: BRANCH (26)
   3:   ANYOF[^\x09\x0a\x0c\x0d "#&-)/:@[-\^-~][^unicode+utf8::IsSpacePerl] (14)
  14:   STAR (95)
  15:     ANYOF[^\x09\x0a\x0c\x0d "#&()/:@[-\^-~][^unicode+utf8::IsSpacePerl] (0)
  26: BRANCH (FAIL)
  27:   TRIE-EXACT["'] (95)
        <"> (29)
  29:     SUSPEND (58)
  31:       CURLYX[0] 1,32767 (55)
  33:         BRANCH (50)
  34:           SUSPEND (54)
  36:             PLUS (48)
  37:               ANYOF[\x00-!#-\xff][unicode_all] (0)
  48:             SUCCEED (0)
  49:           TAIL (53)
  50:         BRANCH (FAIL)
  51:           EXACT <""> (54)
  53:         TAIL (54)
  54:       WHILEM[1/2] (0)
  55:       NOTHING (56)
  56:       SUCCEED (0)
  57:     TAIL (58)
  58:     EXACT <"> (95)
        <'> (63)
  63:     SUSPEND (92)
  65:       CURLYX[0] 1,32767 (89)
  67:         BRANCH (84)
  68:           SUSPEND (88)
  70:             PLUS (82)
  71:               ANYOF[\x00-&(-\xff][unicode_all] (0)
  82:             SUCCEED (0)
  83:           TAIL (87)
  84:         BRANCH (FAIL)
  85:           EXACT <''> (88)
  87:         TAIL (88)
  88:       WHILEM[2/2] (0)
  89:       NOTHING (90)
  90:       SUCCEED (0)
  91:     TAIL (92)
  92:     EXACT <'> (95)
  94: TAIL (95)
  95: END (0)
anchored(BOL) minlen 1 
Matching REx "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"... against "'p%xe3o de a%xe7%xfacar itaucard mastercard platinum S"...
UTF-8 string...
   0 <> <'p%xe3o > |  1:BOL(2)
   0 <> <'p%xe3o > |  2:BRANCH(26)
   0 <> <'p%xe3o > |  3:  ANYOF[^\x09\x0a\x0c\x0d "#&-)/:@[-\^-~][^unicode+utf8::IsSpacePerl](14)
                            failed...
   0 <> <'p%xe3o > | 26:BRANCH(94)
   0 <> <'p%xe3o > | 27:  TRIE-EXACT["'](95)
   0 <> <'p%xe3o > |      State:    1 Accepted: N Charid:  2 CP:  27 After State:    3
   1 <'> <p%xe3o d>|      State:    3 Accepted: Y Charid:  0 CP:   0 After State:    0
                            got 1 possible matches
                            TRIE matched word #2, continuing
                            only one match left, short-circuiting: #2 <'>
   1 <'> <p%xe3o d>| 63:  SUSPEND(92)
   1 <'> <p%xe3o d>| 65:    CURLYX[0] 1,32767(89)
   1 <'> <p%xe3o d>| 88:      WHILEM[2/2](0)
                                whilem: matched 0 out of 1..32767
   1 <'> <p%xe3o d>| 67:        BRANCH(84)
   1 <'> <p%xe3o d>| 68:          SUSPEND(88)
   1 <'> <p%xe3o d>| 70:            PLUS(82)
                                      ANYOF[\x00-&(-\xff][unicode_all] can match 60 times out of 2147483647...
  64 <NTABILIDAD])> <| 82:              SUCCEED(0)
                                        subpattern success...
  64 <NTABILIDAD])> <| 88:          WHILEM[2/2](0)
                                    whilem: matched 1 out of 1..32767
  64 <NTABILIDAD])> <| 67:            BRANCH(84)
  64 <NTABILIDAD])> <| 68:              SUSPEND(88)
  64 <NTABILIDAD])> <| 70:                PLUS(82)
                                          ANYOF[\x00-&(-\xff][unicode_all] can match 0 times out of 2147483647...
                                          failed...
                                        failed...
  64 <NTABILIDAD])> <| 84:            BRANCH(87)
  64 <NTABILIDAD])> <| 85:              EXACT <''>(88)
                                        failed...
                                      BRANCH failed...
                                    whilem: failed, trying continuation...
  64 <NTABILIDAD])> <| 89:            NOTHING(90)
  64 <NTABILIDAD])> <| 90:            SUCCEED(0)
                                      subpattern success...
  64 <NTABILIDAD])> <| 92:  EXACT <'>(95)
                failed...
              BRANCH failed...
Match failed
Match failed.
Freeing REx: "%n    ^ (?: [^'%"\s~:/@\#|^&\[\]()\\]%n          [^%"\s~:/"...

我还是更喜欢我的解决方案。它更短。


编辑

看起来,Java 版本确实比相同模式的 Perl 版本多 100 倍,我不知道为什么——除了 Perl 正则表达式编译器在优化方面比 Java 正则表达式智能大约 100 倍编译器,它永远不会做任何事情,而且应该这样做。

这是等效的 Java 程序。我已经移除了前导锚,以便我们可以正确循环。

$ cat java.crap
import java.util.regex.*;

public class crap 

public static void
main(String[ ] argv) 
    String input = "'pão de açúcar itaucard mastercard platinum SUSTENTABILIDAD])";
    String regex = "\n"
                + "(?: [^'\"\\s~:/@\\#|^&\\[\\]()\\\\]    # alt 1: unquoted         \n"
                + "    [^\"\\s~:/@\\#|^&\\[\\]()\\\\] *                     \n"
                + "  | \" (?: [^\"]++ | \"\" )++ \"       # alt 2: double quoted   \n"
                + "  | ' (?: [^']++ | '' )++ '       # alt 3: single quoted   \n"
                + ")                                                          \n"
                ;
    System.out.printf("Matching ‹%s› =~ qr%sx\n\n", input, regex);

    Pattern regcomp = Pattern.compile(regex, Pattern.COMMENTS);
    Matcher regexec = regcomp.matcher(input);

    int count;
    for (count = 0; regexec.find(); count++) 
       System.out.printf("Found match: ‹%s›\n", regexec.group());
    
    if (count == 0) 
        System.out.printf("Match failed.\n");
    
  

运行时,会产生这个:

$ javac -encoding UTF-8 crap.java && java -Dfile.encoding=UTF-8 crap
Matching ‹'pão de açúcar itaucard mastercard platinum SUSTENTABILIDAD])› =~ qr
(?: [^'"\s~:/@\#|^&\[\]()\\]    # alt 1: unquoted         
    [^"\s~:/@\#|^&\[\]()\\] *                     
  | " (?: [^"]++ | "" )++ "       # alt 2: double quoted   
  | ' (?: [^']++ | '' )++ '       # alt 3: single quoted   
)                                                          
x

Found match: ‹pão›
Found match: ‹de›
Found match: ‹açúcar›
Found match: ‹itaucard›
Found match: ‹mastercard›
Found match: ‹platinum›
Found match: ‹SUSTENTABILIDAD›

如您所见,Java 中的模式匹配有很多话要说,绝对没有一个能通过便盆警察。这简直是​​一种皇家的痛苦。

【讨论】:

tchrist, '(?:[^']+|'')+' 可能用于允许引号与自身一起转义(例如'foo ''bar'' baz')。你可以写(?:'[^']*+')++ @Qtax 我以前从未见过这种约定。相当恶心。【参考方案2】:

我不得不承认这也让我感到惊讶,但我在 RegexBuddy 中得到了相同的结果:它在一百万步后退出尝试。我知道有关灾难性回溯的警告往往集中在嵌套量词上,但根据我的经验,交替至少同样危险。事实上,如果我改变你正则表达式的最后一部分:

'(?:[^']+|'')+'

...到这个:

'(?:[^']*(?:''[^']*)*)'

...它只用了十一个步骤就失败了。这是Friedl 的“展开循环”技术的一个示例,他将其分解如下:

opening normal * ( special normal * ) * closing
   '     [^']        ''     [^']           '

嵌套的星星是安全的,只要:

    specialnormal 永远不能匹配相同的东西, special 始终至少匹配一个字符,并且 special 是原子的(它必须只有一种匹配方式)。

然后,正则表达式将失败匹配最小回溯,并且成功完全没有回溯。另一方面,交替版本几乎可以保证回溯,并且在无法匹配的情况下,随着目标字符串长度的增加,它会迅速失控。如果它没有在某些口味中过度回溯,那是因为它们具有专门针对此问题的内置优化——到目前为止,很少有口味这样做。

【讨论】:

百万步,艾伦?天啊。这比我得到的答案要差两个数量级。我想知道那里发生了什么;想法?我想这说明了为什么你只能相信你实际使用的引擎的结果。 使用所有格量词或原子团不是更好,而且失败得更快吗? RegexBuddy 在分配其“步骤”方面很慷慨,我注意到了。例如,[^']+(?:[^']+|'')+ 在最初的' 之后吞噬了所有东西,因此分别获得了信用,然后在它执行任何 I 操作之前有三个所谓的“回溯”步骤调用回溯。 @Qtax:你的意思是,除了展开的循环之外?它可能会更快地失败一两步,仅此而已。顺便说一句,我并不是说您应该在所有格量词和/或原子团可用时使用此技术。但是了解为什么它会起作用将永远对你有利。 天哪。 Java 版本确实比相同的 Perl 版本多 100 倍的步骤。 Perl 版本完成了,但速度很慢。 Java 版本似乎挂起,因此遭受的损失要严重得多。我不知道为什么。我们永远不会知道,因为我们无法对其进行分析。唉。【参考方案3】:

有人能解释一下为什么 java 的正则表达式引擎会在这个正则表达式上进入灾难性模式吗?

对于字符串:

'pão de açúcar itaucard mastercard platinum SUSTENTABILIDADE])

这部分正则表达式似乎是问题所在:

'(?:[^']+|'')+'

匹配第一个',然后未能匹配结束',从而回溯嵌套量词的所有组合。

如果您允许正则表达式回溯,它将回溯(失败时)。使用原子组和/或所有格量词来防止这种情况发生。


顺便说一句,您不需要该正则表达式中的大部分转义。只有你(可能)需要在字符类([])中转义的是字符^-]。但通常你可以定位它们,这样它们也不需要被转义。当然,\ 和无论你引用的字符串仍然需要(双)转义。

"^(?:[^]['\"\\s~:/@#|^&()\\\\][^][\"\s~:/@#|^&()\\\\]*|\"(?:[^\"]++|\"\")++\"|'(?:[^']++|'')++')"

【讨论】:

其实你还是需要转义一个反斜杠,所以你必须有两个,而不仅仅是一个。 ^ 仅在第一个字符时才需要转义,- 仅在不是终端(第一个或最后一个)时才需要转义。但这没关系:我仍然认为这种模式完全不可读和不可维护。当人们写出这样的东西时,我真的很生气。我知道你只是在模仿原版,但原版应该受到谴责而不是模仿。

以上是关于这个正则表达式不应该发生灾难性的回溯的主要内容,如果未能解决你的问题,请参考以下文章

Go 正则表达式中没有灾难性的回溯吗?

Java 9+中的灾难性回溯正则表达式示例[关闭]

正则表达式灾难性回溯

如何彻底避免正则表达式的灾难性回溯?

正则表达式模式灾难性回溯

使用正则表达式模式时的灾难性回溯错误