2012-06-21 7 views
17

Próbuję parsować złożone wyrażenie logiczne, takie jak poniższe;parsowanie złożonego wyrażenia logicznego w pyparsing w drzewie binarnym

x > 7 AND x < 8 OR x = 4 

i pobierz przeanalizowany ciąg znaków jako drzewo binarne. Na powyższym wyrażeniu oczekiwany analizowany wyrażenie powinno wyglądać

[['x', '>', 7], 'AND', [['x', '<', 8], 'OR', ['x', '=', 4]]] 

„OR” operator logiczny ma wyższy priorytet niż „i” operatora. Nawias może zastąpić domyślny priorytet. Aby być bardziej ogólnym, analizowane wyrażenie powinno wyglądać;

<left_expr> <logical_operator> <right_expr> 

Innym przykładem może być

input_string = x > 7 AND x < 8 AND x = 4 
parsed_expr = [[['x', '>', 7], 'AND', ['x', ',', 8]], 'AND', ['x', '=', 4]] 

Dotychczas wymyśliłem tego prostego rozwiązania, które niestety nie mogą generować przeanalizowanych wyraz w binarnym drzewie mody. operatorPrecedence nie wydaje mi się, że pomogło mi tutaj, gdy jest taki sam operator logiczny, jak w poprzednim przykładzie.

import pyparsing as pp 
complex_expr = pp.Forward() 
operator = pp.Regex(">=|<=|!=|>|<|=").setName("operator") 
logical = (pp.Keyword("AND") | pp.Keyword("OR")).setName("logical") 
vars = pp.Word(pp.alphas, pp.alphanums + "_") | pp.Regex(r"[+-]?\d+(:?\.\d*)?(:?[eE][+-]?\d+)?") 
condition = (vars + operator + vars) 
clause = pp.Group(condition^(pp.Suppress("(") + complex_expr + pp.Suppress(")"))) 

expr = pp.operatorPrecedence(clause,[ 
          ("OR", 2, pp.opAssoc.LEFT,), 
          ("AND", 2, pp.opAssoc.LEFT,),]) 

complex_expr << expr 
print complex_expr.parseString("x > 7 AND x < 8 AND x = 4") 

Wszelkie sugestie lub wskazówki są mile widziane.

BNF dla wyrażenia (bez nawiasów) może być

<expr>  -> <expr> | <expr> <logical> <expr> 
<expr>  -> <opnd> <relational> <opnd> 
<opnd>  -> <variable> | <numeric> 
<relational> -> <'>'> | <'='> | <'>='> | <'<='> | <'!='> 
+0

Kod jest nieco trudne do naśladowania, można zakładać gramatykę w BNF? – georg

+0

właśnie dodałem BNF ... nie jestem pewien czy jest jednoznaczny czy nie. – consumer

Odpowiedz

13

Spróbuj zmienić:

expr = pp.operatorPrecedence(clause,[ 
          ("OR", 2, pp.opAssoc.LEFT,), 
          ("AND", 2, pp.opAssoc.LEFT,),]) 

do:

expr = pp.operatorPrecedence(condition,[ 
          ("OR", 2, pp.opAssoc.LEFT,), 
          ("AND", 2, pp.opAssoc.LEFT,),]) 

Pierwszym argumentem operatorPrecedence jest prymitywny operand być używane z operatorami - nie ma potrzeby dołączania twojego complexExpr nawiasy - operatorPrecedence zrobi to za Ciebie. Ponieważ Twój argument jest rzeczywiście inny głębsze porównanie, można rozważyć zmianę:

condition = (expr + operator + expr) 

do:

condition = pp.Group(expr + operator + expr) 

tak że wyjście operatorPrecedence jest łatwiejszy w obróbce. Z tych zmian, analizowania x > 7 AND x < 8 OR x = 4 daje:

[[['x', '>', '7'], 'AND', [['x', '<', '8'], 'OR', ['x', '=', '4']]]] 

