構文解析器を「つくってみた」というと、四則演算や簡単なプログラミング言語をつくってみたというのをよく見かけるけど、個人的には何かのプログラムの入力データをパース(構文解析)することが多い。
四則演算ができたところで結局は「つくってみた」以上のものではないし、プログラミング言語を(本格的に)つくってみようという人もどちらかといえば稀だろう。
一方でプログラム(それが何であれ)というのはたいてい入力が必要なものだし、ファイルから入力を読み込むことも少なくない。ただデータが並んでいればいい、というのでなければ何らかのフォーマットを決める必要がある。JSON とか YAML とかいった汎用のフォーマットもあるけど、必ずしも目的に対して適当とは言えないこともある。
というわけで構文解析器(パーサ)の出番になるわけだ。
以前には Haskel のパーサコンビネータ(Parsec)や Ruby のパーサジェネレータ(Racc)を使ったことがあるけど、Python でははじめて。かるく調べてみたところ、タイトルに書いた Lark というライブラリが良さげだったので試してみることにした。
今回はずいぶん前に Haskell でつくったあるプログラムの入力ファイルをサンプルとしてとりあげる。↓こんなデータ。
// Example for input data. *MODEL RO *GAMMA0.5 // % 0.150 *HMAX 0.200 *PLOT // % 0.0001 0.0002 0.0005 0.001 0.002 0.005 0.01 0.02 0.05 0.1 0.2 0.5 1.0 2.0 5.0 10.0 *END
*
で始まる文字列がデータの種類を表すラベルで、その後に1つ以上のデータが空白で区切られて続く。改行は意味を持たない(空白とみなす)。ただし //
から行末まではコメント。
さて、Lark ではまず解析対象の文法を定義したファイルを用意する。文法は EBNF ベースの構文で定義する。今回の例ではこうなった。
?input : model gamma hmax plot end model : "*MODEL" modelname modelname : UCASE_LETTER+ gamma : "*GAMMA0.5" real hmax : "*HMAX" real plot : "*PLOT" real+ end : "*END" real : FLOAT %import common.UCASE_LETTER %import common.FLOAT %import common.WS %ignore WS %import common.CPP_COMMENT %ignore CPP_COMMENT
で、これを Lark に渡せばパーサを返してくれる。プログラムはこう。
from lark import Lark import sys with open("larksample.lark", "r", encoding="utf-8") as grammar: parser = Lark(grammar.read(), start="input") with open(sys.argv[1], "r") as f: input_data = f.read() tree = parser.parse(input_data) print(tree)
たったこれだけで構文解析をして構文木を返してくれる。実行してみると:
takatoh@apostrophe:larksample$ python larksample.py larksample.dat Tree('input', [Tree('model', [Tree('modelname', [Token('UCASE_LETTER', 'R'), Token('UCASE_LETTER', 'O')])]), Tree('gamma', [Tree('real', [Token('FLOAT', '0.150')])]), Tree('hmax', [Tree('real', [Token('FLOAT', '0.200')])]), Tree('plot', [Tree('real', [Token('FLOAT', '0.0001')]), Tree('real', [Token('FLOAT', '0.0002')]), Tree('real', [Token('FLOAT', '0.0005')]), Tree('real', [Token('FLOAT', '0.001')]), Tree('real', [Token('FLOAT', '0.002')]), Tree('real', [Token('FLOAT', '0.005')]), Tree('real', [Token('FLOAT', '0.01')]), Tree('real', [Token('FLOAT', '0.02')]), Tree('real', [Token('FLOAT', '0.05')]), Tree('real', [Token('FLOAT', '0.1')]), Tree('real', [Token('FLOAT', '0.2')]), Tree('real', [Token('FLOAT', '0.5')]), Tree('real', [Token('FLOAT', '1.0')]), Tree('real', [Token('FLOAT', '2.0')]), Tree('real', [Token('FLOAT', '5.0')]), Tree('real', [Token('FLOAT', '10.0')])]), Tree('end', [])])
パースした結果が木構造になってるのがなんとなくわかる。とはいえさすがにこれは見づらいので、Tree
クラスに用意されてる pretty()
メソッドを使ってみよう。print(tree)
を print(tree.pretty())
に変えてやればいい。
takatoh@apostrophe:larksample$ python larksample.py larksample.dat input model modelname R O gamma real 0.150 hmax real 0.200 plot real 0.0001 real 0.0002 real 0.0005 real 0.001 real 0.002 real 0.005 real 0.01 real 0.02 real 0.05 real 0.1 real 0.2 real 0.5 real 1.0 real 2.0 real 5.0 real 10.0 end
見やすくなったね。
この木構造をプログラムで利用しやすい形に変換するには lark.Transformer
クラスを継承したクラスをつくってやる。プログラムはこうなった。
from lark import Lark from lark import Transformer import sys from pprint import pprint class MyTransformer(Transformer): def input(self, tokens): (model, gamma, hmax, plot, _) = tokens return { "model" : model, "gamma0.5" : gamma, "hmax" : hmax, "plot" : plot, } def model(self, tokens): (m,) = tokens return m def modelname(self, tokens): return "".join(tokens) def gamma(self, tokens): (g,) = tokens return g def hmax(self, tokens): (h,) = tokens return h def plot(self, tokens): return list(tokens) def end(self, tokens): return None def real(self, tokens): (r,) = tokens return float(r) with open("larksample.lark", "r", encoding="utf-8") as grammar: parser = Lark(grammar.read(), start="input") with open(sys.argv[1], "r") as f: input_data = f.read() tree = parser.parse(input_data) result = MyTransformer().transform(tree) pprint(result)
lark.Transformer
を継承した MyTransformer
クラスの transform()
メッソッドにパースした結果を渡してやると、変換した結果が返ってくる。今回は普通の辞書にした。
MyTransformer
クラスに定義しているメソッドは、それぞれ文法ファイルで定義した構文要素に対応していて、transform()
は構文木をたどりながらノード(つまり構文要素)に対応するメソッドを呼び出す。各メソッドの引数 tokens
は子ノードのリスト。だからこれらのメソッドから適切な値を返せば、最終的に目的の構造を持ったデータ(今回は辞書)が手に入る。
これを実行するとこんなふうに出力される。
takatoh@apostrophe:larksample$ python larksample.py larksample.dat {'gamma0.5': 0.15, 'hmax': 0.2, 'model': 'RO', 'plot': [0.0001, 0.0002, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0]}
無事、目的のものが手に入った。
[追記]
インストール:
takatoh@apostrophe:larksample$ pip install lark-parser
参考ページ:
その他:
- PyPI には lark てのと lark-parser てのがあるけどどう違うの?
lark.Transformer.transform()
から呼び出される各メソッドは、子要素(を評価した値)のリストtokens
を引数にとり、自身を評価した値を返す。- 子要素とはつまり、文法ファイルで定義した
:
の右側の要素のこと。 - ではあるんだけど、文法ファイルで文字列を直接使う(たとえば
"*MODEL"
のように)と、メソッドの引数リストに入らない。これ、気づくまでにちょっとかかった。 - よく使いそうな文法要素(
FLOAT
とかUCASE_LETTER
とか)は予め定義されてて、インポートして使うことができる。ドキュメントには全部は書いてないみたいなので GitHub のリポジトリでソースファイルを見るといい。