解析带注释的配置文件的正则表达式

3 投票
6 回答
3720 浏览
提问于 2025-04-16 04:32

编辑:我只是好奇怎么让这个正则表达式工作。请不要告诉我有更简单的方法,这显而易见!:P

我正在用Python写一个正则表达式,目的是解析配置文件中的每一行。每一行可能看起来像这样:

someoption1 = some value # some comment
# this line is only a comment
someoption2 = some value with an escaped \# hash
someoption3 = some value with a \# hash # some comment

我的想法是,井号(#)后面的内容都被视为注释,除非这个井号前面有一个反斜杠(\)进行转义。

我想用正则表达式把每一行拆分成几个部分:前面的空白字符、赋值的左边、赋值的右边和注释。以示例中的第一行为例,拆分结果应该是:

  • 空白字符:""
  • 赋值左边:"someoption1 ="
  • 赋值右边:" some value "
  • 注释:"# some comment"

这是我目前写的正则表达式:

^(\s)?(\S+\s?=)?(([^\#]*(\\\#)*)*)?(\#.*)?$

我对正则表达式很糟糕,所以请随意批评!

使用Python的re.findAll(),返回的结果是:

  • 第0个索引:空白字符,没问题
  • 第1个索引:赋值的左边
  • 第2个索引:赋值的右边,直到第一个井号,不管有没有转义(这是不对的)
  • 第5个索引:第一个井号,不管有没有转义,以及它后面的所有内容(这是不对的)

可能我对正则表达式的一些基本概念还不太明白。如果有人能解决这个问题,我将永远感激不尽……

6 个回答

2

到目前为止,提出的5个解决方案中,只有Gumbo的方案真正有效。这里是我的解决方案,它也能正常工作,并且有很多注释:

import re

def fn(line):
    match = re.search(
        r"""^          # Anchor to start of line
        (\s*)          # $1: Zero or more leading ws chars
        (?:            # Begin group for optional var=value.
          (\S+)        # $2: Variable name. One or more non-spaces.
          (\s*=\s*)    # $3: Assignment operator, optional ws
          (            # $4: Everything up to comment or EOL.
            [^#\\]*    # Unrolling the loop 1st normal*.
            (?:        # Begin (special normal*)* construct.
              \\.      # special is backslash-anything.
              [^#\\]*  # More normal*.
            )*         # End (special normal*)* construct.
          )            # End $4: Value.
        )?             # End group for optional var=value.
        ((?:\#.*)?)    # $5: Optional comment.
        $              # Anchor to end of line""", 
        line, re.MULTILINE | re.VERBOSE)
    return match.groups()

print (fn(r" # just a comment"))
print (fn(r" option1 = value"))
print (fn(r" option2 = value # no escape == IS a comment"))
print (fn(r" option3 = value \# 1 escape == NOT a comment"))
print (fn(r" option4 = value \\# 2 escapes == IS a comment"))
print (fn(r" option5 = value \\\# 3 escapes == NOT a comment"))
print (fn(r" option6 = value \\\\# 4 escapes == IS a comment"))

上面的脚本产生了以下(正确的)输出:(在Python 3.0.1中测试过)

(' ', None, None, None, '# just a comment')
(' ', 'option1', ' = ', 'value', '')
(' ', 'option2', ' = ', 'value ', '# no escape == IS a comment')
(' ', 'option3', ' = ', 'value \\# 1 escape == NOT a comment', '')
(' ', 'option4', ' = ', 'value \\\\', '# 2 escapes == IS a comment')
(' ', 'option5', ' = ', 'value \\\\\\# 3 escapes == NOT a comment', '')
(' ', 'option6', ' = ', 'value \\\\\\\\', '# 4 escapes == IS a comment')

请注意,这个解决方案使用了Jeffrey Friedl的“循环展开效率技术(可以消除慢速交替)”。它完全不使用回顾查找,并且速度非常快。《正则表达式精髓(第三版)》是任何声称“懂得”正则表达式的人都必须阅读的书。(当我说“懂得”时,我是指那种Neo的“我会功夫!”的感觉 :)

2

我会在多行模式下使用这个正则表达式:

^\s*([a-zA-Z_][a-zA-Z_0-9]*)\s*=\s*((?:[^\\#]|\\.)+)

这样可以让任何字符都可以被转义(比如用\\.)。如果你只想允许#这个字符,可以用\\#来代替。

2

你的正则表达式没有按你想的那样匹配,是因为正则表达式的贪婪匹配特性:每个部分会尽量匹配最长的子串,以便剩下的字符串还能用正则表达式的其他部分匹配。

以你某一行中带有转义字符#为例,具体情况是:

  • [^\#]*(顺便说一下,其实不需要转义#)会匹配第一个#之前的所有内容,包括它前面的反斜杠
  • (\\\#)*不会匹配任何东西,因为此时字符串是以#开头的
  • 最后的(\#.*)会匹配字符串的其余部分

这里有个简单的例子来强调这种可能让人困惑的行为:在正则表达式(a*)(ab)?(b*)中,(ab)?永远不会匹配到任何东西。

我认为这个正则表达式(基于原来的那个)应该可以工作:^\s*(\S+\s*=([^\\#]|\\#?)*)?(#.*)?$

撰写回答