使用正则表达式从源文件中删除注释

34

我正在编写一个程序来自动化编写一些C代码(我正在编写将字符串解析为具有相同名称的枚举的程序)。 C对字符串的处理并不那么出色,所以有些人一直在催促我尝试Python。

我编写了一个函数,它应该从字符串中删除C风格的/* COMMENT *///COMMENT: 以下是代码:

def removeComments(string):
    re.sub(re.compile("/\*.*?\*/",re.DOTALL ) ,"" ,string) # remove all occurance streamed comments (/*COMMENT */) from string
    re.sub(re.compile("//.*?\n" ) ,"" ,string) # remove all occurance singleline comments (//COMMENT\n ) from string

所以我尝试了这段代码。

str="/* spam * spam */ eggs"
removeComments(str)
print str

但它似乎什么都没有做。

你有什么建议,我做错了什么吗?

我听过一句话:

如果你有问题,并试图用正则表达式解决它,那么你最终会得到两个问题。


编辑: 多年后回顾这个问题(经过更多的解析经验), 我认为正则表达式可能是正确的解决方案。 而且此处使用的简单正则表达式已经“足够好”了。 我可能没有在问题中强调这一点。 这是针对一个具体文件的。该文件没有棘手的情况。 我认为保持要解析的文件简单到足以使用正则表达式进行解析,比将正则表达式复杂化成难以阅读的符号组合要容易维护得多。(例如,要求该文件仅使用//单行注释。)


4
只有一个合理的回答:http://kore-nordmann.de/blog/do_NOT_parse_using_regexp.html。虽然他谈论的是另一种语言,但他的结论仍然适用。 - Jerry Coffin
1
@Steve314:你可以猜测一个合理的嵌套限制(例如,在C中,注释根本不会嵌套),但这没什么用。举个明显的例子,在字符串文字中的注释分隔符不计入嵌套,但跨行断开的注释分隔符(字符之间带有反斜杠)计入嵌套的。在正则表达式中正确考虑其中任何一种情况都是至少非常困难的。 - Jerry Coffin
@JerryCoffin 实际上,合理的回复应该是 https://dev59.com/X3I-5IYBdhLWcg3wq6do#1732454 - Tobias Kienzler
另外,你不能只使用 C++ 预处理器吗? - Tobias Kienzler
托比亚斯,是的,如果我没记错,GCC -E。 - Frames Catherine White
显示剩余3条评论
11个回答

57

那么关于"//引号内的注释字符串"怎么办?

OP正在问如何使用正则表达式来完成此操作:

def remove_comments(string):
    pattern = r"(\".*?\"|\'.*?\')|(/\*.*?\*/|//[^\r\n]*$)"
    # first group captures quoted strings (double or single)
    # second group captures comments (//single-line or /* multi-line */)
    regex = re.compile(pattern, re.MULTILINE|re.DOTALL)
    def _replacer(match):
        # if the 2nd group (capturing comments) is not None,
        # it means we have captured a non-quoted (real) comment string.
        if match.group(2) is not None:
            return "" # so we will return empty to remove the comment
        else: # otherwise, we will return the 1st group
            return match.group(1) # captured quoted-string
    return regex.sub(_replacer, string)

这个操作将会移除:

  • /* 多行注释 */
  • // 单行注释

不会移除:

  • String var1 = "this is /* not a comment. */";
  • char *var2 = "this is // not a comment, either.";
  • url = 'http://not.comment.com';

注意:此方法也适用于 Javascript 代码。


1
/* 或许我们//“不应该/*这样做*///但是让我们这样做*/ //”无论如何 - Tobias Kienzler
整洁,它甚至可以管理交错的字符串,例如'a = /* a "-nested string */ "comments can end with */" // comment2'!我发现分组使正则表达式更加强大和易于理解。我想知道是否仍然可以构造有效的C代码来使您的正则表达式失败;) 但这可能涉及一些非常恶心的预编译器内容... - Tobias Kienzler
如果你有转义的引号,比如 "some 2\" string" /* remove this */ "another string",这个方法就会失败。 - ishmael
6
这是一个改进后的正则表达式,它使用负回顾后发断言来避免转义引号。 r"(\".*?(?<!\\)\"|\'.*?(?<!\\)\')|(/\*.*?\*/|//[^\r\n]*$)" - ishmael
2
@ishmael 那么 "带有反斜杠 \\" /* 删除此部分 */ "..." 呢? - Mariano
显示剩余5条评论

