如何编辑我的解析器以正确分组“AND”和“OR”谓词?

时间:2017-07-25 09:52:01

标签: python parsing yacc lex

我目前正在尝试编写一个能够解析非常简单的key = value查询的小型解析器。但它应该足够聪明,可以处理ANDOR组,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)

1 个答案:

答案 0 :(得分:1)

如果您使用优先级声明,则ANDOR都应声明为left,而不是nonassocnonassoc表示a OR b OR c是非法的; left表示将其解释为(a OR b) OR c)right表示a OR (b OR c)。 (鉴于ANDOR的语义,选择leftright没有区别,但在这种情况下left通常更可取。)

使用优先关系,可以编写一个非常简单的语法:

query: predicate
     | query AND query
     | query OR query

(通常,还会有括号表达式的条目。)

上面没有做你正在寻找的链接。您可以通过走树来进行后解析,这通常是我的偏好。但它也可以使用具有明确优先权的语法进行动态链接。

显式优先级意味着语法本身定义了它的可能性;特别是,因为ANDOR绑定得更紧,所以不可能精确地拥有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

注释

  1. 对于两个优先级别的情况,所提供的语法很好,但是制作的数量最终是级别数量的二次方。如果这很烦人,那么另一种模型会有更多的单位产品:

    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]])
    
  2. 如果你不坚持解析器对象是不可变的,你可以将两个链接函数(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的值设置为构造函数而不是匹配的字符串,可以实现额外的简化。 (无论如何,匹配的字符串实际上是多余的。)这将允许构造函数(ORp_disjunction1也被替换为单个函数:

    p_conjunction1