jqfpyのgetを強化してみた([],*,*[])

github.com

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/xmembers/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