【问题标题】:Atomic groups clarity原子团清晰度
【发布时间】:2014-09-29 06:10:25
【问题描述】:

考虑一下这个正则表达式。

a*b

如果是aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac,这将失败

这需要调试器中的67 步骤才能失败。

现在考虑这个正则表达式。

(?>a*)b

如果是aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac,这将失败

这需要调试器中的133 步骤才能失败。

最后是这个正则表达式:

a*+b  (a variant of atomic group)

如果是aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac,这将失败

这需要调试器中的67 步骤才能失败。

当我检查基准 atomic group (?>a*)b 时,179% 的执行速度更快。

现在原子组禁用回溯。所以在比赛中表现不错。

  1. 但为什么步数更多?有人可以解释一下吗?

  2. 为什么会有差异。在两个原子组(?>a*)b 和a*+b 之间步进。

它们的工作方式不同吗?

【问题讨论】:

  • 你的意思是a*+b 一个原子团?
  • 几点: 1. *+ in a*+b 是占有性,没有任何东西使它成为原子。 2.由于引擎回溯匹配,步数较多。这是正则表达式优化的直接需求,以防止大量回溯和潜在的 ReDoS。 3.(?>a*) =/= (?>a)*
  • @AvinashRaj a*+b 不是原子的,但可以防止回溯。所以我试过了
  • @Unihedron 引擎回溯,因为我禁用了优化?原子组是引擎优化的一部分吗?

标签: regex pcre backtracking


【解决方案1】:

作者注:

此答案针对问题 1,由赏金文本 “我期待调试器需要更多步骤的确切原因。我不需要解释原子组如何工作的答案。”;     Jerry's answer 很好地解决了其他问题,而my other answer 则了解了上述结构、它们的工作原理以及它们为何重要。想要获得完整的知识,仅仅阅读这篇文章是不够的!

正则表达式中的每个组都需要一步进出组。

   什么?!
是的,我是认真的,请继续阅读...

首先,我想向您介绍量化的非捕获组,而不是没有组:

Pattern 1: (?:c)at
Pattern 2: cat

那么这里到底发生了什么?我们将在禁用优化的正则表达式引擎上将模式与测试字符串 "concat" 匹配:

在此过程中,我向您介绍了更多组:

  哦不!我要避免使用组!

等一下!请注意,匹配的步数与匹配的性能没有相关性。 引擎优化了我提到的大部分“不必要的步骤”。 尽管在禁用优化的引擎上采取了更多步骤,但原子组仍然是最有效的。

也许相关:

