この形式の概念的な BNF は次のようになります。
node :: word composed of alphas, digits, '_'
verb :: one of several defined keywords
binop :: '->' | '<-' | '<->'
nodeFactor :: node '^' node | node
nodeExpr :: nodeFactor op nodeFactor
nodeCommand :: verb nodeExpr [',' nodeExpr]...
これは、ほぼステップごとに pyparsing にマップされます。
from pyparsing import (Word,alphas,alphanums,Keyword,
infixNotation,opAssoc,oneOf,delimitedList)
nodeRef = Word(alphas,alphanums+'_')
GO, TURN, FOLLOW = map(Keyword, "GO TURN FOLLOW".split())
verb = GO | TURN | FOLLOW
binop = oneOf('-> <- <->')
次の部分は、pyparsing のinfixNotation
方法 (以前は として知られていましたoperatorPrecedence
) を使用して最も簡単に実装できます。infixNotation
操作の階層を定義することができ、階層によって定義された優先順位に従って解析された出力をグループ化します。'^'
「内部にある」演算子は、バイナリ'->'
などの演算子の前に評価する必要があると想定しています。infixNotation
括弧内のネストも許可されますが、これが絶対に必要であると示す例はありません。を定義するにはinfixNotation
、ベース オペランドの型を指定し、続いて 3 つのタプルのリストを指定します。各タプルには、演算子、単項演算子、2 項演算子、または 3 項演算子の値 1、2 または 3、および左結合または右結合の定数opAssoc.LEFT
またはが示されます。RIGHT
オペレーター:
nodeExpr = infixNotation(nodeRef,
[
('^', 2, opAssoc.LEFT),
(binop, 2, opAssoc.LEFT),
])
最後に、ある種のコマンドとして解釈した全体的な式を定義します。ノード式のコンマ区切りのリストは、次のように直接実装できますnodeExpr + ZeroOrMore(Suppress(',') + nodeExpr)
(解析された出力からコンマを抑制します - コンマは解析時に役立ちますが、後でスキップする必要があります)。しかし、これは非常に頻繁に発生するため、pyparsing はメソッドを提供しますdelimitedList
:
nodeCommand = verb('verb') + delimitedList(nodeExpr)('nodes')
「verb」および「nodes」という名前により、それぞれの式で解析された結果がそれらの名前に関連付けられます。これにより、解析が完了した後に解析されたデータを簡単に操作できるようになります。
パーサーをテストするには:
tests = """\
GO node1,node2
TURN node1->node2->node3
GO node1,node2^node3,node4
FOLLOW node1->node2<->node3
GO node5,node1->node2^node4<->node3,node6
""".splitlines()
for test in tests:
test = test.strip()
if not test:
continue
print (test)
try:
result = nodeCommand.parseString(test, parseAll=True)
print (result.dump())
except ParseException as pe:
print ("Failed:", test)
print (pe)
このdump()
メソッドは、解析されたトークンをネストされたリストとして出力し、各結果の名前とそれに添付された値をリストします。
GO node1,node2
['GO', 'node1', 'node2']
- nodes: ['node1', 'node2']
- verb: GO
TURN node1->node2->node3
['TURN', ['node1', '->', 'node2', '->', 'node3']]
- nodes: [['node1', '->', 'node2', '->', 'node3']]
- verb: TURN
GO node1,node2^node3,node4
['GO', 'node1', ['node2', '^', 'node3'], 'node4']
- nodes: ['node1', ['node2', '^', 'node3'], 'node4']
- verb: GO
FOLLOW node1->node2<->node3
['FOLLOW', ['node1', '->', 'node2', '<->', 'node3']]
- nodes: [['node1', '->', 'node2', '<->', 'node3']]
- verb: FOLLOW
GO node5,node1->node2^node4<->node3,node6
['GO', 'node5', ['node1', '->', ['node2', '^', 'node4'], '<->', 'node3'], 'node6']
- nodes: ['node5', ['node1', '->', ['node2', '^', 'node4'], '<->', 'node3'], 'node6']
- verb: GO
この時点で、コマンドを解析してから、 に基づいて、verb
その動詞を実行する適切なメソッドにディスパッチできます。
しかし、Python オブジェクトを使用してこのロジックをキャプチャするのに役立つことがわかった構造を提案させてください。抽象メソッドでさまざまな動詞関数を実装するコマンドの単純なクラス階層を定義しますdoCommand
。
# base class
class Command(object):
def __init__(self, tokens):
self.cmd = tokens.verb
self.nodeExprs = tokens.nodes
def doCommand(self):
"""
Execute command logic, using self.cmd and self.nodeExprs.
To be overridden in sub classes.
"""
print (self.cmd, '::', self.nodeExprs.asList())
# these should implement doCommand, but not needed for this example
class GoCommand(Command): pass
class TurnCommand(Command): pass
class FollowCommand(Command): pass
このメソッドは、解析された結果を適切なコマンド クラスのインスタンスに変換します。
verbClassMap = {
'GO' : GoCommand,
'TURN' : TurnCommand,
'FOLLOW' : FollowCommand,
}
def tokensToCommand(tokens):
cls = verbClassMap[tokens.verb]
return cls(tokens)
ただし、これを解析時のコールバックとしてパーサーに組み込むこともできます。これにより、解析が完了すると、文字列とサブリストのリストだけでなく、doCommand
メソッドを呼び出して「実行」できるオブジェクトを取得できます。これを行うには、式tokensToCommand
全体に解析アクションとしてアタッチします。nodeCommand
nodeCommand.setParseAction(tokensToCommand)
ここで、テスト コードを少し変更します。
for test in tests:
test = test.strip()
if not test:
continue
try:
result = nodeCommand.parseString(test, parseAll=True)
result[0].doCommand()
except ParseException as pe:
print ("Failed:", test)
print (pe)
実際doCommand
にはサブクラスに実装していないため、得られるのはデフォルトの基本クラスの動作だけです。これは、解析された動詞とノード リストをエコー バックするだけです。
GO :: ['node1', 'node2']
TURN :: [['node1', '->', 'node2', '->', 'node3']]
GO :: ['node1', ['node2', '^', 'node3'], 'node4']
FOLLOW :: [['node1', '->', 'node2', '<->', 'node3']]
GO :: ['node5', ['node1', '->', ['node2', '^', 'node4'], '<->', 'node3'], 'node6']
(このコードは Python 3、pyparsing 2.0.0 で実行されました。Python 2、pyparsing 1.5.7 でも実行されます。)
編集
連鎖式を に取得するa op b op c
に[a,op,b], [b, op, c]
は、解析アクションを使用して [a,op,b,op,c] の結果をペアワイズ式に解析して再構築します。このinfixNotation
メソッドを使用すると、演算子階層のレベルにアタッチする解析アクションを定義できます。
連鎖式の結果を再構築する方法は次のようになります。
def expandChainedExpr(tokens):
ret = ParseResults([])
tokeniter = iter(tokens[0])
lastexpr = next(tokeniter)
for op,nextexpr in zip(tokeniter,tokeniter):
ret += ParseResults([[lastexpr, op, nextexpr]])
lastexpr = nextexpr
return ret
これにより、まったく新しい ParseResults が構築され、元の連鎖結果が置き換えられます。それぞれlastexpr op nextexpr
が独自のサブグループとして保存され、次にnextexpr
にコピーされlastexpr
、ループして次の op-nextexpr ペアを取得する方法に注意してください。
このリフォーマッタをパーサーにアタッチするには、その階層のレベルの 4 番目の要素として次のように追加しますinfixNotation
。
nodeExpr = infixNotation(nodeRef,
[
('^', 2, opAssoc.LEFT),
(binop, 2, opAssoc.LEFT, expandChainedExpr),
])
今の出力:
FOLLOW node1->node2<->node3
次のように展開されます。
('FOLLOW', '::', [['node1', '->', 'node2'], ['node2', '<->', 'node3']])