jq コマンドを使う日常のご紹介

  • 1118
    Like
  • 2
    Comment
More than 1 year has passed since last update.

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": "velue" } があった場合、 . がルート {} を表します。.hoge"value" を表現します。だいたいこんな感じです。

サンプル de-ta

ただの整形

$ 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
    }
  ]
}

しばらく↑のJSONを例に進めます。itemsには配列がぶら下がり、その下には二つのオブジェクトが格納されてます。

値の列挙

$ 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

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": "tagkey", "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:とかには :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 byhaving相当ですね。

./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関係ないですが、初心者がつまづきがちなHAVINGWHEREが分かれてる理由がなんとなく分かってきますね。あと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

以上です。