Python 正则表达式读取 C 风格注释
我正在尝试在一个C语言文件中找到C风格的注释,但如果引号里面有//,我就遇到麻烦了。这个文件是:
/*My function
is great.*/
int j = 0//hello world
void foo(){
//tricky example
cout << "This // is // not a comment\n";
}
它会和那个cout匹配。这是我目前的进展(我已经可以匹配/**/类型的注释了)
fp = open(s)
p = re.compile(r'//(.+)')
txt = p.findall(fp.read())
print (txt)
2 个回答
Python的re.findall
方法基本上和大多数词法分析器的工作方式差不多:它会从上一个匹配结束的地方开始,依次返回最长的匹配。你只需要把所有的词法模式组合起来就行了:
(<pattern 1>)|(<pattern 2>)|...|(<pattern n>)
和大多数词法分析器不同的是,它不要求匹配的内容是连续的,但这并不是一个大问题,因为你总是可以在最后加上(.)
,这样就能单独匹配所有未匹配的字符。
re.findall
的一个重要特点是,如果正则表达式中有分组,那么只会返回这些分组的内容。因此,你可以通过简单地去掉括号,或者把它们改成非捕获括号,来排除其他选择:
(<pattern 1>)|(?:<unimportant pattern 2>)|(<pattern 3)
考虑到这一点,我们来看看如何对C语言进行分词,以便识别注释。我们需要处理:
- 单行注释:
// 注释
- 多行注释:
/* 注释 */
- 双引号字符串:
"可能包含像\n这样的转义字符"
- 单引号字符:
'\t'
- (下面还有一些更麻烦的情况)
考虑到这些,我们为上述每种情况创建正则表达式。
- 两个斜杠后面跟着除了换行符以外的任何内容:
//[^\n]*
- 这个正则表达式解释起来比较繁琐:
/*[^*]*[*]+(?:[^/*][^*]*[*]+)*/
注意它使用了(?:...)
来避免捕获重复的分组。 - 一个引号,后面跟着任何重复的非引号和反斜杠的字符,或者一个反斜杠后面跟着的任何字符。这不是对转义序列的精确定义,但足以检测到"何时结束字符串,这正是我们关心的:
"(?:[^"\\]|\\.*)"
- 和(3)相同,但用单引号:
'(?:[^'\\]|\\.)*'
最后,我们的目标是找到C风格注释的文本。所以我们只需要在其他分组中避免捕获。于是:
p = re.compile('|'.join((r"(//[^\n])*"
,r"/*[^*]*[*]+(?:[^/*][^*]*[*]+)*/"
,'"'+r"""(?:[^"\\]|\\.)*"""+'"'
,r"'(?:[^'\\]|\\.)*'")))
return [c[2:] for c in p.findall(text) if c]
在上面,我省略了一些不太可能出现的特殊情况:
在
#include <...>
指令中,<...>
本质上是一个字符串。理论上,它可以包含引号或看起来像注释的序列,但实际上你永远不会看到:#include </*This looks like a comment but it is a filename*/>
以\结尾的行会在下一行继续;\和后面的换行符会从输入中直接去掉。这发生在任何词法扫描之前,所以下面的内容是一个完全合法的注释(实际上是两个注释):
/\ **************** Surprise! **************\ //////////////////////////////////////////
更糟糕的是,三重符号
??/
和\是一样的,这种替换发生在处理续行之前。/************************************//??/ **************** Surprise! ************??/ //////////////////////////////////////////
在混淆比赛之外,实际上没有人会使用三重符号。但它们仍然在标准中。处理这两个问题的最简单方法是预先扫描字符串:
return [c[2:] for c in p.findall(text.replace('//?','\\').replace('\\\n','')) if c]
如果你真的在意#include <...>
的问题,唯一的解决办法就是再加一个模式,比如#define\s*<[^>\n]*>
。
第一步是找出那些//
或/*
不应该被当作注释开始的情况。例如,当它们在字符串内部(也就是在引号之间)时。为了避免处理引号之间的内容,我们可以用一个捕获组把它们包起来,并在替换模式中插入一个反向引用:
模式:
(
"(?:[^"\\]|\\[\s\S])*"
|
'(?:[^'\\]|\\[\s\S])*'
)
|
//.*
|
/\*(?:[^*]|\*(?!/))*\*/
替换:
\1
因为引号部分是优先搜索的,所以每次找到//
或/*...*/
时,你可以确定自己不在字符串内部。
需要注意的是,这个模式故意设计得不够高效(因为有(A|B)*
这样的子模式),这样做是为了让它更容易理解。如果想让它更高效,可以这样重写:
("(?=((?:[^"\\]+|\\[\s\S])*))\2"|'(?=((?:[^'\\]+|\\[\s\S])*))\3')|//.*|/\*(?=((?:[^*]+|\*(?!/))*))\4\*/
(?=(something+))\1
只是模拟一个原子组 (?>something+)
的方法。
所以,如果你只想找到注释(而不是删除它们),最方便的方法是把注释部分放在捕获组里,然后测试它是否为空。以下模式经过更新(根据Jonathan Leffler的评论),以处理三重字符??/
,这个字符在预处理器中被解释为反斜杠(我假设代码不是为-trigraphs
选项编写的),并处理后面跟着换行符的反斜杠,这样可以把一行代码分成多行:
fp = open(s)
p = re.compile(r'''(?x)
(?=["'/]) # trick to make it faster, a kind of anchor
(?:
"(?=((?:[^"\\?]+|\?(?!\?/)|(?:\?\?/|\\)[\s\S])*))\1" # double quotes string
|
'(?=((?:[^'\\?]+|\?(?!\?/)|(?:\?\?/|\\)[\s\S])*))\2' # single quotes string
|
(
/(?:(?:\?\?/|\\)\n)*/(?:.*(?:\?\?|\\)/\n)*.* # single line comment
|
/(?:(?:\?\?/|\\)\n)*\* # multiline comment
(?=((?:[^*]+|\*+(?!(?:(?:\?\?/|\\)\n)*/))*))\4
\*(?:(?:\?\?/|\\)\n)*/
)
)
''')
for m in p.findall(fp.read()):
if (m[2]):
print m[2]
这些更改不会影响模式的效率,因为正则引擎的主要工作是找到以引号或斜杠开头的位置。这个任务通过模式开头的前瞻(?=["'/])
的存在得到了简化,这样可以快速找到第一个字符。
另一个优化是使用模拟的原子组,这样可以把回溯减少到最小,并允许在重复组内使用贪婪量词。
注意:C语言中没有 heredoc 语法!