Python Pyparsing:识别可选列表的模式+关键字+可选列表

Python Pyparsing: Identify Pattern of Optional List + Keyword + Optional List

我正在尝试使用 Pyparser 解析 Csound 操作码行(以创建自定义自动格式化程序),但我无法尝试定义以下公式:

optional comma list + keyword + optional comma list

Csound 代码行的可能变化:

        prints  "int: %d%n", 45
a1      oscil   0.5, 440, -1
aL, aR  stereo  a1
        outs    aL, aR

及其语法:

                opcode  string, param
output          opcode  param, param, param
output, output  opcode  param
                opcode  param, param

到目前为止我已经想到了这个:

import pyparsing as pp

samples = [
    "prints \"int: %d%n\", 45",
    "a1 oscil 0.5, 440",
    "aL, aR stereo a1",
    "outs aL, aR"
]

var = pp.Word(pp.alphanums)

outputs = pp.Optional(pp.delimitedList(var))

opcode = pp.Word(pp.alphanums)

param = var | pp.dblQuotedString
params = pp.Optional(pp.delimitedList(param))

csound_line = outputs("outputs") \
    + opcode("opcode") \
    + params("params")

parsed = csound_line.parseString(samples[1])
print(parsed.dump())
parsed = csound_line.parseString(samples[2])
print(parsed.dump())
parsed = csound_line.parseString(samples[3])
print(parsed.dump())
parsed = csound_line.parseString(samples[0])
print(parsed.dump())

但它给了我这个:

['a1', 'oscil', '0']
- opcode: 'oscil'
- outputs: ['a1']
- params: ['0']
['aL', 'aR', 'stereo', 'a1']
- opcode: 'stereo'
- outputs: ['aL', 'aR']
- params: ['a1']
['outs', 'aL']
- opcode: 'aL'
- outputs: ['outs']
Traceback (most recent call last):
  File "/home/oliver/projects/personal/local/csoundformat/./test.py", line 33, in <module>
    parsed = csound_line.parseString(samples[0])
  File "/usr/lib/python3.9/site-packages/pyparsing.py", line 1955, in parseString
    raise exc
  File "/usr/lib/python3.9/site-packages/pyparsing.py", line 3250, in parseImpl
    raise ParseException(instring, loc, self.errmsg, self)
pyparsing.ParseException: Expected W:(ABCD...), found '"'  (at char 7), (line:1, col:8)

解析第 2 行工作正常,但第 1 行和第 3 行缺少结束参数,第 0 行导致对双引号的抱怨,尽管我使用 dblQuotedString.

我似乎无法确定 Optional/ZeroOrMoredelimitedList 的正确组合。如有任何帮助,我们将不胜感激。

谢谢!

回答核心问题前的几点说明:

  1. 我发现您以与执行 运行 不同的顺序列出输入样本非常令人困惑。我想我明白为什么你 运行 他们按那个顺序 - 虽然 try 块会解决问题 - 但它会更 reader - 只是重新排序 - 友好输入列表。随便说说,供以后参考。)

  2. oscil 行提前中断,因为您的 param 只识别字母数字和带引号的字符串。无符号整数由 alphanums 组成,但由于 . 不是字母数字,因此 0.5 等浮点数不匹配。 pp.Word(pp.alphanums)0.

    之后停止

    您 运行 遇到了与 -1 类似的问题,不同之处在于没有可以匹配的前一个数字。


但是,根本问题是语法是不明确的,除非您有办法区分输出变量和操作码。否则,a b 可以解析为 output(a) opcode(b) param()output() opcode(a) param(b).

Pyparsing 的 optional 是贪婪的,因此 pp.optional(outputs) 会将第一个 var 标记解析为单元素 outputs。这意味着它将 a b 解析为 outputs(a) opcode(b) params(),这解决了歧义但并不总是产生正确的解析。即使在它明显错误的情况下(对于可以看到整个命令的人类观察者),它也会这样做,例如命令 opop "foo"op 将被解析为输出而不是操作码这一事实意味着这两者都会产生语法错误。 (这是 prints 命令的问题。)

