7
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Pyparsing を学ぶ

Posted at

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 のメソッドは非破壊的でコピーを返す

ので宣言的に書くことができないが、その代わりにメソッドチェーンができる。

  • setParseActiontry-catch で関数を試す

ドキュメントによるといくつかプロトタイプが設定されているが、各プロトタイプ向けに引数設定しては投げ、失敗したら次を試し、最後までエラーだったら最後のエラーを全体のエラーとして返すため、下手に内部で例外が発生すると猛烈にデバッグがやりづらい。引数処理で間違っていても最後のエラーが引数が足りないプロトタイプエラーになるため、何をいっているのかわからなくなる。

以上。

7
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?