pyparsing nestedExpr 和嵌套括号
pyparsing nestedExpr and nested parentheses
我正在开发一个非常简单的 "querying syntax" 可供具有合理技术技能的人使用(即,本身不是编码员,但能够触及主题)
他们将在表格中输入的内容的典型示例是:
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
)
)
有
- 无需报价
- 可能无限嵌套括号
- 简单的 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>
]
]
]
]
]
有没有办法 return 使用字典而不是 ComparisonExpr
个实例获得结果?
编辑2
提出了一个幼稚且非常具体的解决方案,但到目前为止对我有效:
[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]
。
nestedExpr
是 pyparsing 中的一个便捷表达式,可以轻松定义具有匹配的开始和结束字符的文本。当你想解析嵌套的内容时,nestedExpr
通常结构不够好。
您尝试解析的查询语法最好使用 pyparsing 的 infixNotation
方法。您可以在 pyparsing wiki 的示例页面上看到几个示例 - SimpleBool 与您正在解析的内容非常相似。
"Infix notation" 是表达式的一般解析术语,其中运算符位于其相关操作数之间(相对于 "postfix notation",其中运算符在操作数之后,如“2 3 +”而不是“2 + 3”;或 "prefix notation",看起来像“+ 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 以将比较标记作为 class 初始化参数,然后您可以将行为附加到该 class 以评估比较和输出调试字符串,例如:
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 示例包含更多详细信息,可向您展示如何创建此 class 以及用于 NOT、AND 和 OR 运算符的相关 classes。
编辑:
"Is there a way to return RESULT with dictionaries and not ComparisonExpr instances?"
正在调用 ComparisonExpr
class 上的 __repr__
方法,而不是 __str__
。最简单的解决方案是添加到您的 class:
__repr__ = __str__
或者将 __str__
重命名为 __repr__
。
"The only thing unknown left is for me to turn 'true' into True and '[20,30]' into [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 for ParseResults
.尝试:
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
表示法)。
我正在开发一个非常简单的 "querying syntax" 可供具有合理技术技能的人使用(即,本身不是编码员,但能够触及主题)
他们将在表格中输入的内容的典型示例是:
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
)
)
有
- 无需报价
- 可能无限嵌套括号
- 简单的 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>
]
]
]
]
]
有没有办法 return 使用字典而不是 ComparisonExpr
个实例获得结果?
编辑2
提出了一个幼稚且非常具体的解决方案,但到目前为止对我有效:
[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]
。
nestedExpr
是 pyparsing 中的一个便捷表达式,可以轻松定义具有匹配的开始和结束字符的文本。当你想解析嵌套的内容时,nestedExpr
通常结构不够好。
您尝试解析的查询语法最好使用 pyparsing 的 infixNotation
方法。您可以在 pyparsing wiki 的示例页面上看到几个示例 - SimpleBool 与您正在解析的内容非常相似。
"Infix notation" 是表达式的一般解析术语,其中运算符位于其相关操作数之间(相对于 "postfix notation",其中运算符在操作数之后,如“2 3 +”而不是“2 + 3”;或 "prefix notation",看起来像“+ 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 以将比较标记作为 class 初始化参数,然后您可以将行为附加到该 class 以评估比较和输出调试字符串,例如:
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 示例包含更多详细信息,可向您展示如何创建此 class 以及用于 NOT、AND 和 OR 运算符的相关 classes。
编辑:
"Is there a way to return RESULT with dictionaries and not ComparisonExpr instances?"
正在调用 ComparisonExpr
class 上的 __repr__
方法,而不是 __str__
。最简单的解决方案是添加到您的 class:
__repr__ = __str__
或者将 __str__
重命名为 __repr__
。
"The only thing unknown left is for me to turn 'true' into True and '[20,30]' into [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 for ParseResults
.尝试:
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
表示法)。