【问题标题】:pyparsing nestedExpr and nested parenthesespyparsing nestedExpr 和嵌套括号
【发布时间】:2017-05-25 00:46:19
【问题描述】:

我正在研究一种非常简单的“查询语法”,可供具有合理技术技能的人使用(即,本身不是编码人员,但能够触及该主题)

他们在表单上输入的典型示例是:

address like street
AND
vote =  True
AND
(
  (
    age>=25
    AND
    gender = M
  )
  OR
  (
    age between [20,30]
    AND
    gender = F
  )
  OR
  (
    age >= 70
    AND
    eyes != blue
  )
)

有

  1. 无需报价
  2. 可能无限嵌套的括号
  3. 简单的 AND|OR 链接

我正在使用 pyparsing(好吧,无论如何都在尝试)并取得了一些成果:

from pyparsing import *

OPERATORS = [
    '<',
    '<=',
    '>',
    '>=',
    '=',
    '!=',
    'like'
    'regexp',
    'between'
]

unicode_printables = u''.join(unichr(c) for c in xrange(65536)
                              if not unichr(c).isspace())

# user_input is the text sent by the client form
user_input = ' '.join(user_input.split())
user_input = '(' + user_input + ')'

AND = Keyword("AND").setName('AND')
OR = Keyword("OR").setName('OR')

FIELD = Word(alphanums).setName('FIELD')
OPERATOR = oneOf(OPERATORS).setName('OPERATOR')
VALUE = Word(unicode_printables).setName('VALUE')
CRITERION = FIELD + OPERATOR + VALUE

QUERY = Forward()
NESTED_PARENTHESES = nestedExpr('(', ')')
QUERY << ( CRITERION | AND | OR | NESTED_PARENTHESES )

RESULT = QUERY.parseString(user_input)
RESULT.pprint()

输出是:

[['address',
  'like',
  'street',
  'AND',
  'vote',
  '=',
  'True',
  'AND',
  [['age>=25', 'AND', 'gender', '=', 'M'],
   'OR',
   ['age', 'between', '[20,30]', 'AND', 'gender', '=', 'F'],
   'OR',
   ['age', '>=', '70', 'AND', 'eyes', '!=', 'blue']]]]

我只是部分满意 - 主要原因是所需的最终输出看起来像这样:

[
  {
    "field" : "address",
    "operator" : "like",
    "value" : "street",
  },
  'AND',
  {
    "field" : "vote",
    "operator" : "=",
    "value" : True,
  },
  'AND',
  [
    [
      {
        "field" : "age",
        "operator" : ">=",
        "value" : 25,
      },
      'AND'
      {
        "field" : "gender",
        "operator" : "=",
        "value" : "M",
      }
    ],
    'OR',
    [
      {
        "field" : "age",
        "operator" : "between",
        "value" : [20,30],
      },
      'AND'
      {
        "field" : "gender",
        "operator" : "=",
        "value" : "F",
      }
    ],
    'OR',
    [
      {
        "field" : "age",
        "operator" : ">=",
        "value" : 70,
      },
      'AND'
      {
        "field" : "eyes",
        "operator" : "!=",
        "value" : "blue",
      }
    ],
  ]
]

非常感谢!

编辑

在 Paul 的回答之后,这就是代码的样子。显然它工作得更好:-)

unicode_printables = u''.join(unichr(c) for c in xrange(65536)
                              if not unichr(c).isspace())

user_input = ' '.join(user_input.split())

AND = oneOf(['AND', '&'])
OR = oneOf(['OR', '|'])
FIELD = Word(alphanums)
OPERATOR = oneOf(OPERATORS)
VALUE = Word(unicode_printables)
COMPARISON = FIELD + OPERATOR + VALUE

QUERY = infixNotation(
    COMPARISON,
    [
        (AND, 2, opAssoc.LEFT,),
        (OR, 2, opAssoc.LEFT,),
    ]
)

class ComparisonExpr:
    def __init__(self, tokens):
        self.tokens = tokens

    def __str__(self):
        return "Comparison:('field': {!r}, 'operator': {!r}, 'value': {!r})".format(*self.tokens.asList())

COMPARISON.addParseAction(ComparisonExpr)

RESULT = QUERY.parseString(user_input).asList()
print type(RESULT)
from pprint import pprint
pprint(RESULT)

