jqfpyのgetを強化してみた([],*,*[])
jqfpyのgetを強化してみた。具体的には、<attirubte>[]
,*
,*[]
という表記が増えた。
ショートカット用の機能が増えたので、学習コストは上がっていき(?)、当初の想定とは異なりどんどんjqに近づいていっているということがないわけではないのだけれど。
(当初の想定というのはこのあたりの話)
今回は、サンプルのJSONとしてelastic searchのsearch APIのレスポンスを使うことにする。
こういうもの。
{ "took": 1, "timed_out": false, "_shards":{ "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits":{ "total" : 2, "max_score": 1.3862944, "hits" : [ { "_index" : "twitter", "_type" : "tweet", "_id" : "0", "_score": 1.3862944, "_source" : { "user" : "kimchy", "message": "trying out Elasticsearch", "date" : "2009-11-15T14:12:12", "likes" : 0 } }, { "_index" : "twitter", "_type" : "tweet", "_id" : "1", "_score": 10.3862944, "_source" : { "user" : "someone", "message": "hmm", "date" : "2009-11-15T14:12:12", "likes" : 1000000000000000 } } ] } }
ちょっと長い。
もともとの機能は1つの要素のためのもの
もともとのget()
は、ネストした辞書のアクセスを手軽に行えるためのもの。
例えば、1つ目のtweetのuserを取りに行くようなコードは以下のような感じ。
$ cat 00data.json | jqfpy 'get("hits/hits/0/_source/user")' "kimchy"
数値として利用可能な文字列が渡された場合には数値として認識されるので1つ目の値などは取ることができる。
ただ、もとのデータに戻って見てみると複数ある。具体的には2つあるのだけれど。この2つの全部の名前を取ってこなくちゃいけない場合にこれでは困る。
$ cat 00data.json | jqfpy --squash 'L = get("hits/hits"); [get("_source/user", d=d) for d in L]' "kimchy" "someone"
そうは言っても、結局ただのpythonなのでリストになる部分を変数に束縛して、リスト内包表記などでどうにかなる話ではあるのだけれど。やっぱりめんどう。
新たに増えた機能は複数の要素のためのもの
そんなわけで複数の候補を全部取りたいみたいな部分の処理をそのまま書けるようにした。
$ cat 00data.json | jqfpy --squash 'get("hits/hits[]/_source/user")' "kimchy" "someone"
json pointerとは何だったのかみたいになってしまったけれど。まぁ便利なので。。 (少しだけ言い訳させてもらうと、既存のpythonの知識のみで書き下すことができてかつ便利なショートカットがあるものと簡潔な記法のみが存在するものとの違いを意識してほしい)
任意の何かを選択したいという場合もある
あともう一つ増えたのは*
という記法。何か名前は忘れたけれど次の階層ではnameを持っているみたいなものを指定したいような場合がある。そのようなときに今までは少し困っていた。
例えば、先程の1つ目のユーザーの代わりに1つ目のメッセージを抽出したいような場合に、_source
部分が思い浮かばなかったとする。そのようなときには以下の様に書けるようになった。
$ cat 00data.json | jqfpy 'get("hits/hits/0/*/message")' "trying out Elasticsearch"
もちろん全部取る場合には以下の様に組み合わせれば良い。
$ cat 00data.json | jqfpy --squash -r 'get("hits/hits[]/*/message")' trying out Elasticsearch hmm
補足1
添字でのアクセスとの対称性を考えて以下のような記法を許すという形も良いのかもしれない(これはまだ考えているだけ)。
以下を同じものとするということ。
get("hits/hits[]/*/message") get("hits/hits/[]/*/message")
補足2
実は任意のみたいに考えてしまうとちょっと問題がある。それは候補が1つに限らないということになってしまうということ。
これはあんまり嬉しくない。[]
というような注釈がないにもかかわらずアクセスした結果返ってくる値の数が変わるというのは嬉しくない。例えば、members/*/name
というキーに対して、members/x
もmembers/y
もnameを保持していた場合には、その2つの内のどれかということ。
そんなわけで、任意の次のキーを持つ最初の要素ということにした。
ちなみに真面目にバックトラックで探索のようなことをしていないので次のキーしかみていない。(例えば、foo/*/bar/baz
があったとしてbarが存在することは保証するけれど。そのbarがbazを持っているかは保証していない)
任意の何かを全部欲しいという場合もある
もちろん任意の何かを全部欲しいという場合もある。例えば以下のようなJSONがあったとして(実はjqfpyのreadmeにあるものと同じもの)、
{ "apps": { "foo": { "use": true }, "bar": { "use": true }, "boo": { "use": true }, "bee": { "use": false } } }
あんまり良い例ではないけれど、useの値がほしいとする。そのような時に*
だけではちょっと困る。
$ cat 02data.json | jqfpy 'get("apps/*/use")' true
全部を取り出したい。そういうときには*[]
をつかう。ちょっとキモい見た目だけれど。
$ cat 02data.json | jqfpy --squash 'get("apps/*[]/use")' true true true false
おまけ
とはいえ、上の例のようにuseだけ見ようということはあんまりないだろうし名前もほしいとなることが多い。それなら普通にpythonを書いたほうが良いのでは?みたいな話も一方であります(拡張されたget()の利用は計画的に)。
$ cat 02data.json | jqfpy --squash -r '["{k}:{v[use]}".format(k=k,v=v) for k, v in get("apps").items() if "use" in v]' foo:True bar:True boo:True bee:False
あと、お試しでltsvでの出力もサポートしたので、key-vaueを一行でまとめて出力されてほしいみたいな場合にそちらを使っても良いのかも。
$ cat 02data.json | jqfpy --squash -o ltsv '[{"name": k, "use": v["use"]} for k, v in get("apps").items() if "use" in v]' name:foo use:True name:bar use:True name:boo use:True name:bee use:False