jq コマンドとは
http://stedolan.github.io/jq/
JSONから簡単に値を抜き出したり、集計したり、整形して表示したりできるJSON用のgrepとかawkみたいなコマンドです。
WebサービスがJSONを吐いたり、AWS CLIが JSON を吐いたりする現代社会で大変便利なコマンドです。
マニュアル
だいたいここ読めばOK.
http://stedolan.github.io/jq/manual/
あ、これで、終わってしまう。だけど気にせず進めます。
簡単な例
まず、空気をつかみましょう。
以下jqコマンドの記法を見ていきます。JSON { "hoge": "value" }
があった場合、 .
がルート {}
を表します。.hoge
で "value"
を表現します。だいたいこんな感じです。
ただの整形
しばらく下記のJSONを例に進めます。itemsには配列がぶら下がり、その下には二つのオブジェクトが格納されてます。
$ echo '{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500}]}' \
| jq .
{
"items": [
{
"item_id": 1,
"name": "すてきな雑貨",
"price": 2500
},
{
"item_id": 2,
"name": "格好いい置物",
"price": 4500
}
]
}
値の列挙
$ echo '{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500}]}' \
| jq '.items[].name'
"すてきな雑貨"
"格好いい置物"
ダブルクォートが邪魔
別のコマンドに流すときはダブルクォートが邪魔ですね。-r
オプションで消えます。
$ echo '{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500}]}' \
| jq -r '.items[].name'
すてきな雑貨
格好いい置物
フィルタ(パイプ)
shell のパイプみたいなことが出来ます。↑と同じ事をフィルタで。↓の例は、.items[]
をフィルタに渡しているので、要素毎に次の処理が適用されます。もし、.items
とすると、要素毎では無く、配列まるごと次のフィルタに渡されます。
$ echo '{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500}]}' \
| jq -r '.items[] | .name'
すてきな雑貨
格好いい置物
集計
$ echo '{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500}]}' \
| jq '[.items[].price] | add'
7000
フィルタを通じて再整形
$ echo '{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500}]}' \
| jq '.items[] | { name: .name, yen: .price }'
{
"name": "すてきな雑貨",
"yen": 2500
}
{
"name": "格好いい置物",
"yen": 4500
}
map
$ echo '{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500}]}' \
| jq '.items | map({ name: .name, yen: .price })'
[
{
"name": "すてきな雑貨",
"yen": 2500
},
{
"name": "格好いい置物",
"yen": 4500
}
]
reduce
$ echo '{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500}]}' \
| jq 'reduce .items[] as $item (0; . + $item.price)'
7000
unique
重複を削除してユニークな(一意の)値を取得できます。振る舞いをわかりやすくするためにJSONを少し変えています。
echo '{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500},{"item_id":3,"name":"ナイスなお皿","price":4500}]}' \
| jq '[.items[].price] | unique'
[
2500,
4500
]
jq コマンドを使う場面でのちょっとしたこと
カンマは区切り文字じゃ無くて、フィルタの入力を分ける
カンマは配列やオブジェクトの要素を区切るものとしてシンタックスが用意されているのでは無く、フィルタの入力処理を分ける意味で用意されています。
$ echo '{"key1": "val1", "key2": "val2"}' \
| jq '[.key1, .key2]'
[
"val1",
"val2"
]
なので、括弧でくくれば入力を次々と再整形していくことが出来ます。
$ echo '{"key1": "val1", "key2": "val2"}' \
| jq '[.key1, (keys | map(.))]'
[
"val1",
[
"key1",
"key2"
]
]
これを理解できれば結構色々遊べるので、是非知っておいていただければと思います。
Key/Valueをオブジェクトに変換する
from_entries
AWSリソースを操作管理出来るコマンドラインツール"AWS CLI"では、リソースに付けられたタグを[{"Key": "tagkey", "Value": "value"},{"Key": "tagkey2", "Value": "value2"}]
のような形式で取得することができます。
これを別コマンドに流すとか、リソース単位でタグを元にソートするとかgroup byしたいので、{"tagkey": "value", "tagkey2": "value2"}
のような一つのオブジェクトの形式にしたいときがあると思います。
こういう場合にはjqにはfrom_entries
が用意されています。
$ echo '[{"Key": "tagkey", "Value": "value"},{"Key": "tagkey2", "Value": "value2"}]' \
| jq 'from_entries'
{
"tagkey": "value",
"tagkey2": "value2"
}
それっぽい活用事例
なんとなく、分かってきたでしょう。ここからはネットを使ったそれっぽい例を出してみたいと思います。
ある人のgithubリポジトリを列挙する
githubのAPIで取れるアカウント情報には、リポジトリ一覧を見るためのURLが格納されてるので、2回クエリを投げれば取得できます。えいえい。
$ curl -s `curl -s https://api.github.com/users/takeshinoda | jq -r .repos_url` \
| jq '.[].name'
"avr-test"
"doctree"
"HogeApp"
"rubykaigi"
"sprk2012-cflt"
"thinreports-handler"
"thinreports-rails"
東京の気温の履歴
寒くなってきましたので、CSVに気温の履歴を出します。Openweathermapという世界中の都市の気象情報をAPIで提供しているサービスがあるのでそれを利用してみます。
フォーマットは、{ ..., list: [...] }
みたいな感じで、listアトリビュート以下にそれっぽいデータが格納されてます。
$ curl -s 'http://api.openweathermap.org/data/2.5/history/city?id=1850147&type=hour' \
| jq -r '.list[] | [.dt, .main.temp_min, .main.temp_max] | @csv'
1418348513,281.15,284.15
1418350162,281.15,284.15
1418352118,281.15,284.15
1418355724,281.15,284.15
1418359309,281.15,283.15
1418361358,282.15,283.15
1418362930,282.15,283.15
1418367148,282.15,283.15
読めないですね。左からunixtime(UTC), 最低気温(ケルビン), 最高気温(ケルビン)です。適宜awkとかに喰わせて変換すれば大丈夫です。うん。ちなみに1418380960は2014年 12月12日 金曜日 19時42分40秒 JST
なのでまぁその辺のデータだとわかります。
こういう関数(Zsh)を定義しておけばターミナルのどっかに今の天気を出せますね。:sunny:
とかには みたいな絵文字があると読み替えてください><
function weather_icon() {
typeset -A weathers
weathers=(sky :sunny: clouds :cloud: rain :umbrella: thunderstorm :zap: snow :snowman: mist :foggy: )
owm=`curl -s 'http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp' \
| jq -r '.weather[0].description'`
for weather in ${(k)weathers}
do
current=`echo $owm | grep $weather`
if [ "$current" != '' ]
then
echo -n $weathers[$weather]
exit
fi
done
}
もうjqあんま関係なくなってきてますね。いろんなモノ組み合わせないと説得力出ないのはUNIXという考え方なのでいいんだと曲解して前に進みましょう。
SQL っぽく使う
全然違う話題に移ります。
あーなんかRDBにJSON配置するの面倒くさいなというときにjqをどう使うかという話。SQLと比較してみてどんな感じで書けるか見てみます。
なお、記事用のJSONを作るのが面倒くさかったので、SQLite3にデータを作って、それをJSONに変換して下記の記事を書いてます。あと、日常で私はこんなので集計したこと無いのがこの章の難点です。
例で使うデータ
商品マスタとユーザーがいて、ユーザーはいくつか商品をまとめて一つの注文するというようなテーブルです。
items_old はなんかの手違いで残ってしまったテンポラリなテーブルです。駄目ですね。
- items(item_id, name, price)
- items_old(item_id, name, price)
- orders
- item_orders(item_order_id, item_id, order_id, number)
JSONをRDBストレージとする
一つのJSONオブジェクトがデータベースになってます。冗長なので$ ./json
で出ることとします。
いやー見にくい。jq .
で整形してね。
$ ./json
{"items":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":2,"name":"格好いい置物","price":4500},{"item_id":3,"name":"間接照明","price":3400},{"item_id":4,"name":"小物入れ","price":1200}],"items_old":[{"item_id":1,"name":"すてきな雑貨","price":2500},{"item_id":5,"name":"カエルの置物","price":500}],"orders":[{"order_id":1,"user_id":1},{"order_id":2,"user_id":2},{"order_id":3,"user_id":3}],"item_orders":[{"item_order_id":1,"item_id":1,"order_id":1,"number":1},{"item_order_id":2,"item_id":4,"order_id":1,"number":2},{"item_order_id":3,"item_id":1,"order_id":3,"number":2},{"item_order_id":4,"item_id":2,"order_id":3,"number":1},{"item_order_id":5,"item_id":3,"order_id":3,"number":1}]}
わかりにくいので表にしておきます
例で使うデータだけ載せておきます。
items
item_id | name | price |
---|---|---|
1 | すてきな雑貨 | 2500 |
2 | 格好いい置物 | 4500 |
3 | 間接照明 | 3400 |
4 | 小物入れ | 1200 |
items_old
item_id | name | price |
---|---|---|
1 | すてきな雑貨 | 2500 |
2 | 格好いい置物 | 4500 |
item_orders
item_order_id | item_id | order_id | number |
---|---|---|---|
1 | 1 | 1 | 1 |
2 | 4 | 1 | 2 |
3 | 1 | 3 | 2 |
4 | 2 | 3 | 1 |
5 | 3 | 3 | 1 |
演算
Wikipediaによれば幾つかの演算ができればSQLっぽいことできるようです。
和
union
ですね。 itemsとitems_old全部でどのくらい商品あったっけ?
./json \
| jq '.items + .items_old | unique'
差
あんまり使わないですし、実装されてないRDBMSもありますが、except
ですね。items_old にあるやつを除くと?
./json \
| jq '.items - .items_old'
あ、なんか簡単にできた。
積
同じやつを抜き出します。SQLだとこれもまるで見かけないintersect
。
./json \
| jq '[.items[] as $item | .items_old[] | select($item == .)]'
やばいですね。苦しくなってきた。急に変数っぽいのでてきました。as $item
は変数です。foreachのように振る舞いますので、二つのフィルタで2重ループのような構造になってます。苦しいので手続き型っぽくなってきてます。
直積
条件を指定しないjoin
ですね。cross join
とか。
./json \
| jq '[.items[] as $item | .item_orders[] | { item: $item, item_orders: . }]'
さらに辛くなってきましたね。
テーブルを普通にひとつのオブジェクトとしてがっちゃんこすると、どのテーブルの項目か分からなくなるので、分かるようにcross join
させてみました。↓みたいな感じ。でも直積が出来れば…。
[
{
"item": {
"item_id": 1,
"name": "すてきな雑貨",
"price": 2500
},
"item_orders": {
"item_order_id": 1,
"item_id": 1,
"order_id": 1,
"number": 1
}
},
選択
where
みたいな。item_id: 1 の注文を抜き出します。
./json \
| jq '.item_orders | map(select(.item_id == 1))'
射影
select
ですね。これは簡単か。
$ ./json \
| jq '.items | map({name: .name})'
3つ以上売れた商品の名前を出す
なんとなく色々出来そうなので、SQLっぽいことをやってみたいと思います。3つ以上売れた商品の名前を出してみます。
必要そうなのはgroup by
とhaving
相当ですね。
./json \
| jq '[.items[] as $item | .item_orders[] | { item: $item, item_orders: . } | select(.item.item_id == .item_orders.item_id)] | [group_by(.item.item_id)[] | { item: .[0].item, sum: (. | map(.item_orders.number) | add) } | select(.sum >= 3) | { name: .item.name }]'
SQLだとこうなります。
SELECT items.name
FROM items, item_orders
WHERE items.item_id = item_orders.item_id
GROUP BY items.name
HAVING SUM(item_orders.number) >= 3
分解するとこういう感じになります。
1, CROSS JOIN
.items[] as $item | .item_orders[] | { item: $item, item_orders: . }
2, INNER JOIN
select(.item.item_id == .item_orders.item_id)]
3, GROUP BY
group_by(.item.item_id)
4, HAVING
{ item: .[0].item, sum: (. | map(.item_orders.number) | add) } | select(.sum >= 3)
5, SELECT
{ name: .item.name }
最早jq
関係ないですが、初心者がつまづきがちなHAVING
とWHERE
が分かれてる理由がなんとなく分かってきますね。あとSQLの偉大さが伝わってきます。
JSONを分析するときのプラクティス
@csv
を使って、CSVにはき出して、RDBMS付属のCSVローダーからRDBに入れて、素直にSQLたたくことをお勧めしますw。
$ ./json \
| jq -r '.items[] | [.item_id, .name, .price] | @csv'
1,"すてきな雑貨",2500
2,"格好いい置物",4500
3,"間接照明",3400
4,"小物入れ",1200
以上です。