输出是:

[
  [
    <[snip]ComparisonExpr instance at 0x043D0918>,
    'AND',
    <[snip]ComparisonExpr instance at 0x043D0F08>,
    'AND',
    [
      [
        <[snip]ComparisonExpr instance at 0x043D3878>,
        'AND',
        <[snip]ComparisonExpr instance at 0x043D3170>
      ],
      'OR',
      [
        [
          <[snip]ComparisonExpr instance at 0x043D3030>,
          'AND',
          <[snip]ComparisonExpr instance at 0x043D3620>
        ],
        'AND',
        [
          <[snip]ComparisonExpr instance at 0x043D3210>,
          'AND',
          <[snip]ComparisonExpr instance at 0x043D34E0>
        ]
      ]
    ]
  ]
]

有没有办法用字典而不是 ComparisonExpr 实例返回 RESULT?

EDIT2

想出了一个幼稚且非常具体的解决方案,但到目前为止对我有用:

[snip]
class ComparisonExpr:
    def __init__(self, tokens):
        self.tokens = tokens

    def __str__(self):
        return "Comparison:('field': {!r}, 'operator': {!r}, 'value': {!r})".format(*self.tokens.asList())

    def asDict(self):
        return {
            "field": self.tokens.asList()[0],
            "operator": self.tokens.asList()[1],
            "value": self.tokens.asList()[2]
        }

[snip]
RESULT = QUERY.parseString(user_input).asList()[0]
def convert(list):
    final = []
    for item in list:
        if item.__class__.__name__ == 'ComparisonExpr':
            final.append(item.asDict())
        elif item in ['AND', 'OR']:
            final.append(item)
        elif item.__class__.__name__ == 'list':
            final.append(convert(item))
        else:
            print 'ooops forgotten something maybe?'

    return final

FINAL = convert(RESULT)
pprint(FINAL)

哪些输出:

[{'field': 'address', 'operator': 'LIKE', 'value': 'street'},
   'AND',
   {'field': 'vote', 'operator': '=', 'value': 'true'},
   'AND',
   [[{'field': 'age', 'operator': '>=', 'value': '25'},
     'AND',
     {'field': 'gender', 'operator': '=', 'value': 'M'}],
    'OR',
    [[{'field': 'age', 'operator': 'BETWEEN', 'value': '[20,30]'},
      'AND',
      {'field': 'gender', 'operator': '=', 'value': 'F'}],
     'AND',
     [{'field': 'age', 'operator': '>=', 'value': '70'},
      'AND',
      {'field': 'eyes', 'operator': '!=', 'value': 'blue'}]]]]

再次感谢 Paul 为我指明了正确的方向!

唯一未知的事情就是让我把'true'变成True和'[20,30]'变成[20, 30]。

