2017-05-25 321 views
3

我对工作很简单的“查询语法”可通过合理的技术技能的人(即未编码器本身,而是能够触及的主题)pyparsing nestedExpr和嵌套的括号

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

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. 简单和婷|或链接

我使用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实例?

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]

回答

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”或“年龄在[20,30]之间”。所以,如果你定义表单fieldname operator value的一个比较表达式的表达式,那么你可以使用infixNotation做的,公司和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,), 
       ]) 

最后,我建议你定义一个类承担的比较令牌作为类初始化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情况下返回结果?” 您的ComparisonExpr类中的__repr__方法被调用而不是__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所有的时间来做到这这,我只是把它添加到API的ParseResults。尝试:

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

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

EDIT2

最后,结果名字都好学习。如果你把这个变化对比,一切都还在工作,你就会明白:

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

但现在你可以这样写:

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

,您可以通过名称,而不是索引位置访问解析值(或者使用result['field']符号或result.field符号)。

+0

关闭OPIC但无论如何都要说:当我开始在看看基于Python的解析库,我花了时间上的SO和在其他地方检查什么,我会选择是否是普遍的社会。 pyparsing不仅如此,还得到惊人的答案:从它的作者的支持。 真的道具为此,保罗! 然后回到主题:谢谢你,我会修改我的代码,并相应的问题! – Hal