使用pyparsing解析数学表达式

7

我在尝试使用pyparsing解析数学表达式。我知道我可以直接复制pyparsing网站上的计算器示例,但我想理解它,以便以后可以添加功能。我来这里是因为我尝试理解这个示例,但我无法做到,所以我尽力了,最终写出了下面的代码:

symbol = (
    pp.Literal("^") |
    pp.Literal("*") |
    pp.Literal("/") |
    pp.Literal("+") |
    pp.Literal("-")
)
operation = pp.Forward()
atom = pp.Group(
    pp.Literal("(").suppress() + operation + pp.Literal(")").suppress()
) | number
operation << (pp.Group(number + symbol + number + pp.ZeroOrMore(symbol + atom)) | atom)
expression = pp.OneOrMore(operation)


print(expression.parseString("9-1+27+(3-5)+9"))

这将打印出:

[[9, '-', 1, '+', 27, '+', [[3, '-', 5]], '+', 9]]

它能够工作,但不是很好。我希望能按照的优先级和排序方式进行排序,但是经过多次尝试,我无法找到方法来实现。大致上就像这样:

[[[[9, '-', 1], '+', 27], '+', [3, '-', 5]], '+', 9]

我希望保持AST的样子,我想从中生成代码。
我确实看到了operatorPrecedence类?类似于Forward,但我不认为我理解它的工作原理。
编辑:
更深入地尝试了operatorPrecedence,我得到了这个结果:
expression = pp.operatorPrecedence(number, [
    (pp.Literal("^"), 1, pp.opAssoc.RIGHT),
    (pp.Literal("*"), 2, pp.opAssoc.LEFT),
    (pp.Literal("/"), 2, pp.opAssoc.LEFT),
    (pp.Literal("+"), 2, pp.opAssoc.LEFT),
    (pp.Literal("-"), 2, pp.opAssoc.LEFT)
])

我需要处理括号,因为当前程序不能处理括号(我不确定是否需要对结果进行后处理)。


1
operatorPrecedence 处理 () 内部,测试一下就知道了。 - PaulMcG
真的 ;) 我用一个复杂的表达式进行了测试,但是它太嵌套了,实际上看不出它是否有效。 - gcq
1个回答

14
这个解析问题的实际名称是“中缀表示法”(在pyparsing的最新版本中,我正在将operatorPrecedence重命名为infixNotation)。要查看中缀表示法解析的典型实现,请查看pyparsing wiki上的fourFn.py示例。在那里,您将看到一个实现此简化的BNF以执行四则运算的操作优先级的示例:
operand :: integer or real number
factor :: operand | '(' expr ')'
term :: factor ( ('*' | '/') factor )*
expr :: term ( ('+' | '-') term )*

所以一个表达式是由一个或多个项通过加减运算符分隔而成。
一个项是由一个或多个因子通过乘除运算符分隔而成。
一个因子可以是一个最低级操作数(在这种情况下,只是整数或实数),或者是用()括起来的表达式。
注意,这是一个递归解析器,因为因子在表达式的定义中间接使用,但表达式也用于定义因子。
在pyparsing中,大致看起来是这样的(假设整数和实数已经被定义):
LPAR,RPAR = map(Suppress, '()')
expr = Forward()
operand = real | integer
factor = operand | Group(LPAR + expr + RPAR)
term = factor + ZeroOrMore( oneOf('* /') + factor )
expr <<= term + ZeroOrMore( oneOf('+ -') + term )

现在使用expr,您可以解析这些任何一个:
3
3+2
3+2*4
(3+2)*4

infixNotation是pyparsing的辅助方法,它处理所有递归定义和分组,并允许您将其定义为:

expr = infixNotation(operand,
        [
        (oneOf('* /'), 2, opAssoc.LEFT),
        (oneOf('+ -'), 2, opAssoc.LEFT),
        ])

但这样会掩盖所有的基础理论,所以如果您想了解如何实现它,请查看fourFn.py中的原始解决方案。

[编辑-2022年12月18日] 对于那些寻找预定义解决方案的人,我已经将infixNotation封装成了一个名为plusminus的可安装pip包。 plusminus定义了一个BaseArithmeticParser类,用于创建一个准备运行的解析器和支持以下运算符的求值器:

  **   ÷   >=  ∈  in   ?:
  *    +   ==  ∉  not  |absolute-value|
  //   -   !=  ∩  and
  /    <   ≠   ∪  ∧
  mod  >   ≤   &  or
  ×    <=  ≥   |  ∨

还有这些函数:

  abs    ceil   max
  round  floor  str
  trunc  min    bool

“BaseArithmeticParser” 类允许您为特定领域的表达式定义其他运算符和函数,示例展示了如何为掷骰子、零售价格折扣等定义具有自定义函数和运算符的解析器。”

那么,我理解得对吗,infixNotation是一种通用的定义语法的形式,就像我的语法一样?BNF示例可以使用infixNotation实现吗? - gcq
infixNotation 有一个小问题,就是指数运算的操作顺序是从右到左而不是从左到右,仅仅使用 opAssoc.RIGHT 标记并不能完全处理好这个问题。即使是 Python 内部的算术字符串解析器也必须将指数运算视为特殊情况。此外,如果您发现操作优先级列表变得非常深,性能可能会开始受到影响 - 在这种情况下,请阅读有关 ParserElement.enablePackrat 的内容以加速处理速度。 - PaulMcG
1
此外,您错误地将 + 的优先级高于 -,将 * 的优先级高于 /,而它们应该是相等的。这就是为什么我在加法操作中使用 oneOf("+ -"),在乘法操作中使用 oneOf("* /")。这很重要,因为现在的情况是,即使是从左到右的顺序,它也总是先计算 +,而不是 -。看看 7 + 2 - 3 + 5 - 您现在的方法会优先考虑加法而不是减法,就像 (7+2) - (3+5),值为1,而实际上应该是直接从左到右计算,结果为11。 - PaulMcG
哦,所以oneOf测试的是值本身,而不是让infixNotation来做。有很多棘手的事情要注意 :) - gcq
1
oneOf('+ -') 只是 Literal("+") | Literal("-") 的简写。关键在于,在传递给 infixNotation 的操作优先级列表中,同时检查了 '+' 和 '-',而不是在两个不同的级别上进行检查。 - PaulMcG
感谢 Daewon Lee 提出的编辑建议,尽管最终被拒绝了,但它确实是合法的。此外,我之前关于指数运算的警告可以在解析后轻松解决。只需将 '**' 操作符标记为 opAssoc.LEFT 结合性,但在评估时从右到左评估操作数即可。不过,贪婪解析的评论仍然适用。 - PaulMcG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接