Parsing In Python: Tools And Libraries より。
parsimonious。pyPEG と違ってこちらは「ちゃんと PEG」なことはドキュメントからすぐにわかったし、ノードトラパースのやり方も「書いてあるから印象いいよな」、と思った、初見で。
やってみたらノードのトラバースはやや鬱陶しい。ともあれ「へろわるど」:
1 # -*- coding: utf-8 -*-
2 from parsimonious.grammar import Grammar, NodeVisitor
3
4 g = Grammar(
5 """
6 sentences = sentence (~"\s+" sentence)* #
7 sentence = word (~"\s+" word)* "." # Grouping, One or more things.
8 word = styled_word / alnum #
9 styled_word = bold_word / italic_word # Ordered choice.
10 bold_word = "((" alnum "))" #
11 italic_word = "''" alnum "''" #
12 alnum = ~"[A-Z0-9]+"i # identical to re.compile(r"[A-Z0-9]+", flags=re.I)
13 """)
14
15 class _Visitor(NodeVisitor):
16 def visit_sentences(self, node, visited_children):
17 return " ".join(visited_children)
18
19 def visit_sentence(self, node, visited_children):
20 return " ".join(visited_children)
21
22 # word = styled_word / alnum
23 def visit_word(self, node, visited_children):
24 return " ".join(visited_children)
25
26 # styled_word = bold_word / italic_word
27 def visit_styled_word(self, node, visited_children):
28 return "".join(visited_children)
29
30 def visit_bold_word(self, node, visited_children):
31 return "<strong>{}</strong>".format(node.text[2:-2])
32
33 def visit_italic_word(self, node, visited_children):
34 return "<i>{}</i>".format(node.text[2:-2])
35
36 def visit_alnum(self, node, visited_children):
37 return node.text
38
39 def generic_visit(self, node, visited_children): # fallback
40 return " ".join(visited_children)
41
42 tree = g.parse(
43 "Everything that exists ''works''. Test coverage is ((good)).")
44 visitor = _Visitor()
45 print(visitor.visit(tree))
Visitor を自作せずとも「ぷりちぃぷりんと」は(この例での) tree オブジェクトが勝手にやってくれる。「何か価値あること」をしたいなら Visitor を書く、というノリ。「やや鬱陶しい」は後述。けどやや鬱陶しかろうが、pyPEG で悶々としたような類のストレスではなくて、だってほんの一時間程度でここまで辿りついたもの。印象はいいよ、すごく。
「構文定義としての PEG 記述」部分はやや独自。けどすぐにわかるであろ。Wikipedia の説明との対応も取りやすいであろう。
「ビジターの記述はやや鬱陶しい」については、基底クラスの NodeVisitor のせい:
161 class NodeVisitor(with_metaclass(RuleDecoratorMeta, object)):
162 # ...
163 def visit(self, node):
164 # ...
165 method = getattr(self, 'visit_' + node.expr_name, self.generic_visit)
166
167 # Call that method, and show where in the tree it failed if it blows
168 # up.
169 try:
170 return method(node, [self.visit(n) for n in node])
171 except (VisitationError, UndefinedLabel):
172 # Don't catch and re-wrap already-wrapped exceptions.
173 raise
174 except self.unwrapped_exceptions:
175 raise
176 except Exception:
177 # Catch any exception, and tack on a parse tree so it's easier to
178 # see where it went wrong.
179 exc_class, exc, tb = exc_info()
180 reraise(VisitationError, VisitationError(exc, exc_class, node), tb)
181
182 def generic_visit(self, node, visited_children):
183 # ...
184 raise NotImplementedError('No visitor method was defined for this expression: %s' %
185 node.expr.as_rule())
何が鬱陶しいかといえば、node と visited_children のどちらに関与すべきかを、Rule 記述を踏まえつつ考えなければならんから…、というか伝わりにくいな。yacc とかだと、各 produce は基本的に上の visitor で言うところの「node」だけ意識し、「最終的な木」とは別々に考えるわけなので、(それが場合によってはわずらわしいことはあるにせよ)少なくとも「同時に二つの異なる思考をする必要はない」のね。けどこのビジターは、自ノードと木全体を両方一気に意識しながら書かないといけない。なのでちと慣れないと相当頭混乱する。
アタシが書いたビジターで、visit_word
と visit_styled_word
にだけあえてコメント入れてるのは、この例の場合は visit_bold_word
、visit_italic_word
が「処理済」のテキスト(<strong>
, <i>
で修飾したテキスト)を「横流しすべきその時」だから。つまり node.text
に言及してはダメ、そこでは。
それと、なんとなく「必要なとこだけ書いて、そうでないとこは全部 generic_visit
で誤魔化したい」気分にはなるんだけれど、「そういう仕組み」な都合、現実には結局全ルール分書くことになるような気がする。まぁ yacc なんかも全部書くんだから、なんとなく釈然としない、というのもヘンな話ではあるんだけれど、「誤魔化せそうじゃん」と思うシカケになってるのと、あと yacc と違って「ルールとアクションが一体」じゃないからそう感じるんだろうね。
そういうわけで、「ほんの少しだけ、むむ」はあるけれど、非常に直接的で素直だし簡単。あと定義を文字列で記述するスタイルが好きなんだよね、あたしゃ。「普通の Python に混ぜ込む」スタイルって、書きながら自分が何を書いてるんだか混乱しがち(普通の python 使いをしてるのか PEG 定義部分を書いてるのか、てこと)だし、読み手も苦労するのさ。無論、pyPEG に感じた「PEG ってる気になれない」のとは正反対。なんなら「設計書を PEG で書いたとして、それをほぼストレートに記述出来る」であろう。
これは気に入りそうだ。よいとオモイマス。
2:50 追記:
もう一つ第一印象の良い Arpeggio をみているところなのだが、比較していて parsimonious のビジターの印象が良くない真の原因がなんとなくわかってきた。要するに terminal、non-terminal 「全部」について書かなければならないのが大変鬱陶しい原因。Arpeggio は、terminal に関しては「必要ならば」、基本 non-terminal だけ書けばいいようだ。
実際 parsimonious のビジターがいやらしいのは、ワタシが書いた例だと「\s+
」が generic_visit
にディスパッチされてしまうからなのだ。けれどこれはルール sentence
、sentences
の右辺なのだから、「child
」として入ってくれてればそれで良かったのである。まだ実際に動かして試してはいないけれど、Arpeggio のほうのビジターの例(対応する構文定義) がまさにそうなっているようで、だから遥かにずっとこちらが印象が良い。圧倒的にわかりやすく、ワタシが良く知る yacc なんかとほとんど同じ思考でスムーズに理解出来る。