使用 pyparsing 解析可变数量的可选参数

Parse variable number of optional parameters with pyparsing

我正在使用 pyparsing 为我们公司内部使用的消息协议构建 DSL。我一直未能找到解决方案的一个特定问题是一种为具有可选参数的输入字符串生成一致结果的方法。以下是我构造的解析规则:

from pyparsing import *

label       = SkipTo(';' ^ LineEnd())
delim       = Char(';').suppress()
value       = (Word(nums) ^ Combine('0x' + Word(hexnums)))
descr       = QuotedString(quoteChar="'''", multiline=True) ^ SkipTo(LineEnd())

field       = Keyword('field') + label + delim + Keyword('u8') \
                + Optional(delim + Optional(value, default=None) + Optional(delim + descr, default = None), default = None)

print(field.parseString('field Field #1; u8'))
print(field.parseString('field Field #2; u8; 1'))
print(field.parseString('field Field #3; u8; 1; This is a description of the field'))
print(field.parseString('field Field #3; u8; ; This is a description of the field'))

那段代码的输出是:

['field', ' Field #1', 'u8', None]
['field', ' Field #2', 'u8', '1', None]
['field', ' Field #3', 'u8', '1', 'This is a description of the field']
['field', ' Field #3', 'u8', None, 'This is a description of the field']

我的首选输出是:

['field', 'Field #1', 'u8', None, None]
['field', 'Field #2', 'u8', '1', None]
['field', 'Field #3', 'u8', '1', 'This is a description of the field']
['field', 'Field #3', 'u8', None, 'This is a description of the field']

另一个让我恼火的是字段名称以 space 开头,我想去掉它。

我应该如何构建解析规则以使实际输出与首选匹配?

下面的代码应该适合您。它需要将最后的 Optional 分成两个单独的部分以获得字段 1 所需的 None, None 行为,然后在第一个中嵌套另一个 Optional 以正确处理 ; ;在决赛中。

field = Keyword('field') + White(' ').suppress() + label \
    + delim + Keyword('u8') \
    + Optional(delim + Optional(value, default=None), default=None) \
    + Optional(delim + descr, default = None)

看来您的开端不错。我总是建议人们写出来 他们的解析器采用非代码格式,例如 Backus-Naur 形式。它不一定是 超级严谨,只是一些符号来帮助你考虑你计划解析的格式 在你真正开始思考编码思想之前。

根据您的示例,我想到了这个(其中“|”表示交替,“[]”表示可选):

"""
BNF:
field ::= 'field' label ';' 'u8' [';' [value] [';' [description] ] ] 
label ::= all non-';' characters
value ::= integer | '0x' hex_integer
description ::= multiline string | rest of line
"""

我使用了您当前的 field 定义,并进行了一些小的调整。一个是添加 Empty 在标签之前,这样前导空格就不会包含在解析值中 对于标签(这是一种 hack,jdaz 的回答中的 White().suppress() 更明确)。

然后我为表达式的每个部分添加了结果名称。我强烈 推荐使用结果名称,它们使您的 post-解析工作更容易 通过名称而不是位置索引来查找和处理单个已解析的元素。 结果名称也使您的解析器在将来更易于维护,如果您添加新的 解析器中可能会改变解析结果位置的元素。

field       = (Keyword('field') 
               + Empty() # skips whitespace
               + label("label")
               + delim
               + Keyword('u8')("type")
               + Optional(delim 
                            + Optional(value("value")) 
                            + Optional(delim 
                                       + descr("descr"))
                          )
               )

由于有多个可选字段和可选字段,这超出了 Optional 的默认值可以做什么。所以缺少的字段可以使用 parse 动作(在特定表达式之后的解析过程中调用的回调函数 已成功解析)。我们定义的结果名称也使它更容易 确定哪些字段已经提供或没有,本来应该提供的东西 使用普通的未命名结果有点困难。

def fill_in_defaults(t):
    if "descr" not in t:
        t["descr"] = None
        t.append(None)
    if "value" not in t:
        t["value"] = None
        t.insert(-1, None)
    else:
        # convert value to int or float
        if t.value.startswith("0x"):
            t["value"] = int(t.value[2:], 16)
        else:
            try:
                t["value"] = int(t.value)
            except ValueError:
                t["value"] = float(t.value)

field.addParseAction(fill_in_defaults)

可以使用字典式 [key] 符号或对象式 .key 属性访问命名结果 符号。但是要手动分配新的结果名称,您必须使用 dict 样式的形式。

最后,我使用 runTests 重新做了你的测试字符串,这使得创建它变得容易多了 一个解析表达式的多个测试用例:

field.runTests("""\
    field Field #1; u8
    field Field #2; u8; 1
    field Field #3; u8; 1; This is a description of the field
    field Field #4; u8; ; This is a description of the field
    field Field #5; u8; 0x1b
    """)

这给出了每个测试的输出:

  • 回显输入字符串
  • 转储从 results.dump() 打印的解析结果
    • 将解析后的值列为列表
    • 列出命名结果的层次结构
  • 或者如果出现解析错误,则在解析失败的位置显示“^” 和错误消息