który rozpoznaje wyższy priorytet i grupy to albo pierwszy. (Czy na pewno chcesz tej kolejności pierwszeństwa AND i OR? Myślę, że tradycyjna kolejność jest odwrotnością, jak pokazano w this wikipedia entry.)

Myślę, że również pytasz, dlaczego pyparsing i operatorPrecedence nie zwraca wyników w zagnieżdżonych pary binarne, czyli można się spodziewać parsowania „a i B i C” wrócą:

[['A', 'and', 'B'] 'and', 'C'] 

ale co masz jest:

['A', 'and', 'B', 'and', 'C'] 

to dlatego operatorPrecedence analizuje powtarzane operacje w tym samym priorytecie poziom przy użyciu re petycja, a nie rekursja.Zobacz this question, który jest bardzo podobny do twojego, i którego odpowiedź zawiera akcję parsowania, aby przekonwertować twoje powtarzalne drzewo parsowania do bardziej tradycyjnego drzewa parsowania binarnego. Możesz również znaleźć a sample boolean expression parser implemented using operatorPrecedence na stronie wiki pyparskiej.

EDIT: celu wyjaśnienia, to jest to, co polecam Ci zmniejszyć parser do:

import pyparsing as pp 

operator = pp.Regex(">=|<=|!=|>|<|=").setName("operator") 
number = pp.Regex(r"[+-]?\d+(:?\.\d*)?(:?[eE][+-]?\d+)?") 
identifier = pp.Word(pp.alphas, pp.alphanums + "_") 
comparison_term = identifier | number 
condition = pp.Group(comparison_term + operator + comparison_term) 

expr = pp.operatorPrecedence(condition,[ 
          ("AND", 2, pp.opAssoc.LEFT,), 
          ("OR", 2, pp.opAssoc.LEFT,), 
          ]) 

print expr.parseString("x > 7 AND x < 8 OR x = 4") 

Jeśli wsparcie nie może być również coś chcesz dodać, a następnie to będzie wyglądać:

expr = pp.operatorPrecedence(condition,[ 
          ("NOT", 1, pp.opAssoc.RIGHT,), 
          ("AND", 2, pp.opAssoc.LEFT,), 
          ("OR", 2, pp.opAssoc.LEFT,), 
          ]) 

w pewnym momencie, może chcesz rozszerzyć definicję comparison_term z pełniejszego wyrażenia arytmetycznego, określonym z własnym operatorPrecedence definicji. Sugerowałbym, aby zrobić to w ten sposób, zamiast tworzyć jedną definicję potwora opPrec, jak już wspomniałeś o niektórych wadach wydajności do opPrec. Jeśli nadal występują problemy z wydajnością, spójrz na ParserElement.enablePackrat.

+0

Cześć Paul, Dziękuję za kierowanie mną. Mam jednak inne pytanie. Korzystanie z trzech poziomów priorytetów zgodnie z zaleceniami zwiększyło czas analizowania, a także czas sprawdzania poprawności gramatyki również drastycznie się zwiększył. Czy widzisz jakąkolwiek możliwość poprawy wydajności zapytania? – consumer

+1

??? Jestem nieco zdezorientowany tym, co sugerowałem na 3. poziomie. Właściwie to sugerowałem * usunięcie * dodania 'complex_expr' i' clause', i użycie 'expr'. Nie sugerowałem, że dodajesz coś do swojej listy operatorów. Poleciłam, abyś ponownie odwiedził * zlecenie * operatorów, jak ocenia tradycyjna matematyka ORAZ przed OR, a nie na odwrót, jak to masz obecnie. – PaulMcG

5

Pozwól mi zasugerować to podejście parsowania, pochodzące bezpośrednio z klasy Petera Norviga w projektowaniu programów komputerowych w udacity (i modyfikowane do twoich potrzeb).

from functools import update_wrapper 
from string import split 
import re 