【讨论】:

    【解决方案2】:

    那么什么是回溯?

    引擎采用默认贪婪的量词。贪婪修饰符匹配所有可能的和按需回溯,从而实现高效匹配,

    由Greedy vs. Reluctant vs. Possessive Quantifiers引用:

    greedy 量词首先尽可能匹配。所以.* 匹配整个字符串。然后匹配器尝试匹配下面的f,但没有剩余字符。所以它“回溯”,使贪婪的量词匹配更少的东西(使字符串末尾的“o”不匹配)。这仍然与正则表达式中的f 不匹配,因此它“回溯”了一步,使贪婪的量词再次匹配更少的东西(使字符串末尾的“oo”不匹配)。 still 与正则表达式中的f 不匹配,因此它又回溯了一步(使字符串末尾的“foo”不匹配)。现在,匹配器最终匹配了正则表达式中的f,并且o 和下一个o 也匹配了。成功! [...]

    这和a*+b有什么关系?

    在/a*+b/:

    • a 文字字符“a”。
    •   *+ 零个或多个,占有。
    • b 文字字符"b"。

    如Greedy vs. Reluctant vs. Possessive Quantifiers所引用:

    占有量词就像贪婪量词一样,但它不会回溯。所以它以.* 匹配整个字符串开始,没有任何不匹配的内容。然后没有任何东西可以与正则表达式中的f 匹配。由于所有格量词不回溯,因此匹配失败。

    为什么重要?

    机器不会意识到它自己是否在进行(低效)匹配。请参阅此处以获取一个不错的示例:Program run forever when matching regex。在许多情况下,快速编写的正则表达式可能不是高效,而且很容易在部署中出现问题。

    那么什么是原子团?

    原子组内的模式完成匹配后,永远不会放手。研究这个例子:

    Pattern: (?>\d\w{2}|12)c
    Matching string: 12c
    

    看起来完全合法,但是这个匹配失败。步骤很简单:原子组的第一个交替完美匹配 - \d\w{2} 消耗 12c。然后该组完成匹配 - 现在这里是我们的指针位置:

    Pattern: (?>\d\w{2}|12)c
                       ^
    Matching string: 12c
                        ^
    

    模式前进。现在我们尝试匹配c,但没有c。匹配失败,而不是尝试回溯(释放\d\w{2} 并消耗12)。

    那么这是个坏主意! Unihedron,我们为什么要防止回溯?

    现在假设我们正在使用 JSON 对象进行操作。这个文件不小。从头开始回溯将是一个坏主意。

           "2597401":[{"jobID":"2597401",
                     "account":"TG-CCR120014",
                     "user":"charngda",
                     "pkgT":{"pgi/7.2-  5":{"libA":["libpgc.so"],
                     "flavor":["default"]}},          
                     "startEpoch":"1338497979",
                     "runTime":"1022",
                     "execType":"user:binary",              
                     "exec":"ft.D.64",
                     "numNodes":"4",
                     "sha1":"5a79879235aa31b6a46e73b43879428e2a175db5",
                     "execEpoch":1336766742,
                     "execModify":"Fri May 11 15:05:42 2012",
                     "startTime":"Thu May 31 15:59:39 2012",
                     "numCores":"64",
                     "sizeT":{"bss":"1881400168","text":"239574","data":"22504"}},  
                     {"jobID":"2597401",
                     "account":"TG-CCR120014",
                     "user":"charngda",
                     "pkgT":{"pgi/7.2-5":{"libA":["libpgc.so"],
                     "flavor":["default"]}},
                     "startEpoch":"1338497946",
                     "runTime":"33"  "execType":"user:binary",
                     "exec":"cg.C.64",
                     "numNodes":"4",
                     "sha1":"caf415e011e28b7e4e5b050fb61cbf71a62a9789",
                     "execEpoch":1336766735,
                    "execModify":"Fri May 11 15:05:35 2012",
                    "startTime":"Thu May 31 15:59:06 2012",
                    "numCores":"64",
                    "sizeT":{"bss":"29630984","text":"225749","data":"20360"}},
                    {"jobID":"2597401",
                    "account":"TG-CCR120014",
                    "user":"charngda",
                    "pkgT":{"pgi/7.2-5":  {"libA":["libpgc.so"],
                    "flavor":["default"]}},
                    "startEpoch":"1338500447",
                    "runTime":"145",
                    "execType":"user:binary",
                    "exec":"mg.D.64",
                    "numNodes":"4",
                    "sha1":"173de32e1514ad097b1c051ec49c4eb240f2001f",
                    "execEpoch":1336766756,
                    "execModify":"Fri May 11 15:05:56 2012",
                    "startTime":"Thu May 31 16:40:47 2012",
                    "numCores":"64",
                    "sizeT":{"bss":"456954120","text":"426186","data":"22184"}},{"jobID":"2597401",
                    "account":"TG-CCR120014",
                    "user":"charngda",
                    "pkgT":{"pgi/7.2-5":{"libA":["libpgc.so"],
                    "flavor":["default"]}},
                    "startEpoch":"1338499002",
                    "runTime":"1444",
                    "execType":"user:binary",
                    "exec":"lu.D.64",
                    "numNodes":"4",
                    "sha1":"c6dc16d25c2f23d2a3321d4feed16ab7e10c2cc1",
                    "execEpoch":1336766748,
                    "execModify":"Fri May 11 15:05:48 2012",
                    "startTime":"Thu May 31 16:16:42 2012",
                    "numCores":"64",
                    "sizeT":{"bss":"199850984","text":"474218","data":"27064"}}],
    

    哦哦...

    你现在明白我的意思了吗? :P

    剩下的让你自己去弄清楚,并尝试更多地了解所有格量词和原子团;我不会在这篇文章中写任何其他内容。这里是json的出处,前几天看到了答案,很受启发:REGEX reformatting。

    另请阅读

    【讨论】:

    • 数据实际上很棒。清除一两件事。但问题仍然存在。为什么在定义原子组时调试器需要更多步骤才能失败?
    【解决方案3】:

    我觉得有些不对劲……

    我不知道您是如何进行基准测试的,但 a*+b 和 (?>a*)b 应该是相同的。引用regular-expressions.info(强调我的):

    基本上,不是X*+,而是写(?>X*)。重要的是要注意量化标记X 和量词都在原子组内。即使X 是一个组,您仍然需要在其周围放置一个额外的原子组才能达到相同的效果。 (?:a|b)*+ 等同于(?>(?:a|b)*),但不等同于(?>a|b)*。后者是一个有效的正则表达式,但是当它用作更大的正则表达式的一部分时,它不会产生相同的效果。

    为了确认上述内容,我在ideone 上运行了以下内容:

    $tests = 1000000;
    
    $start = microtime( TRUE );
    for( $i = 1; $i <= $tests; $i += 1 ) {
        preg_match('/a*b/','aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac');
    }
    $stop = microtime( TRUE );
    
    printf( "For /a*b/    : %1.15f per iteration for %s iterations\n", ($stop - $start)/$tests, $tests );
    unset( $stop, $start );
    
    $start = microtime( TRUE );
    for( $i = 1; $i <= $tests; $i += 1 ) {
        preg_match('/(?>a*)b/','aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac');
    }
    $stop = microtime( TRUE );
    
    printf( "For /(?>a*)b/: %1.15f per iteration for %s iterations\n", ($stop - $start)/$tests, $tests );
    unset( $stop, $start );
    
    $start = microtime( TRUE );
    for( $i = 1; $i <= $tests; $i += 1 ) {
        preg_match('/a*+b/','aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac');
    }
    $stop = microtime( TRUE );
    
    printf( "For /a*+b/   : %1.15f per iteration for %s iterations\n", ($stop - $start)/$tests, $tests );
    unset( $stop, $start );
    
    $start = microtime( TRUE );
    for( $i = 1; $i <= $tests; $i += 1 ) {
        preg_match('/(?>a)*b/','aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac');
    }
    $stop = microtime( TRUE );
    
    printf( "For /(?>a)*b/: %1.15f per iteration for %s iterations\n", ($stop - $start)/$tests, $tests );
    unset( $stop, $start );
    

    将其作为输出:

    For /a*b/    : 0.000000879034996 per iteration for 1000000 iterations
    For /(?>a*)b/: 0.000000876362085 per iteration for 1000000 iterations
    For /a*+b/   : 0.000000880002022 per iteration for 1000000 iterations
    For /(?>a)*b/: 0.000000883045912 per iteration for 1000000 iterations
    

    现在,我绝不是 PHP 专家,所以我不知道这是否是对其中的内容进行基准测试的正确方法,但它们都具有大致相同的性能,考虑到简单性,这是意料之中的任务。

    不过,我从上面注意到了几件事:

    (?&gt;a)*b 和 (?&gt;a*)b 都比另一个正则表达式快 179%;以上都在7%以内。

    回到实际问题

    但是为什么步数更多呢?有人可以解释一下吗?

    需要注意的是,步数并不是正则表达式性能的直接表示。这是一个因素,但不是最终的决定因素。步骤比较多,因为细分有进组前的步骤,进组后...

    1   / (?> a* ) b/x    aaaaaaaaaaaaaaaaaaaa...
         ^
    2   / (?> a* ) b/x    aaaaaaaaaaaaaaaaaaaa...
          ^^^^^^^^
    3   / (?> a* ) b/x    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac
              ^^
    4   / (?> a* ) b/x    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac
                ^
    5   / (?> a* ) b/x    aaaaaaaaaaaaaaaaaaaaa...
                   ^
    

    由于组的原因,这比 3 个步骤多 2 个步骤...

    1   / a*+ b /x    aaaaaaaaaaaaaaaaaaaa...
         ^
    2   / a*+ b /x    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac
          ^^^
    3   / a*+ b /x    aaaaaaaaaaaaaaaaaaaaa...
              ^
    

    您可以说a*b 与(?:a*)b 相同,但后者的步骤更多:

    1   / (?: a* ) b/x    aaaaaaaaaaaaaaaaaaaa...
         ^
    2   / (?: a* ) b/x    aaaaaaaaaaaaaaaaaaaa...
          ^^^^^^^^
    3   / (?: a* ) b/x    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac
              ^^
    4   / (?: a* ) b/x    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaac
                ^
    5   / (?: a* ) b/x    aaaaaaaaaaaaaaaaaaaaa...
    

    注意:即使在那里,您也会看到 regex101 的步骤针对a*b 中的步骤数进行了一些优化。


    结论

    所有格量词和原子组的工作方式不同,具体取决于您使用它们的方式。如果我以正则表达式为例并稍微调整一下:

    (?&gt;a|b)*ac匹配aabaac

    但是

    (?:a|b)*+ac 和 (?&gt;(?:a|b)*)ac 不匹配 aabaac。

    【讨论】:

    • 我在regexhero.net检查了性能问题。我所说的步骤差异在a*b和(?&gt;a*)b之间。在regex101.com,当我们禁用优化时存在差异。我我试图找出它背后的原因。分别是 67 和 133
    • @vks 好吧,好吧,但我看不出这会如何改变我的答案内容的步骤。在性能方面,(?&gt;a*)b is the fastest 但不是 (?&gt;a)*b。
    • 哦,谢谢指出。有人编辑过。编辑回原来的。问题是 67 与 133。原子团的存在是否增加了步骤?为什么比正常情况下失败需要很长时间one.... atomic 的定义表明它应该是相反的吧?
    • @vks 具有(正确)原子组的那个是最快的;不是最慢的。
    • 这就是我所怀疑的。原子组的性能最快,但失败的步骤更多。这就是我所追求的:(
    猜你喜欢
    • 2016-05-13
    • 2011-07-29
    • 1970-01-01
    • 1970-01-01
    • 2023-02-25
    • 1970-01-01
    • 2013-07-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多