【问题标题】:Parsing logical expressions解析逻辑表达式
【发布时间】:2020-11-03 16:11:41
【问题描述】:

我有一个任务,我必须根据用户指定的逻辑表达式过滤 Pandas DataFrame。现在,我看到了一个我想使用的名为 PyParser 或 LARK 的模块,但我似乎不知道如何设置它们。

我有几个运算符,如CONTAINSEQUALFUZZY_MATCH 等。另外,我想将一些表达式组合成更复杂的表达式。

示例表达式:

ColumnA CONTAINS [1, 2, 3] AND (ColumnB FUZZY_MATCH 'bla' OR ColumnC EQUAL 45)

因此,我希望有一些结构化的 Dict 或 List 具有操作级别,以便按顺序执行它们。因此,此示例表达式的预期结果将类似于:

[['ColumnA', 'CONTAINS', '[1, 2, 3]'], 'AND', [['ColumnB', 'FUZZY_MATCH', 'bla'], OR, ['ColumnC', 'EQUAL', '45']]]

或以dict的形式:

{
  'EXPR1': {
    'col': 'ColumnA', 
    'oper': 'CONTAINS', 
    'value': '[1, 2, 3]']
  },
  'OPERATOR': 'AND', 
  'EXPR2': {
    'EXPR21': {
      'col': 'ColumnB', 
      'oper': 'FUZZY_MATCH', 
      'value': 'bla'
    }, 
    'OPERATOR': OR, 
    'EXPR22': {
      'col': 'ColumnC', 
      'oper': 'EQUAL', 
      'value': '45'
    }
  }
}

或者类似的东西。如果您有更好的方法来构建结果,我愿意接受建议。我对此很陌生,所以我相当肯定这可以改进。

【问题讨论】:

  • 我知道您已经得到了答案,但如果您对使用 Lark 的解决方案感兴趣,请告诉我。

标签: python string parsing boolean-logic boolean-expression


【解决方案1】:

有趣的问题:)

似乎是shunting yard 算法的一个相对简单的应用。
我编写了代码来解析 "((20 - 10 ) * (30 - 20) / 10 + 10 ) * 2" 之类的表达式,而不是 here

import re


def tokenize(str):
   return re.findall("[+/*()-]|\d+", expression)

def is_number(str):
    try:
        int(str)
        return True
    except ValueError:
        return False


def peek(stack):
    return stack[-1] if stack else None


def apply_operator(operators, values):
    operator = operators.pop()
    right = values.pop()
    left = values.pop()
    values.append(eval("{0}{1}{2}".format(left, operator, right)))


def greater_precedence(op1, op2):
    precedences = {"+": 0, "-": 0, "*": 1, "/": 1}
    return precedences[op1] > precedences[op2]


def evaluate(expression):
    tokens = tokenize(expression)
    values = []
    operators = []
    for token in tokens:
        if is_number(token):
            values.append(int(token))
        elif token == "(":
            operators.append(token)
        elif token == ")":
            top = peek(operators)
            while top is not None and top != "(":
                apply_operator(operators, values)
                top = peek(operators)
            operators.pop()  # Discard the '('
        else:
            # Operator
            top = peek(operators)
            while top is not None and top != "(" and greater_precedence(top, token):
                apply_operator(operators, values)
                top = peek(operators)
            operators.append(token)
    while peek(operators) is not None:
        apply_operator(operators, values)

    return values[0]


def main():
    expression = "((20 - 10 ) * (30 - 20) / 10 + 10 ) * 2"
    print(evaluate(expression))


if __name__ == "__main__":
    main()

我认为我们可以稍微修改代码以使其适用于您的情况:

  1. 我们需要修改在tokenize()中标记输入字符串的方式。
    基本上,给定字符串ColumnA CONTAINS [1, 2, 3] AND (ColumnB FUZZY_MATCH 'bla' OR ColumnC EQUAL 45),我们需要一个标记列表:
    ['ColumnA', 'CONTAINS', '[1, 2, 3]', 'AND', '(', 'ColumnB', 'FUZZY_MATCH', "'bla'", 'OR', 'ColumnC', 'EQUAL', '45', ')']
    这在很大程度上取决于输入字符串的复杂程度,并且需要一些字符串处理,但它相当简单,我将把它留给你。
  2. 修改is_number() 函数以检测ColumnA[1, 2, 3] 等内容。
    基本上,除了谓词CONTAINS/FUZZY_MATCH/EQUAL、运算符AND/OR 和括号(/) 之外的所有内容。
  3. 修改greater_precedence(op1, op2)以在op1['CONTAINS', 'EQUAL', ..]op2['AND', 'OR']之间的情况下返回true。
    这是因为我们希望始终在 AND/OR 之前评估 containsequals
  4. 修改apply_operator(operators, values)以实现如何评估布尔表达式ColumnA CONTAINS [1, 2, 3]或表达式true AND false的逻辑。
    记住CONTAINS/FUZZY_MATCH/EQUAL/AND/OR等都是这里的操作符。
    可能您需要在此处编写大量 if-else 案例,因为可能有很多不同的运算符。

【讨论】:

  • 好的,感谢您提供详细而完整的答案:) 似乎这就是我想要的。我会在接下来的几天内对其进行测试。
  • 当然。如果有效,请不要忘记接受答案并关闭问题;)