Python 正则表达式读取 C 风格注释

2 投票
2 回答
2308 浏览
提问于 2025-04-20 14:05

我正在尝试在一个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 个回答

2

Python的re.findall方法基本上和大多数词法分析器的工作方式差不多:它会从上一个匹配结束的地方开始,依次返回最长的匹配。你只需要把所有的词法模式组合起来就行了:

(<pattern 1>)|(<pattern 2>)|...|(<pattern n>)

和大多数词法分析器不同的是,它不要求匹配的内容是连续的,但这并不是一个大问题,因为你总是可以在最后加上(.),这样就能单独匹配所有未匹配的字符。

re.findall的一个重要特点是,如果正则表达式中有分组,那么只会返回这些分组的内容。因此,你可以通过简单地去掉括号,或者把它们改成非捕获括号,来排除其他选择:

(<pattern 1>)|(?:<unimportant pattern 2>)|(<pattern 3)

考虑到这一点,我们来看看如何对C语言进行分词,以便识别注释。我们需要处理:

  1. 单行注释:// 注释
  2. 多行注释:/* 注释 */
  3. 双引号字符串:"可能包含像\n这样的转义字符"
  4. 单引号字符:'\t'
  5. (下面还有一些更麻烦的情况)

考虑到这些,我们为上述每种情况创建正则表达式。

  1. 两个斜杠后面跟着除了换行符以外的任何内容://[^\n]*
  2. 这个正则表达式解释起来比较繁琐:/*[^*]*[*]+(?:[^/*][^*]*[*]+)*/ 注意它使用了(?:...)来避免捕获重复的分组。
  3. 一个引号,后面跟着任何重复的非引号和反斜杠的字符,或者一个反斜杠后面跟着的任何字符。这不是对转义序列的精确定义,但足以检测到"何时结束字符串,这正是我们关心的:"(?:[^"\\]|\\.*)"
  4. 和(3)相同,但用单引号:'(?:[^'\\]|\\.)*'

最后,我们的目标是找到C风格注释的文本。所以我们只需要在其他分组中避免捕获。于是:

p = re.compile('|'.join((r"(//[^\n])*"
                        ,r"/*[^*]*[*]+(?:[^/*][^*]*[*]+)*/"
                        ,'"'+r"""(?:[^"\\]|\\.)*"""+'"'
                        ,r"'(?:[^'\\]|\\.)*'")))
return [c[2:] for c in p.findall(text) if c]

在上面,我省略了一些不太可能出现的特殊情况:

  1. #include <...>指令中,<...>本质上是一个字符串。理论上,它可以包含引号或看起来像注释的序列,但实际上你永远不会看到:

    #include </*This looks like a comment but it is a filename*/>
    
  2. \结尾的行会在下一行继续;\和后面的换行符会从输入中直接去掉。这发生在任何词法扫描之前,所以下面的内容是一个完全合法的注释(实际上是两个注释):

    /\
    **************** Surprise! **************\
    //////////////////////////////////////////
    
  3. 更糟糕的是,三重符号??/\是一样的,这种替换发生在处理续行之前。

    /************************************//??/
    **************** Surprise! ************??/
    //////////////////////////////////////////
    

    在混淆比赛之外,实际上没有人会使用三重符号。但它们仍然在标准中。处理这两个问题的最简单方法是预先扫描字符串:

    return [c[2:]
            for c in p.findall(text.replace('//?','\\').replace('\\\n',''))
            if c]
    

如果你真的在意#include <...>的问题,唯一的解决办法就是再加一个模式,比如#define\s*<[^>\n]*>

7

第一步是找出那些///*不应该被当作注释开始的情况。例如,当它们在字符串内部(也就是在引号之间)时。为了避免处理引号之间的内容,我们可以用一个捕获组把它们包起来,并在替换模式中插入一个反向引用:

模式:

(
    "(?:[^"\\]|\\[\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 语法!

撰写回答