Pyparsing を学ぶ


# 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](http://pyparsing.wikispaces.com/)

だそうです。O'Reilly の本もあるそうなので金持ちは買えばいいと思う。ぱっと探してみて日本語の情報がなさそうなので書く。Python 2.7環境で試しているため、現代的ではない可能性がある。pyparsing のバージョンは 2.0.2 を使用した。

# 使い方

* 個別のパーザをつくる
* パーザを連結しておおきなパーザをつくる
* パーザの`.parseString`に文字列を入力し結果を評価する

以上。

# パーザをつくる

パーザは2通りの作り方がある。1つは `ParserElement` 派生クラスのインスタンスから作る方法、もうひとつはモジュールからのジェネレータファンクションを使って適切な `ParserElement` 作る方法。

```python
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`は受け付けない)

```python
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つめが情報量(衛星の数))
 
```python
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` で関数を試す

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