49

re.sub返回一个字符串,所以将您的代码更改为以下内容将会有结果:

def removeComments(string):
    string = re.sub(re.compile("/\*.*?\*/",re.DOTALL ) ,"" ,string) # remove all occurrences streamed comments (/*COMMENT */) from string
    string = re.sub(re.compile("//.*?\n" ) ,"" ,string) # remove all occurrence single-line comments (//COMMENT\n ) from string
    return string

5
char *note = "您可以使用//来进行单行注释"; 哎呀。 - Mike Graham
确实。这只回答了为什么 OP 的函数没有返回任何结果。 - msanders
字符串 = re.sub(re.compile("^//.*?$", re.MULTILINE ) ,"" ,字符串) - Nicholas Franceschina
上面的代码对于单行注释对我来说不起作用。removeComments("// 单行注释")返回'// 单行注释'。 - Bill
这是因为文本“// 单行注释”没有换行符作为最后一个符号吗? - Gombat
显示剩余2条评论

24

我建议使用像SimpleParsePyParsing这样的真正解析器。 SimpleParse要求您实际上知道EBNF,但速度非常快。 PyParsing有自己的类似EBNF的语法,但适用于Python,并使构建功能强大的准确解析器变得轻而易举。

编辑:

以下是如何在此上下文中轻松使用PyParsing的示例:

>>> test = '/* spam * spam */ eggs'
>>> import pyparsing
>>> comment = pyparsing.nestedExpr("/*", "*/").suppress()
>>> print comment.transformString(test)         
' eggs'

这是一个更复杂的例子,使用单行和多行注释。

之前:


/*
 * multiline comments
 * abc 2323jklj
 * this is the worst C code ever!!
*/
void
do_stuff ( int shoe, short foot ) {
    /* this is a comment
     * multiline again! 
     */
    exciting_function(whee);
} /* extraneous comment */

之后:

>>> print comment.transformString(code)   

void
do_stuff ( int shoe, short foot ) {

     exciting_function(whee);
} 

它在剥离注释的地方会留下额外的空行,但这可以解决。


正则表达式不好,但解析又过度了?我很困惑,还有什么其他的选择? - jathanism
我之前的思路有误 - 基于简单交替正则表达式进行搜索比编写解析器要容易得多。尽管如此,它并不能解决字符串中出现的混淆问题。正如Mike所评论的那样,解析器(或Lexer)可能是完成任务的恰当工具。 - user180247
是的,如果你的输入数据容易处理,比如像 IP 地址或电话号码这样有一致格式的数据,那么正则表达式是“容易”的。对于其他所有情况,请使用词法分析器。 - jathanism
我认为这并不会留下额外的换行符 - 换行符只是注释的一部分,因此它不会被剥离,而且在 C 中,删除它并不一定安全,因为换行符 可以 用作有意义的空格。 - John La Rooy
啊,这是一个很好的观察,现在我再看一遍,我同意。 :) - jathanism

8

在 Jathanism 的基础上使用 pyparsing 找到了另一种解决方案。

import pyparsing

test = """
/* Code my code
xx to remove comments in C++
or C or python */

include <iostream> // Some comment

int main (){
    cout << "hello world" << std::endl; // comment
}
"""
commentFilter = pyparsing.cppStyleComment.suppress()
# To filter python style comment, use
# commentFilter = pyparsing.pythonStyleComment.suppress()
# To filter C style comment, use
# commentFilter = pyparsing.cStyleComment.suppress()

newtest = commentFilter.transformString(test)
print(newest)

产生以下输出:
include <iostream> 

