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

Posted

技术标签:

【中文标题】使用正则表达式解析 C 样式注释,避免回溯【英文标题】:Parse C-Style Comments with Regex, avoid Backtracking 【发布时间】:2016-01-10 07:29:24 【问题描述】:

我想匹配 javascript 文件中的所有块和多行 cmets(这些是 C 样式 cmets)。我有一个很好用的模式。但是,它会产生一些回溯,从而显着减慢速度,尤其是在较大的文件上。

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

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

如何避免回溯?

【问题讨论】:

使用\/\*[\s\S]*?\*\/|\/\/.*你会获得一些性能。 我猜如果你通过了两次,你唯一可能出错的是/* comments such as // this. */ 【参考方案1】:

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

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

见demo

在 Python 中,您也可以使用 re.S/re.DOTALL 修饰符使 . 匹配换行符(注意单行注释模式应与 \/\/[^\r\n]* 匹配):

/\*.*?\*/|//[^\r\n]*

见another demo

然而,由于*? 惰性量词 也会导致类似于贪婪量词引起的开销,您应该考虑为@ 使用更优化的模式987654323@ - /\*[^*]*\*+(?:[^/*][^*]*\*+)*/,整个正则表达式现在看起来像:

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

见yet another demo

详情

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

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

注意:最后一个模式不允许简单捕获 /**/ 内部的内容,但由于该模式比其他模式更稳定,我建议即使在您需要捕获带有尾随 * - /\*([^*]*\*+(?:[^/*][^*]*\*+)*)/|//(.*) 的内容,然后您需要从 .group(1) 中删除最后一个字符。

【讨论】:

这只有一个问题。事实上,字符串可以包含类似于语法的注释而不是真正的注释 @SMJS 是的,这适用于本网站上发布的 90% 的评论匹配正则表达式。解决方案是使用专用解析器。一种解决方法是匹配和忽略引号之间的字符串,在 JS 中,要删除字符串文字之外的 cmets,您可以使用 .replace(/((['"])(?:(?!\2)[^\\]|\\[\w\W])*\2)|\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\/|\/\/.*/g, (x, y) => y || '')。要提取,捕获正则表达式匹配 cmets 的另一部分,然后仅获取该捕获结果。【参考方案2】:

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

你的实际模式是:

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

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

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

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

但是,如果您将第一个字符 / 考虑在内,您可以做得更好,这对于您的主要交替的两个分支(多行 cmets 的分支和单行 cmets 的分支)是共同的:

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

这种变化的两个好处:

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

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

您可以改进的其他方面:非贪婪量词

非贪心量词本质上是慢的(与贪心量词相比),因为每次它需要一个字符时,它必须测试模式的结尾是否成功(直到模式的结尾成功)。换句话说,当回溯机制发生时,非贪婪量词可能比贪婪量词更糟糕(回溯机制和量词如何工作是更(最?)重要的事情之一,请花点时间了解) .

您可以以更有效的方式重写子模式\*[\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

【讨论】:

以上是关于使用正则表达式解析 C 样式注释,避免回溯的主要内容,如果未能解决你的问题,请参考以下文章

使用 Perl 正则表达式删除多行 C 样式 /* 注释 */

防止在正则表达式上回溯以查找非注释行(不以缩进的“#”开头)

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

Javascript--正则表达式工作原理, 回溯

正则表达式回溯法原理

正则表达式删除单行 SQL 注释 (--)