Pyparsing とは?
The pyparsing module is an alternative approach to creating and executing simple grammars, vs. the traditional lex/yacc approach, or the use of regular expressions. The pyparsing module provides a library of classes that client code uses to construct the grammar directly in Python code.
pyparsing - home
だそうです。O'Reilly の本もあるそうなので金持ちは買えばいいと思う。ぱっと探してみて日本語の情報がなさそうなので書く。Python 2.7環境で試しているため、現代的ではない可能性がある。pyparsing のバージョンは 2.0.2 を使用した。
使い方
- 個別のパーザをつくる
- パーザを連結しておおきなパーザをつくる
- パーザの
.parseString
に文字列を入力し結果を評価する
以上。
パーザをつくる
パーザは2通りの作り方がある。1つは ParserElement
派生クラスのインスタンスから作る方法、もうひとつはモジュールからのジェネレータファンクションを使って適切な ParserElement
作る方法。
import pyparsing as pp
word = pp.Word(pp.alphanums)
comma = pp.Literal(',')
csv = word + pp.ZeroOrMore(comma + word)
# >>> csv.parseString("1,2,3,a,5")
(['1', ',', '2', ',', '3', ',', 'a', ',', '5'], {})
派生クラスにはインスタンスに文字列や正規表現を引数にとるものと、pyparsing のクラスそのものを引数にとるものがある。
- 引数をとる、もしくはゼロのもの:
Word
,Literal
等 - クラスをとるもの:
OneOrMore
,NotAny
,SkipTo
等
クラス(のリスト)をとるものは一部演算子が用意されている。対応するクラスはないが*
で繰り返しを表現できる。かける数とかけられる数は入れ替わってもいいが、片方は必ず0以上のint
である必要がある。(long
は受け付けない)
pp.And([p1, p2]) #== p1 + p2 ; 順序ありの結合
pp.Each([p1, p2]) #== p1 & p2 ; 順序なしの結合
pp.MatchFirst([p1, p2]) #== p1 | p2 ; 優先度つきマッチ
pp.Or([p1, p2]) #== p1 ^ p2 ; 最長マッチ
pp.NotAny(p) #== ~ p ; 否定
p + p + p #== p * 3 or 3 * p ; 結合の略記
あまり賢い命名法ではないかも知れないが、馴れでなんとかする他ない。また、Each
は順序なしとはいえバックトラックせず先頭から順に試すため、各要素に一部重なる部分があると入力を食いきれずに例外を出す。本当に和集合を使いたいのであれば適度にMatchFirst
を使うことになる。
ジェネレータファンクションは元々特定用途に特化し、内部的には正規表現で構成されている。こちらばかりになったらパーザの設計方針が誤っていると考えるべき。ここでは省く。
例題
GPS レシーバから出力される NMEA 0183 の $GPGSV 行をパーズしてみる。
$GPGSV は以下のような構造をしている。
$GPGSV,3,2,12,16,02,229,,22,21,224,16,24,02,095,,25,52,039,35*73\r\n
$
のスタートキャラクターからはじまり、\r\n
のエンドシークエンスで終わる。エンドシークエンス前には*%2h
形式のチェックサムが入る。残りの部分はカンマ区切りの値(空の可能性有)が7から16個入る。
総個数はメッセージ中からは判別できない。メッセージは分割されて送信され、また伝えるべき情報量は記載されているため計算すれば判別できるが、今回はそこまでは実施しない。(2つめのフィールドが総メッセージ数、3つめが現在のメッセージ番号、4つめが情報量(衛星の数))
buf = '$GPGSV,3,2,12,16,02,229,,22,21,224,16,24,02,095,,25,52,039,35*73\r\n'
# checksum (for validation)
ck = "{:02X}".format(reduce(lambda a,b: a^b, [ord(_) for _ in buf[1+buf.find("$"):buf.rfind("*")]]))
# simple parser unit
toint = lambda a: int(a[0])
c = pp.Literal(',').suppress()
nemo = pp.Literal('$GPGSV')
numMsg = pp.Word(pp.nums).setParseAction(toint)
msgNum = pp.Word(pp.nums).setParseAction(toint).setResultsName("msgNum")
numSV = pp.Word(pp.nums).setParseAction(toint).setResultsName("numSV")
sv = pp.Word(pp.nums).setParseAction(toint)
# combinated parser unit
toint_maybe = lambda a: int(a[0]) if a[0] else -1
elv = pp.Combine(pp.Word(pp.nums) ^ pp.Empty()).setParseAction(toint_maybe)
az = pp.Combine(pp.Word(pp.nums) ^ pp.Empty()).setParseAction(toint_maybe)
cno = pp.Combine(pp.Word(pp.nums) ^ pp.Empty()).setParseAction(toint_maybe)
cs = pp.Combine(pp.Literal('*') + pp.Literal(ck)).suppress()
block = pp.Group(c + sv + c + elv + c + az + c + cno)
blocks = pp.OneOrMore(block)
parser = nemo + c + numMsg + c + msgNum + c + numSV + blocks + cs
# result
ret = parser.parseString(buf)
覚えておくべきはWord("ab...") == Each([Literal('a'), Literal('b'),...])
というぐらいか。Literal
には長さ1以上の文字も入るため、固定文字列はこれで食わせ、数値などはWord
で消費するとよい。
ret
にパーズされた結果が帰る。一応整数に直るものはsetParseAction
で整数に直している。そうしない場合は単に文字列のリストが返る。バッファは食いたいが結果に残ってほしくないものは.suppress()
メソッドを使うかエレメントをpp.Suppress
でラップすることで消せる。有効に使っていないが.setResultsName
メソッドで名前もつけられる。
便利なところ、またはハマるところとしては
-
And
は変数を結合するだけで、結果は分離したまま
最終的なparseString
メソッドで得られる値はリストになるのだが、この要素の切れ目は各 ParserElement か、Combine
で明示的にまとめられた単位になる。そのため、単純にAnd
を使って結合していくと最後のパーザを組み立てる式と結果の対応がおかしくなる。適度にCombine
をするべき。
-
Group
を使わないとOneOrMore
等でくりかえされた時に塊を保持できない
Combine
の結合にも関連するが、何も工夫しないと結果は文字列のリストになる。入れ子にもならないため、下手にZeroOrMore
などした日には結果がわけわからんことになる。入れ子を作りたいときには Group
を使う。setParseAction
を使うことを考えると必ず使うことになる。
-
ParserElement
のメソッドは非破壊的でコピーを返す
ので宣言的に書くことができないが、その代わりにメソッドチェーンができる。
-
setParseAction
はtry-catch
で関数を試す
ドキュメントによるといくつかプロトタイプが設定されているが、各プロトタイプ向けに引数設定しては投げ、失敗したら次を試し、最後までエラーだったら最後のエラーを全体のエラーとして返すため、下手に内部で例外が発生すると猛烈にデバッグがやりづらい。引数処理で間違っていても最後のエラーが引数が足りないプロトタイプエラーになるため、何をいっているのかわからなくなる。
以上。