我目前正在尝试编写一个能够解析非常简单的key = value
查询的小型解析器。但它应该足够聪明,可以处理AND
和OR
组,AND
具有更高的优先级。示例文本输入:
a = 10 && b = 20
a = 10 || b = 20
a = 10 && b = 20 || c = 30
前两个是微不足道的。最后一个应该将前两个谓词分组为“AND”组,然后该组应该分组在“OR”组中。
我的基础知识已关闭,但卡在正确的分组上。我正在使用ply,它使用flex / bison / lex / yacc语法来定义语法。如果我使用现有语法完全走错了轨道,请告诉我......这将是一个有关解析器的宝贵学习经验。
我已尝试设置优先级,但我认为这不是由减少/减少冲突造成的。我认为这更像是我一般定义语法的方式问题,但我无法弄清楚我需要改变什么。
以下是我当前的实现和单元测试文件。测试文件应该有助于理解预期的输出。目前有一个失败的测试。那是让我头痛的原因。
可以使用内置unittest
模块运行 测试,但是,当我在测试中执行一些print
语句时,我建议使用pytest
作为它可以拦截那些并减少混乱。例如(假设两个文件都在同一个文件夹中):
python -m venv env
./env/bin/pip install pytest
./env/bin/pytest test_query_string.py
queryparser.py
import logging
from collections import namedtuple
import ply.lex as lex
import ply.yacc as yacc
LOG = logging.getLogger(__name__)
Predicate = namedtuple('Predicate', 'key operator value')
class Production:
def __repr__(self):
preds = [repr(pred) for pred in self._predicates]
return '%s(%s)' % (self.__class__.__name__, ', '.join(preds))
def __eq__(self, other):
return (
self.__class__ == other.__class__ and
self._predicates == other._predicates)
def debug(self, indent=0, aslist=False):
lines = []
lines.append(' ' * indent + self.__class__.__name__)
for predicate in self._predicates:
if hasattr(predicate, 'debug'):
lines.extend(predicate.debug(indent + 1, aslist=True))
else:
lines.append(' ' * (indent+1) + repr(predicate))
if aslist:
return lines
else:
return '\n'.join(lines)
class Conjunction(Production):
def __init__(self, *predicates):
self._predicates = predicates
class Disjunction(Production):
def __init__(self, *predicates):
self._predicates = predicates
def parse(query: str, debug=False) -> Predicate:
lexer = QueryLexer().build()
parser = QueryParser().build()
if debug:
output = parser.parse(query, debug=LOG)
else:
output = parser.parse(query)
return output or []
class QueryLexer:
tokens = (
'WORD',
'OPERATOR',
'QUOTE',
'AND',
'OR'
)
t_ignore = ' \t'
t_QUOTE = '"'
def t_error(self, t):
LOG.warning('Illegal character %r', t.value[0])
t.lexer.skip(1)
def t_WORD(self, t):
r'\w+'
return t
def t_OPERATOR(self, t):
r'(=|!=|>|<|<=|>=)'
return t
def t_AND(self, t):
r'&&'
return t
def t_OR(self, t):
r'\|\|'
return t
def build(self, **kwargs):
self.lexer = lex.lex(module=self, **kwargs)
class QueryParser:
precedence = (
('nonassoc', 'OR'),
('nonassoc', 'AND'),
)
def p_query(self, p):
'''
query : disjunction
| conjunction
| predicate
'''
p[0] = p[1]
def p_disjunction(self, p):
'''
disjunction : predicate OR predicate
| predicate OR conjunction
| predicate OR disjunction
'''
output = [p[1]]
if p.slice[3].type == 'disjunction':
# We can merge multiple chanined disjunctions together
output.extend(p[3]._predicates)
else:
output.append(p[3])
p[0] = Disjunction(*output)
def p_conjunction(self, p):
'''
conjunction : predicate AND predicate
| predicate AND conjunction
| predicate AND disjunction
'''
if len(p) == 4:
output = [p[1]]
if p.slice[3].type == 'conjunction':
# We can merge multiple chanined disjunctions together
output.extend(p[3]._predicates)
else:
output.append(p[3])
p[0] = Conjunction(*output)
else:
p[0] = Conjunction(p[1])
def p_predicate(self, p):
'''
predicate : maybequoted OPERATOR maybequoted
'''
p[0] = Predicate(p[1], p[2], p[3])
def p_maybequoted(self, p):
'''
maybequoted : WORD
| QUOTE WORD QUOTE
'''
if len(p) == 4:
p[0] = p[2]
else:
p[0] = p[1]
def p_error(self, p):
"""
Panic-mode rule for parser errors.
"""
if not p:
LOG.debug('Syntax error at EOF')
else:
self.parser.errok()
LOG.error('Syntax Error at %r', p)
def build(self):
self.tokens = QueryLexer.tokens
self.parser = yacc.yacc(module=self, outputdir='/tmp', debug=True)
return self.parser
test_query_string.py
import unittest
from queryparser import parse, Conjunction, Disjunction, Predicate
class TestQueryString(unittest.TestCase):
def test_single_equals(self):
result = parse('hostname = foo')
self.assertEqual(result, Predicate('hostname', '=', 'foo'))
def test_single_equals_quoted(self):
result = parse('hostname = "foo"')
self.assertEqual(result, Predicate('hostname', '=', 'foo'))
def test_anded_equals(self):
result = parse('hostname = foo && role=cpe')
self.assertEqual(result, Conjunction(
Predicate('hostname', '=', 'foo'),
Predicate('role', '=', 'cpe'),
))
def test_ored_equals(self):
result = parse('hostname = foo || role=cpe')
self.assertEqual(result, Disjunction(
Predicate('hostname', '=', 'foo'),
Predicate('role', '=', 'cpe'),
))
def test_chained_conjunction(self):
result = parse('hostname = foo && role=cpe && bla=blub')
print(result.debug()) # XXX debug statement
self.assertEqual(result, Conjunction(
Predicate('hostname', '=', 'foo'),
Predicate('role', '=', 'cpe'),
Predicate('bla', '=', 'blub'),
))
def test_chained_disjunction(self):
result = parse('hostname = foo || role=cpe || bla=blub')
print(result.debug()) # XXX debug statement
self.assertEqual(result, Disjunction(
Predicate('hostname', '=', 'foo'),
Predicate('role', '=', 'cpe'),
Predicate('bla', '=', 'blub'),
))
def test_mixed_predicates(self):
result = parse('hostname = foo || role=cpe && bla=blub')
print(result.debug()) # XXX debug statement
self.assertEqual(result, Disjunction(
Predicate('hostname', '=', 'foo'),
Conjunction(
Predicate('role', '=', 'cpe'),
Predicate('bla', '=', 'blub'),
)
))
def test_mixed_predicate_and_first(self):
result = parse('hostname = foo && role=cpe || bla=blub')
print(result.debug()) # XXX debug statement
self.assertEqual(result, Conjunction(
Predicate('hostname', '=', 'foo'),
Disjunction(
Predicate('role', '=', 'cpe'),
Predicate('bla', '=', 'blub'),
)
))
def test_complex(self):
result = parse(
'a=1 && b=2 || c=3 && d=4 || e=5 || f=6 && g=7 && h=8',
debug=True
)
print(result.debug()) # XXX debug statement
expected = Disjunction(
Conjunction(
Predicate('a', '=', '1'),
Predicate('b', '=', '2'),
),
Conjunction(
Predicate('c', '=', '3'),
Predicate('d', '=', '4'),
),
Predicate('e', '=', '5'),
Conjunction(
Predicate('f', '=', '6'),
Predicate('g', '=', '7'),
Predicate('h', '=', '8'),
),
)
self.assertEqual(result, expected)
答案 0 :(得分:1)
如果您使用优先级声明,则AND
和OR
都应声明为left
,而不是nonassoc
。 nonassoc
表示a OR b OR c
是非法的; left
表示将其解释为(a OR b) OR c)
,right
表示a OR (b OR c)
。 (鉴于AND
和OR
的语义,选择left
或right
没有区别,但在这种情况下left
通常更可取。)
使用优先关系,可以编写一个非常简单的语法:
query: predicate
| query AND query
| query OR query
(通常,还会有括号表达式的条目。)
上面没有做你正在寻找的链接。您可以通过走树来进行后解析,这通常是我的偏好。但它也可以使用具有明确优先权的语法进行动态链接。
显式优先级意味着语法本身定义了它的可能性;特别是,因为AND
比OR
绑定得更紧,所以不可能精确地拥有conjunction: predicate AND disjunction
,因为生成意味着AND
的第二个操作数可能是一个析取,这不是理想的结果。对于这种情况,您需要常见的级联序列:
query : disjunction # Redundant, but possibly useful for didactic purposes
disjunction : conjunction
| disjunction OR conjunction # Left associative
conjunction : predicate
| conjunction AND predicate
使用该语法,链接是直截了当的,但它需要在您的操作中进行明确的测试(例如,if p.slice(1).type == 'conjunction:
),这可能有点难看。
理想情况下,我们希望直接从语法中触发正确的操作,这意味着这样的事情(这与你的语法非常相似):
conjunction: predicate
# p[0] = p[1]
| predicate AND predicate
# p[0] = Conjunction(p[1], p[3])
| conjunction AND predicate
# p[0] = Conjunction(*(p[1]._predicates + [p[3]])
上述规则存在的问题是,第二个和第三个都适用于a AND b
,因为在将a
缩减为predicate
之后,我们同时选择将其缩减为{{ 1}}或立即转移conjunction
。在这种情况下,我们希望解析器通过无条件移位来解决shift-reduce冲突,它将执行此操作,但仅在生成警告之后。对于显式解决方案,我们需要第三个规则中的AND
为真正的连词,并且至少有一个conjunction
运算符。
考虑到这一点,我们可以将单位产品转移到级联的顶部,从而产生以下结果:
AND
现在我们不需要动作中的条件,因为我们确切地知道我们在每种情况下都有什么。
query : disjunction
| conjunction
| predicate
disjunction: predicate OR predicate
| conjunction OR predicate
| disjunction OR predicate
conjunction: predicate AND predicate
| conjunction AND predicate
对于两个优先级别的情况,所提供的语法很好,但是制作的数量最终是级别数量的二次方。如果这很烦人,那么另一种模型会有更多的单位产品:
def p_query(self, p):
'''
query : disjunction
| conjunction
| predicate
'''
p[0] = p[1]
def p_disjunction1(self, p):
'''
disjunction: predicate OR predicate
| conjunction OR predicate
'''
p[0] = Disjunction(p[1], p[3])
def p_disjunction2(self, p):
'''
disjunction: disjunction OR predicate
'''
p[0] = Disjunction(*(p[1]._predicate + [p[3]])
def p_conjunction1(self, p):
'''
conjunction: predicate AND predicate
'''
p[0] = Conjunction(p[1], p[3])
def p_conjunction2(self, p):
'''
conjunction: conjunction AND predicate
'''
p[0] = Disjunction(*(p[1]._predicate + [p[3]])
如果你不坚持解析器对象是不可变的,你可以将两个链接函数(query : disjunction
disjunction : conjunction
| disjunction_2
disjunction_2 : conjunction OR predicate
| disjunction_2 OR predicate
conjunction : predicate
| conjunction_2
conjunction_2 : predicate AND predicate
| conjunction_2 AND predicate
和p_conjunction2
)组合成一个函数:
p_disjunction2
通过将运算符标记def p_chain(self, p):
'''
conjunction: conjunction AND predicate
disjunction: disjunction OR predicate
'''
p[0] = p[1]
p[0]._predicate.append(p[3])
和AND
的值设置为构造函数而不是匹配的字符串,可以实现额外的简化。 (无论如何,匹配的字符串实际上是多余的。)这将允许构造函数(OR
和p_disjunction1
也被替换为单个函数:
p_conjunction1