int main (){
    cout << "hello world" << std::endl; 
}

还可以使用python风格的注释、java风格的注释和cpp风格的注释。我发现这很有用。


4
我建议您阅读这个页面,它对问题进行了详细分析,并使您更好地理解为什么您的方法不起作用:http://ostermiller.org/findcomment.html 简短版本:您要查找的正则表达式是这样的:
(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|(//.*)

这应该匹配两种类型的注释块。如果您有问题,请阅读我链接的页面。


除非我漏掉了什么,否则这将会忽略跨行的注释分隔符(斜杠、反斜杠、换行符、星号或星号、反斜杠、换行符、斜杠)。更糟糕的是,那个反斜杠可以被生成为三字符序列 ??/(尽管我承认三字符序列非常罕见)。 - Jerry Coffin

1
mystring="""
blah1 /* comments with
multiline */

blah2
blah3
// double slashes comments
blah4 // some junk comments

"""
for s in mystring.split("*/"):
    s=s[:s.find("/*")]
    print s[:s.find("//")]

输出

$ ./python.py

blah1


blah2
blah3

1

你做错了。

正则表达式是用于正则语言的,而C语言不是。


当然,词法分析器和语法分析器之间的一个普遍期望的区别是词法分析器只支持正则语言。当然并非总是如此(例如,查看Ragel),就像使用正则表达式一样。一个好的词法分析器可以完成工作,但是和使用语法分析器一样,仅仅为了注释去除而使用它似乎过于浪费。 - user180247
@Steve314,如果你所说的“overkill”的意思是完全正确的工具,那就是这样。这里发布的所有正则表达式都有严重的缺陷,在面对有效的、现实的C(++)代码时无法正确处理。 - Mike Graham
了解词法分析器相关知识,删除我对词法分析器的推荐。 - Otto Allmendinger
@Mike - 重新考虑,我同意你的观点 - 但具体原因还没有被提到(尽管它是你“有效,现实”观点的一个特例)。我只是想到了像注释标记一样的东西,但实际上只是字符串文字中的字符。如果没有正确的工具来避免这些字符,那将会是一项不愉快的工作。使用现有的C词法分析器(只要它保留空格)- 并不那么糟糕。 - user180247
@Mike - 我自己的答案被删除了,这是有害的。 - user180247
@Steve314,那是明显有效、现实的代码。(就像我之前回复msanders时发布的示例一样。) - Mike Graham

1

我看到你可能想修改的几个地方。

首先,Python按值传递对象,但有些对象类型是不可变的。字符串和整数就属于这些不可变的类型。所以如果你把一个字符串传给一个函数,在函数内对该字符串进行的任何更改都不会影响你传入的那个字符串。你应该尝试返回一个新的字符串。另外,在removeComments()函数内部,你需要将re.sub()返回的值赋给一个新的变量 - 就像任何接受字符串作为参数的函数一样,re.sub()不会修改字符串本身。

其次,我同意其他人关于解析C代码的观点。在这里,正则表达式并不是最佳选择。


0

正如我在其他评论中指出的那样,注释嵌套并不是问题(在C语言中,注释不能嵌套,尽管一些编译器仍支持嵌套注释)。问题在于像字符串文字这样的东西,它们可以包含与注释分隔符完全相同的字符序列,但实际上不是注释。

正如Mike Graham所说,适合此工作的正确工具是词法分析器。解析器是不必要的且过度的,但是词法分析器是确切的正确工具。恰好我今天早上发布了一个(部分)C(和C ++) lexer。它不会尝试正确识别所有词法元素(即所有关键字和运算符),但它完全足以剥离注释。它对“使用Python”毫无帮助,因为它完全是用C编写的(它比我更多地用于实验性代码之前)。


0

想要添加另一个正则表达式,以便在 Python 中删除 * 和 ; 之间的任何内容

data = re.sub(re.compile("*.*?\;",re.DOTALL),' ',data)

在元字符前面加上反斜杠以转义


为什么这与原问题有关? - E.Coms

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