pyparsing nestedExpr和嵌套括号

时间:2017-05-25 00:46:19

标签: python nested pyparsing

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

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

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链接
  4. 我正在使用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",
          }
        ],
      ]
    ]
    

    非常感谢!

    修改

    保罗回答后,这就是代码的样子。显然它的效果更好: - )

    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'}]]]]
    

    再次感谢保罗指出我是否正确的方向!

    我唯一不知道的是将'true'变成True而将'[20,30]'变成[20, 30]

1 个答案:

答案 0 :(得分:2)

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

使用pyparsing的infixNotation方法可以更好地处理您尝试解析的查询语法。您可以在pyparsing wiki的Examples页面上看到几个示例 - 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,),
                ])

最后,我建议你定义一个类来将比较标记作为类init args,然后你可以将行为附加到该类来评估比较和输出调试字符串,如:

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?” 正在调用__repr__课程中的ComparisonExpr方法,而不是__str__。最简单的解决方案是添加到您的班级:

__repr__ = __str__

或者只需将__str__重命名为__repr__

“唯一不为人知的就是让我把'真''变成真,'[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表示法)访问已解析的值。