所以解析一行,需要区分以下几种情况(我用[...]表示可选):

a , ...    => a is an output, ... is (more) outputs followed by opcode [params]
a b , ...  => a is an opcode, b is a param, ... is more params
a b c ...  => a is an output, b is an opcode, ... is [, params]
a b        => ambiguous. Either output opcode or opcode param.
a          => a is an opcode (and nothing follows)

这只是一个近似值,因为参数可以是带引号的字符串、数字或(我想)表达式。我认为下面的语法可以正确处理这种情况,但您需要扩展 param 的定义才能尝试。

通过使用 opt_params 而不是 params 来组合第三和第四种情况可能会更有效;然后,您可以通过缺少参数来检测模棱两可的情况。但我这样保留是为了让歧义更清楚。

var = pp.Word(pp.alphanums)

outputs = pp.delimitedList(var)
opt_outputs = pp.Optional(outputs)

opcode = pp.Word(pp.alphanums)

# Note: Probably need to add expressions. I added a very simple
# floating point syntax, but it doesn't handle signs. (It has to go first
# in order to avoid 'var' matching the initial integer.)
param = pp.Combine(pp.Word(pp.nums) + '.' + pp.Word(pp.nums)) \
        | var \
        | pp.dblQuotedString

params = pp.delimitedList(param)
opt_params = pp.Optional(params)

comma = pp.Suppress(',')

csound_line = ( (var + comma + outputs)("outputs")
                + opcode("opcode")
                + opt_params("params")
              ) | (
                opcode("opcode") + (param + comma + params)("params")
              ) | (
                pp.And((var,))("outputs") + opcode("opcode") + params("params")
              ) | (
                (var + var)("ambiguous")
              ) | (
                opcode("opcode") + opt_params("params")
              )

(使用 pp.And((var,)) 是为了将单个输出标记放入列表中,以与其他 outputs 解析保持一致。可能有更好的方法来做到这一点。)

请注意,pyparsing 的 parseString 并不坚持解析到输入的末尾,这就是为什么您的一些测试用例静默失败的原因。我认为失败是明确的更好,所以我在调用中添加了 parseAll 。我还在它周围放了一个 try 块,使测试更容易编写:

samples = """
    prints "int: %d%n", 45
    a1 oscil 0.5, 440
    aL, aR stereo a1
    outs aL, aR
    output opcode param
    opcode param
    output opcode
    opcode
"""
for sample in samples.splitlines()[1:]:
    print(sample)
    try:
        print(csound_line.parseString(sample, parseAll=True).dump())
    except pp.ParseException as e:
        print("Parse failed:")
        print(e)
    print('-----------------------')

这是测试输出:

    prints "int: %d%n", 45
['prints', '"int: %d%n"', '45']
- opcode: 'prints'
- params: ['"int: %d%n"', '45']
-----------------------
    a1 oscil 0.5, 440
['a1', 'oscil', '0.5', '440']
- opcode: 'oscil'
- outputs: ['a1']
- params: ['0.5', '440']
-----------------------
    aL, aR stereo a1
['aL', 'aR', 'stereo', 'a1']
- opcode: 'stereo'
- outputs: ['aL', 'aR']
- params: ['a1']
-----------------------
    outs aL, aR
['outs', 'aL', 'aR']
- opcode: 'outs'
- params: ['aL', 'aR']
-----------------------
    output opcode param
['output', 'opcode', 'param']
- opcode: 'opcode'
- outputs: ['output']
- params: ['param']
-----------------------
    opcode param
['opcode', 'param']
- ambiguous: ['opcode', 'param']
-----------------------
    output opcode
['output', 'opcode']
- ambiguous: ['output', 'opcode']
-----------------------
    opcode
['opcode']
- opcode: 'opcode'
-----------------------