使用正则表达式解析C样式注释,避免回溯

时间:2015-10-12 18:10:44

标签: javascript python regex backtracking

我希望匹配JavaScript文件中的所有块和多行注释(这些是C样式注释)。我有一个运作良好的模式。但是,它会创建一些回溯速度,从而显着降低速度,尤其是在较大的文件上。

模式:\/\*(?:.|[\r\n])*?\*\/|(?:\/\/.*)

示例:https://www.regex101.com/r/pR6eH6/2

如何避免回溯?

2 个答案:

答案 0 :(得分:6)

由于交替,你有大量的回溯。您可以考虑使用可以显着提升性能的字符类(?:.|[\r\n]),而不是[\s\S]

\/\*[\s\S]*?\*\/|\/\/.*

请参阅demo

在Python中,您也可以使用re.S / re.DOTALL修饰符来使.匹配换行符(请注意,单行注释模式应与{{1}匹配然后):

\/\/[^\r\n]*

请参阅another demo

但是,因为/\*.*?\*/|//[^\r\n]* 延迟量词也会导致类似于贪婪量词引起的开销,您应该考虑使用更优化的模式对于C style multiline comments - *?,整个正则表达式现在看起来像:

/\*[^*]*\*+(?:[^/*][^*]*\*+)*/

请参阅yet another demo

<强>详情:

  • /\*[^*]*\*+(?:[^/*][^*]*\*+)*/|//.* - /\*
  • /* - 除[^*]*
  • 以外的零个或多个字符
  • * - 一个或多个星号
  • \*+ - 零个或多个序列:
    • (?:[^/*][^*]*\*+)* - 除[^/*]/
    • 以外的其他符号
    • * - 除[^*]*
    • 以外的零个或多个符号
    • * - 1+ asterisks
  • \*+ - /符号
  • / - 或
  • | - //.*以及除了换行符之外的任何0 +字符。

只是想注意,在Python中,您不需要转义//(在JS中,在使用RegExp构造函数声明正则表达式时,您不需要转义/

注意:最后一种模式不允许简单捕捉//*内部的内容,但由于模式比其他模式更稳定,因此我可以d建议使用它,即使您需要使用尾随*/ - *捕获内容 - 然后您需要从/\*([^*]*\*+(?:[^/*][^*]*\*+)*)/|//(.*)删除最后一个字符。

答案 1 :(得分:1)

你可以用你的模式做什么?

您的实际模式是:

 \/\*(?:.|[\r\n])*?\*\/|(?:\/\/.*)

或没有无用的反斜杠和组:

/\*(?:.|[\r\n])*?\*/|//.*

正如stribizhev所解释的(?:.|[^\r\n])*?可以使用DOTALL模式以更简单的方式编写,即:.*?或不使用[\s\S]代替点。

但是如果你将主要交替的两个分支(多行注释的分支和单行注释的分支)的第一个字符/放在一起,你可以做得更好:

/(?:\*[\s\S]*?\*/|/.*)

这种变化的两个好处是:

  1. 开始一个带有交替的模式并不是一个好主意,必须尽可能避免,因为正则表达式引擎必须测试字符串中每个位置的交替的两个分支(在最坏的情况下)。所以在你的情况下(只有两个分支),你可以认为正则表达式引擎工作是X2。 如果你把第一个字符(或者更多的标记,如果可能的话)放在因子中,字符串中不感兴趣的位置的最大部分被更快地丢弃(不以/开头的位置),因为只有一个分支测试第一个字符不是好字符。

  2. 当您使用文字字符串启动模式时,正则表达式引擎能够使用更快的算法直接查找模式可能成功的字符串中的位置(文字字符串出现的位置)。在您的情况下,使用此优化将使您的模式更快。<​​/ p>

  3. 您可以改进的其他事项:非贪婪量词

    非贪婪量词本质上是缓慢的(与贪心量词相比)因为每次取一个字符时,它必须测试模式的结尾是否成功(直到模式结束成功)。换句话说,当回溯机制发生时,非贪婪的量词可能比贪婪的量词更糟糕(回溯机制以及量词的工作方式是更重要的(最重要的)重要事项之一,花时间去做)

    您可以更有效地重写子模式\*[\s\S]*?\*/

    \*[^*]*\*+(?:[^*/][^*]*\*+)*/
    

    细节:

    \*    # literal asterisk
    [^*]* # zero or more character that are not an asterisk
    \*+   # one or more asterisks: this one will match either the last asterisk(s)
          # before the closing slash or asterisk(s) inside the comment.
    
    (?:[^*/][^*]*\*+)* # In case there are asterisks(s) inside the comment, this
                       # optional group ensures the next character isn't a slash: [^*/]
                       # and reach the next asterisk(s): [^*]*\*+
    /    # a literal slash
    

    这个子模式更长,但效率更高,因为它只使用贪婪量词,并且将回溯步骤减少到最小。

    现在的模式是:

    /(?:\*[^*]*\*+(?:[^*/][^*]*\*+)*/|/.*)
    

    并且只需要~950步(而不是~12500)来查找示例字符串的63次出现。

    demo