19

以下のような複雑な論理式を解析しようとしています。

x > 7 AND x < 8 OR x = 4

解析された文字列をバイナリツリーとして取得します。上記の式の場合、予想される解析済みの式は次のようになります。

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

「OR」論理演算子は「AND」演算子よりも優先されます。括弧はデフォルトの優先順位を上書きできます。より一般的には、解析された式は次のようになります。

<left_expr> <logical_operator> <right_expr>

別の例は

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

これまでのところ、残念ながらバイナリツリー形式で解析された式を生成できないこの単純なソリューションを思いつきました。operatorPrecedenceは、前の例と同じ論理演算子が連続して存在する場合、ここでは役に立たないようです。

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")

任意の提案やガイダンスは大歓迎です。

BNF式(括弧なし)は次のようになります。

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

2 に答える 2

16

注:operatorPrecedencepyparsingのメソッドは廃止され、メソッド名が優先されinfixNotationます。

変更してみてください:

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

に:

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

operatorPrecedenceの最初の引数は、演算子で使用されるプリミティブオペランドです。complexExprを括弧で囲む必要はありません。operatorPrecedenceがそれを行います。オペランドは実際には別のより深い比較であるため、以下を変更することを検討してください。

condition = (expr + operator + expr)

に:

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

これにより、operatorPrecedenceの出力の処理が容易になります。これらの変更により、解析により次のことが可能になりますx > 7 AND x < 8 OR x = 4

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

これは、ORのより高い優先順位を認識し、それを最初にグループ化します。(このANDとORの優先順位が必要ですか?このウィキペディアのエントリに示されているように、従来の順序は逆だと思います。)

また、pyparsingとoperatorPrecedenceがネストされたバイナリペアで結果を返さない理由も質問していると思います。つまり、「AとBとC」を解析すると次のように返されると予想されます。

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

しかし、あなたが得るものは次のとおりです。

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

これは、operatorPrecedenceが、再帰ではなく繰り返しを使用して、同じ優先順位レベルで繰り返される操作を解析するためです。あなたの質問と非常によく似たこの質問を参照してください。その答えには、反復的な解析ツリーをより従来のバイナリ解析ツリーに変換するための解析アクションが含まれています。また、pyparsingwikiページでoperatorPrecedenceを使用して実装されたサンプルブール式パーサーを見つけることができます。

編集:明確にするために、これはパーサーを次のように減らすことをお勧めします:

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")

NOTのサポートも追加したい場合は、次のようになります。

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

comparison_termある時点で、独自の定義で定義された、より完全な算術式での定義を拡張したい場合がありoperatorPrecedenceます。opPrecパフォーマンスの欠点のいくつかをすでにほのめかしているので、1つのモンスター定義を作成するのではなく、この方法で行うことをお勧めしますopPrec。それでもパフォーマンスの問題が発生する場合は、を調べてParserElement.enablePackratください。

于 2012-06-21T08:59:44.663 に答える
7

この解析アプローチを提案させてください。これは、udacityでのコンピュータープログラムの設計におけるPeter Norvigのクラスから直接得られたものです(そして、ニーズに合わせて調整されています)。

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")

出力:

(['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']]]]], '')
于 2012-06-21T09:11:45.513 に答える