【问题讨论】:

    标签: python nested pyparsing


    【解决方案1】:

    nestedExpr 是 pyparsing 中的一个方便表达式,可以轻松定义具有匹配的开始和结束字符的文本。当您要解析嵌套内容时,nestedExpr 通常结构不够好。

    您尝试解析的查询语法最好使用 pyparsing 的 infixNotation 方法。您可以在 pyparsing wiki 的示例页面上看到几个示例 - SimpleBool 与您正在解析的内容非常相似。

    “中缀表示法”是运算符位于其相关操作数之间的表达式的一般解析术语(相对于“后缀表示法”,其中运算符位于操作数之后,如“2 3 +”而不是“2 + 3” "; 或看起来像 "+ 2 3" 的 "前缀表示法")。运算符在求值中可以有一个优先顺序,可以覆盖从左到右的顺序 - 例如,在“2 + 3 * 4”中,操作的优先级表明在加法之前先计算乘法。中缀表示法还支持使用括号或其他分组字符来覆盖该优先级,如 "(2 + 3) * 4" 强制先执行加法运算。

    pyparsing 的infixNotation 方法采用基本操作数表达式,然后是运算符定义元组列表,按优先顺序排列。例如,4 函数整数运算如下所示:

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

    这意味着我们将解析整数操作数,按顺序使用“*”和“/”二进制左关联操作以及“+”和“-”二进制操作。 infixNotation 内置了对括号覆盖顺序的支持。

    查询字符串通常是布尔运算 NOT、AND 和 OR 的某种组合,并且通常按照优先顺序进行计算。在您的情况下,这些运算符的操作数是比较表达式,例如“address = street”或“age between [20,30]”。因此,如果您为比较表达式定义一个表达式,格式为 fieldname operator value,那么您可以使用 infixNotation 对 AND 和 OR 进行正确的分组:

    import pyparsing as pp
    query_expr = pp.infixNotation(comparison_expr,
                    [
                        (NOT, 1, pp.opAssoc.RIGHT,),
                        (AND, 2, pp.opAssoc.LEFT,),
                        (OR, 2, pp.opAssoc.LEFT,),
                    ])
    

    最后,我建议您定义一个类以将比较标记作为类初始化参数,然后您可以将行为附加到该类以评估比较并输出调试字符串,例如:

    class ComparisonExpr:
        def __init__(self, tokens):
            self.tokens = tokens
    
        def __str__(self):
            return "Comparison:('field': {!r}, 'operator': {!r}, 'value': {!r})".format(
                                *self.tokens.asList())
    
    # attach the class to the comparison expression
    comparison_expr.addParseAction(ComparisonExpr)
    

    然后你可以得到如下输出:

    query_expr.parseString(sample).pprint()
    
    [[Comparison:({'field': 'address', 'operator': 'like', 'value': 'street'}),
      'AND',
      Comparison:({'field': 'vote', 'operator': '=', 'value': True}),
      'AND',
      [[Comparison:({'field': 'age', 'operator': '>=', 'value': 25}),
        'AND',
        Comparison:({'field': 'gender', 'operator': '=', 'value': 'M'})],
       'OR',
       [Comparison:({'field': 'age', 'operator': 'between', 'value': [20, 30]}),
        'AND',
        Comparison:({'field': 'gender', 'operator': '=', 'value': 'F'})],
       'OR',
       [Comparison:({'field': 'age', 'operator': '>=', 'value': 70}),
        'AND',
        Comparison:({'field': 'eyes', 'operator': '!=', 'value': 'blue'})]]]]
    

    SimpleBool.py 示例包含更多详细信息,向您展示如何创建此类以及 NOT、AND 和 OR 运算符的相关类。

    编辑:

    “有没有办法用字典而不是 ComparisonExpr 实例返回 RESULT?” 正在调用 ComparisonExpr 类上的 __repr__ 方法而不是 __str__。最简单的解决方案是添加到您的课程中:

    __repr__ = __str__
    

    或者只是将__str__ 重命名为__repr__。

    “唯一未知的事情就是让我把'true'变成True,把'[20,30]'变成[20, 30]”

    试试:

    CK = CaselessKeyword  # 'cause I'm lazy
    bool_literal = (CK('true') | CK('false')).setParseAction(lambda t: t[0] == 'true')
    LBRACK,RBRACK = map(Suppress, "[]")
    # parse numbers using pyparsing_common.number, which includes the str->int conversion parse action
    num_list = Group(LBRACK + delimitedList(pyparsing_common.number) + RBRACK)
    

    然后将这些添加到您的 VALUE 表达式中:

    VALUE = bool_literal | num_list | Word(unicode_printables)
    

    最后:

    from pprint import pprint
    pprint(RESULT)
    

    我所以厌倦了一直导入pprint 来做这件事,我只是将它添加到ParseResults 的API 中。试试:

    RESULT.pprint()  # no import required on your part
    

    或

    print(RESULT.dump()) # will also show indented list of named fields
    

    EDIT2

    最后,结果名称很好学。如果您对 COMPARISON 进行此更改,则一切照旧:

    COMPARISON = FIELD('field') + OPERATOR('operator') + VALUE('value')
    

    但是现在你可以写了:

    def asDict(self):
        return self.tokens.asDict()
    

    您可以通过名称而不是索引位置访问解析的值(使用result['field'] 表示法或result.field 表示法)。

    【讨论】:

    • off-opic 但无论如何必须说出来:当我开始查看基于 python 的解析库时,我花时间在 SO 和其他地方检查我选择的内容是否受欢迎社区。 pyparsing 不仅如此,它还从作者那里得到了惊人的答案和支持。真的支持这个,保罗!然后回到主题:谢谢,我将修改我的代码,并相应地修改问题!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-07-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-06-15
    • 1970-01-01
    相关资源
    最近更新 更多