改进/修复C风格块注释的正则表达式

10

我正在编写一个简单的解析器(使用C#语言),用于处理类似于经典C语言的脚本语言。

在我的一个脚本文件中,我使用的正则表达式来识别/*块注释*/进入了某种无限循环,CPU占用率达到了100%,持续了很长时间。

我正在使用的正则表达式是这个:

/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/

有什么建议可以解释为什么会死锁吗?

或者,我能用另一种正则表达式来替代吗?

更多信息:

  • 在C# 3.0中工作,目标框架是.NET 3.5;
  • 我正在使用Regex.Match(string,int)方法从字符串的特定索引开始匹配;
  • 我已经让程序运行超过一个小时了,但匹配还没有完成;
  • 传递给Regex构造函数的选项是RegexOptions.MultilineRegexOptions.IgnorePatternWhitespace
  • 这个正则表达式对我的453个测试文件中的452个都可以正确工作。

使用正则表达式进行这种操作很困难。你应该如何区分注释和包含与注释相同语法序列的字符串? - Gumbo
在他的书《精通正则表达式》中,Jeffrey Friedl 解释了如何在 C 语言中找到所有 /* 在引号字符串中的组合、反之亦然等所有情况,这被认为是几乎不可能的。但是他接着描述了一个复杂的正则表达式来实现这一点。 - codybartfast
@Gumbo - 我使用这个正则表达式来检查从已知索引点开始的注释,而不是在文本中的任何位置。当我找到一个字符串或注释时,在检查另一个匹配之前,我跳过整个范围。 - Bevan
5个回答

18

您的正则表达式存在一些问题:

在您的正则表达式中,不需要使用| [\r\n] 序列; 像[^*]这样的否定字符类将匹配除了*以外的所有内容,包括行分隔符。只有“。”(点)元字符不匹配它们。

一旦您进入注释,您需要查找的唯一字符是星号;只要您没有看到其中之一,您就可以吞掉尽可能多的字符。这意味着当您可以使用[^*]+时,请勿使用[^*]。实际上,您可以将其放在原子组中 - (?>[^*]+) - 因为一旦匹配了这些非星号字符,您永远没有任何理由放弃它们。

过滤掉杂乱无用的部分,您最外层括号内的最终选择是\*+[^*/],表示“一个或多个星号,后跟一个不是星号或斜杠的字符”。它将始终匹配注释末尾的星号,并且它将始终不得不放弃该星号,因为下一个字符是斜杠。实际上,如果在最终斜杠之前有20个星号,则您的正则表达式将匹配它们所有,然后逐个放弃它们。然后最后一部分 - \*+/ - 将永久匹配它们。

为了获得最佳性能,我建议使用以下正则表达式:

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

这将快速匹配格式良好的注释,但更重要的是,如果开始匹配不是有效注释的内容,它会尽可能快地失败。


感谢David,这里提供了一个可以匹配任意嵌套层次的注释的版本:

(?s)/\*(?>/\*(?<LEVEL>)|\*/(?<-LEVEL>)|(?!/\*|\*/).)+(?(LEVEL)(?!))\*/

它使用了 .NET 的平衡组,因此在其他任何语言中都不适用。为了完整起见,这里有另一个版本(来自RegexBuddy库),它使用了Perl、PCRE和Oniguruma/Onigmo支持的递归组语法:

/\*(?>[^*/]+|\*[^/]|/[^*])*(?>(?R)(?>[^*/]+|\*[^/]|/[^*])*)*\*/

谢谢Alan,这看起来就是我需要的 - 不过我需要仔细研究一下以确保我理解了它!在我尝试之后会回报的。 - Bevan
抱歉,忘记及时回复了。是的,这就是我需要的。感谢你的帮助。 - Bevan
这段代码无法处理嵌套的C风格注释。当第一个*/出现时,它就会中断,而不考虑之前是否有另一个/*。 - BeanFlicker
@David:你知道有哪种语言支持嵌套注释吗? - Alan Moore
@ Alan:是的,MS SQL Server允许嵌套块样式注释。我也找到了一个有效的正则表达式。 /*(?>/*(?<LEVEL>)|*/(?<-LEVEL>)|(?! /* | */ ).)+(?(LEVEL)(?!))*/ - BeanFlicker
1
@David:我已经将你的正则表达式添加到答案中,这样更易读了。我一直以为嵌套注释只是一个神话。 :-/ - Alan Moore

15

不不不!难道没有其他人读过《精通正则表达式(第3版)》吗?在这本书中,Jeffrey Friedl详细研究了这个问题,并将其作为示例(第272-276页)来说明他的“展开循环”技术。对于大多数正则表达式引擎,他的解决方案如下:

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

然而,如果正则表达式引擎被优化以处理惰性量词(像Perl一样),那么最有效的表达式会更简单一些(如上所建议):

/\*.*?\*/

(当然,应用了等效的“点匹配所有”限定符)。请注意,我不使用.NET,因此无法确定哪个版本对该引擎更快。


+1,从超过7个stackoverflow问题的多个可能性中,这是唯一一个真正有效的! - Stefan Steiger
即使在初始的/\*之后我不理解正在发生的事情,但我关心的是它能够成功匹配注释。 - Alex Essilfie

2

如果您使用Singleline选项而不是Multiline选项,那么您就不必担心\r\n的问题。启用该选项后,以下内容对我进行了简单测试,并包含了跨越多行的注释:

/\*.*?\*/

但是,正则表达式匹配器的贪婪匹配不会导致这个匹配从第一个注释的/开始/一直延伸到最后一个注释的*/结束吗? - Bevan
问号使得 .* 变成非贪婪或懒惰模式,详见此处:http://www.regular-expressions.info/repeat.html#lazy - Alan Moore

1

我目前正在使用这个

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

1

我认为你的表达式过于复杂。应用于大字符串时,许多替代方案意味着很多回溯。我猜这就是你看到性能下降的原因。

如果基本假设是匹配从"/*"到遇到第一个"*/"的所有内容,则一种方法是这样做的(通常,正则表达式不适用于嵌套结构,因此嵌套块注释无法工作):

/\*(.(?!\*/))*.?\*/             // run this in single line (dotall) mode

基本上这个意思是:"/*",后面跟着任何不被"*/"紧随其后的内容,最后以"*/"结尾。

或者,您可以使用更简单的方法:

/\*.*?\*/                       // run this in single line (dotall) mode

像这样的非贪婪匹配在某些边缘情况下可能会出现问题 - 目前我想不到一个表达式可能失败的情况,但我并不完全确定。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接