Pyparsing:将类似字典的结构解析为实际的字典

Pyparsing: Parse Dictionary-Like Structure into an actual Dictionary

我正在尝试将配置文件解析为 python 字典。 我无法更改文件的语法。

我正在使用 pyparsing。到目前为止,这是我的代码。

def file_parser():

    # Example data
    data = """
    root {
        level_one {
            key = value
            local {
                auth = psk
            }
            remote {
                auth = psk
            }
            children {
                net {
                    local_ts  = 1.1.0.0/16 
                    updown = /usr/local/test noticethespace
                    esp_proposals = yht123-h7583
                }
            }
            version = 2
            proposals = ydn162-jhf712-h7583
        }
    }

    usr {
        level_one {
            key = value
        }
    }
    """

    integer = Word(nums)
    ipAddress = Combine(integer + "." + integer + "." + integer + "." + integer)
    name = Word(alphas + "_-")
    any_word = Word(printables, excludeChars="{} ")
    EQ, LBRACE, RBRACE = map(Suppress, "={}")

    gram = Forward()

    entry = Group(name + ZeroOrMore(EQ) + gram)

    struct = Dict(LBRACE + OneOrMore(entry) + RBRACE)

    gram << (struct | ipAddress | name | any_word)

    result = Dict(OneOrMore(entry)).parseString(data)

    print(result)

当我 运行 此代码时,我收到以下错误:

pyparsing.ParseException: Expected {Dict:({{Suppress:("{") {Group:({W:(ABCD...) [Suppress:("=")]... : ...})}...} Suppress:("}")}) | Combine:({W:(0123...) "." W:(0123...) "." W:(0123...) "." W:(0123...)}) | W:(ABCD...) | W:(0123...)}, found 'c'  (at char 191), (line:11, col:13)

此代码的部分内容摘自 this 答案。我调整了此代码以适用于我的特定格式。

解析递归语法总是需要一些额外的思考。我总是鼓励解析器开发人员采取的一个步骤是在编写任何代码之前为您的语法编写一个 BNF。有时您可以根据自己的原始语法设计来执行此操作,有时您会尝试从示例文本中重构 BNF。无论哪种方式,编写 BNF 都会将您的大脑置于创意区域而不是编码的逻辑区域,并且您会在解析概念而不是代码中思考。

在你的例子中,你属于第二组,你正在根据示例文本重建 BNF。你看一下这个例子,发现有些部分有名字,而且它看起来像一个嵌套的 dict 是一个很好的目标。有名字的东西是什么?一些事物以 'name = a-value' 的排列方式命名,另一些事物以 'name structure-in-braces' 的方式命名。您创建的代码遵循类似 BNF 的内容:

name ::= (alpha | "_-")+
integer ::= digit+
ip_address ::= integer '.' integer '.' integer '.' integer
any_word ::= printable+
entry ::= name '='* gram
struct ::= '{' entry+ '}'
gram ::= struct | ip_address | name | any_word+

在您的代码中,您尝试创建一个 entry 表达式来处理这两种情况(使用 ZeroOrMore(EQ)),这就是当您过早跳转到代码时发生的那种优化.但是这些非常不同,应该在你的 BNF 中分开保存。

(您还未指定您的 IP 地址,在您的示例代码中有尾随 "/16"。)

还有问题的any_word,它不处理由多个词组成的值,如果用OneOrMore扩展可能会读太多词,吃掉下一个name.

那么让我们重新开始,并考虑您的命名元素。这是你有 name = something:

的行
auth = psk
auth = psk
local_ts  = 1.1.0.0/16 
updown = /usr/local/test noticethespace
esp_proposals = yht123-h7583
version = 2
proposals = ydn162-jhf712-h7583

如果我们想将表达式定义为 name_value = name + EQ + value,那么值将是 IP 地址、整数或该行中剩下的任何其他内容。如果您发现还有其他类型,则需要在此值表达式中包含其他类型,但请务必将“剩下的所有内容”放在最后。

对于嵌套情况,我们希望有 name_struct = name + struct,其中结构是 name_valuename_struct 的列表,用大括号括起来。这就是 name_struct.

需要说的全部内容

这是我根据此描述构建的 BNF:

name ::= alpha + ('_' | '-' | alpha)*
integer ::= digit+
ip_address ::= integer '.' integer '.' integer '.' integer ['/' integer]
value ::= ip_address | integer | rest_of_the_line
name_value ::= name '=' value
name_struct ::= name struct
struct ::= '{' (name_value | name_struct)* '}'

并且整个解析器是一个或多个 name_structs。

遵循此 BNF 并将其转换为 pyparsing 表达式,我将 file_parser() 转换为仅 return 生成的解析器 - 包括示例文本以及解析和打印它太多而无法包含在这一种方法。相反,代码如下:

data = """...sample text..."""
result = file_parser().parseString(data, parseAll=True)
result.pprint()

并打印出:

[['root',
  ['level_one',
   ['key', 'value'],
   ['local', ['auth', 'psk']],
   ['remote', ['auth', 'psk']],
   ['children',
    ['net',
     ['local_ts', '1.1.0.0/16'],
     ['updown', '/usr/local/test noticethespace'],
     ['esp_proposals', 'yht123-h7583']]],
   ['version', '2'],
   ['proposals', 'ydn162-jhf712-h7583']]],
 ['usr', ['level_one', ['key', 'value']]]]

我将根据这些建议将 file_parser 的实施留给您。在关于 SO 的其他问题中,我继续 post 实际的解析器,但我总是想知道我是否做了太多的勺子喂养,而不是将学习经验更多地留在 OP 的手中。所以我在这里停下来,并确保通过遵循上述 BNF 在 file_parser() 中实现解析器将 产生一个有效的解决方案。

一些提示:

  • 使用 pyparsing 的 restOfLine 读取所有内容直到行尾(代替 any_word)
  • name_valuename_struct 使用 Group,然后结构变为简单的 struct <<= Dict(LBRACE + (key_value | key_struct)[...] + RBRACE),其中我使用 [...] 作为新的表示法ZeroOrMore
  • 最终的整体解析器将是Dict(name_struct[...])