def grammar(description, whitespace=r'\s*'): 
    """Convert a description to a grammar. Each line is a rule for a 
    non-terminal symbol; it looks like this: 
     Symbol => A1 A2 ... | B1 B2 ... | C1 C2 ... 
    where the right-hand side is one or more alternatives, separated by 
    the '|' sign. Each alternative is a sequence of atoms, separated by 
    spaces. An atom is either a symbol on some left-hand side, or it is 
    a regular expression that will be passed to re.match to match a token. 

    Notation for *, +, or ? not allowed in a rule alternative (but ok 
    within a token). Use '\' to continue long lines. You must include spaces 
    or tabs around '=>' and '|'. That's within the grammar description itself. 
    The grammar that gets defined allows whitespace between tokens by default; 
    specify '' as the second argument to grammar() to disallow this (or supply 
    any regular expression to describe allowable whitespace between tokens).""" 
    G = {' ': whitespace} 
    description = description.replace('\t', ' ') # no tabs! 
    for line in split(description, '\n'): 
     lhs, rhs = split(line, ' => ', 1) 
     alternatives = split(rhs, ' | ') 
     G[lhs] = tuple(map(split, alternatives)) 
    return G 

def decorator(d): 
    def _d(fn): 
     return update_wrapper(d(fn), fn) 
    update_wrapper(_d, d) 
    return _d 

@decorator 
def memo(f): 
    cache = {} 
    def _f(*args): 
     try: 
      return cache[args] 
     except KeyError: 
      cache[args] = result = f(*args) 
      return result 
     except TypeError: 
      # some element of args can't be a dict key 
      return f(args) 
    return _f 

def parse(start_symbol, text, grammar): 
    """Example call: parse('Exp', '3*x + b', G). 
    Returns a (tree, remainder) pair. If remainder is '', it parsed the whole 
    string. Failure iff remainder is None. This is a deterministic PEG parser, 
    so rule order (left-to-right) matters. Do 'E => T op E | T', putting the 
    longest parse first; don't do 'E => T | T op E' 
    Also, no left recursion allowed: don't do 'E => E op T'""" 

    tokenizer = grammar[' '] + '(%s)' 

    def parse_sequence(sequence, text): 
     result = [] 
     for atom in sequence: 
      tree, text = parse_atom(atom, text) 
      if text is None: return Fail 
      result.append(tree) 
     return result, text 

    @memo 
    def parse_atom(atom, text): 
     if atom in grammar: # Non-Terminal: tuple of alternatives 
      for alternative in grammar[atom]: 
       tree, rem = parse_sequence(alternative, text) 
       if rem is not None: return [atom]+tree, rem 
      return Fail 
     else: # Terminal: match characters against start of text 
      m = re.match(tokenizer % atom, text) 
      return Fail if (not m) else (m.group(1), text[m.end():]) 

    # Body of parse: 
    return parse_atom(start_symbol, text) 

Fail = (None, None) 

MyLang = grammar("""expression => block logicalop expression | block 
block => variable operator number 
variable => [a-z]+ 
operator => <=|>=|>|<|= 
number => [-+]?[0-9]+ 
logicalop => AND|OR""", whitespace='\s*') 

def parse_it(text): 
    return parse('expression', text, MyLang) 

print parse_it("x > 7 AND x < 8 AND x = 4") 

Wyjścia:

(['expression', ['block', ['variable', 'x'], ['operator', '>'], ['number', '7']], ['logicalop', 'AND'], ['expression', ['block', ['variable', 'x'], ['operator', '<'], ['number', '8']], ['logicalop', 'AND'], ['expression', ['block', ['variable', 'x'], ['operator', '='], ['number', '4']]]]], '') 
+1

Dzięki za sugestię ... ale rozwiązanie zaproponowane przez Paula było tym, którego szukałem. Dziękuję za poświęcenie czasu :) – consumer

Powiązane problemy