jqで少し複雑なjsonを検索+ソートする

udomomo.hatenablog.com

以前の記事で、jqを使って小さな困りごとを解決したことを書いたが、あれから実際にjqをいろいろな場面で使い始めている。とはいえ最初はけっこう試行錯誤したので、実際に使ったコマンドを忘れないように記録しておきたい。

jqとは

以前の記事でも書いたが、jqはJSONデータに特化したsedコマンドのようなもので、JSONの特定のキーの値を使った検索・ソート・置換などが簡単にできる。(以前同じようことをsedawkでやろうとしたことがあるが、かなり手間がかかったのでおすすめはしない)
JSONで吐かれる大量の生データ・ログデータを集計したいときなどに非常に重宝する。

今回はサンプルとして以下のようなJSONファイルを作ってみた。1行ごとにJSONが1つ吐き出される形式だ。

# test.json
{"x":"hoge","y":"foo","s":{"a":true,"timestamp":"1557626945"}}
{"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}}
{"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557625712"}}
{"x":"fuga","y":"foo","s":{"a":false,"timestamp":"1557626978"}}
{"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557624928"}}

特定の値で検索・置換する

基本的に、jqを使うときは操作の基準となるキーを指定すれば良い。例えば、上のファイルでyの値がbarである行だけを取り出すときは以下のようになる。

$ jq -c '. | select(.y == "bar")' test.json
{"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}}
{"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557625712"}}
{"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557624928"}}

入れ子の場合も、感覚的に指定できる。以下はaがfalseのもののみを取り出している。boolean型などもよしなに変換してくれるようだ。

$ jq -c '. | select(.s.a == false)' test.json
{"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}}
{"x":"fuga","y":"foo","s":{"a":false,"timestamp":"1557626978"}}

値を置き換える場合、新しい値を指定するだけでよい。

$ jq -c '. | select(.s.a == false) | .s.a = true' test.json
{"x":"fuga","y":"bar","s":{"a":true,"timestamp":"1557626721"}}
{"x":"fuga","y":"foo","s":{"a":true,"timestamp":"1557626978"}}

特定の値でソートする

ソートする場合、やり方が少し複雑になる。jqでは sort_by を使うことで特定のキーの値でソートできるのだが、普通にやろうとするとエラーになる。

$ jq -c '. | sort_by(.s.timestamp)' test.json 
jq: error (at test.json:1): Cannot index string with string "s"
jq: error (at test.json:2): Cannot index string with string "s"
jq: error (at test.json:3): Cannot index string with string "s"
jq: error (at test.json:4): Cannot index string with string "s"
jq: error (at test.json:5): Cannot index string with string "s"

これは、sort_byを使うときの入力値は配列でなければいけないため。
そこで、ファイルを指定する際に --slurpオプションをつけることで、入力値を配列にでき、ソートが可能になる。その後に.[]と指定すれば、今度は配列を外すことができ、出力を1行ごとに戻すことができる。

$ jq -c '. | sort_by(.s.timestamp) |.[]' --slurp test.json
{"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557624928"}}
{"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557625712"}}
{"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}}
{"x":"hoge","y":"foo","s":{"a":true,"timestamp":"1557626945"}}
{"x":"fuga","y":"foo","s":{"a":false,"timestamp":"1557626978"}}

逆に、selectを配列の状態で使うことはできない。

$ jq -c '. | select(.y == "bar") |.[]' --slurp test.json
jq: error (at test.json:5): Cannot index array with string "y"

検索とソートを一緒にやりたいときは、まずソートを行ってから、配列を外して検索すると良いだろう。

$ jq -c '. | sort_by(.s.timestamp) |.[] | select(.y=="bar")' --slurp test.json
{"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557624928"}}
{"x":"hoge","y":"bar","s":{"a":true,"timestamp":"1557625712"}}
{"x":"fuga","y":"bar","s":{"a":false,"timestamp":"1557626721"}}

これらを組み合わせることで、JSONを簡単に加工することができる。出力を確認するテスト等の時に大いに役立つだろう。