jqを活用してAPIレスポンスから欲しい情報を抽出する【上級編】

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

よく訓練されたアップル信者、都元です。引き続きjqのお話。本日は上級編です。

まずおさらいの意味も込めて下記は、ざっと読んでおいて頂けると。

前回の練習問題の答え

さて、まず前回の練習問題の答えをさらっと。1つ目の解答例は下記の通りです。「どっと・ふー・むきむき」と読んでください。

$ cat example.json | jq '.foo[][]'

2つ目の解答例はこんな感じでしょうか。

$ cat example.json | jq '[.foo[][] | {(.key): .value}] | add'

jqにおける関数

さて。

ある値をから別の値を作る操作のことを関数(function)と呼びます。例えば今まで見てきたjqのクエリはそれぞれ全てが(あるJSONから別のJSONを作る)関数です。関数を使って入力値から出力値を計算することを、関数の適用(apply)と呼びます。

  • .は、入力値をそのまま出力する関数(恒等関数)です。
  • .coordは、入力値の一部分だけを抽出したものを出力する関数です。
  • .coord.lonは、.coordを適用した後に.lonを適用する関数(合成関数)です。つまり | というのは関数を合成する演算子だったのです。

さてここで、例えば「値に10を足す」という関数があったとします。2に対してこの関数を適用すると12ですね? jqではこの関数を. + 10と書きます。コマンドはこんな感じです。

$ echo 10 | jq ". + 10"
20

jqのクエリというのは、最小の構成単位が関数で、それが複雑に合成されて、その結果として目的の関数ができているんだなぁ、ということを感じてください。

select関数

配列中の多くの要素の中から、特定の条件を満たすものだけを取り出す操作を絞り込み(filter)と呼びます。例えば、 [3,-1,6,1000,19] という配列の中から、0以上100未満という条件で絞り込みをすると、結果は [3,6,19] となります。このセクションではこの絞り込み操作をゴールとしたいと思いますが、それを実現する要素を一つ一つ確認しましょう。

この処理をする前に、この「0以上100未満という条件」を判定する関数を考えましょう。. >= 0 and . < 100ですね。andは初出ですがまぁわかりますね? 他にも ornot もあります。

さて、この関数を値に適用してどのような結果となるか見てみましょう。

$ echo -10 | jq '. >= 0 and . < 100'
false
$ echo 10 | jq '. >= 0 and . < 100'
true
$ echo 100 | jq '. >= 0 and . < 100'
false
$ echo 99 | jq '. >= 0 and . < 100'
true
$ echo 1000 | jq '. >= 0 and . < 100'
false

うん、期待通り動いていそうですね。さてここで、値に対してとある関数を適用した時、結果が真(true)となったら元の値をそのまま出力、結果が偽(false)となったら元の値は捨てて空(empty)の出力を行う、という関数selectの登場です。jqの記述としては、select(. >= 0 and . < 100)です。

$ echo -10 | jq 'select(. >= 0 and . < 100)'
$ echo 10 | jq 'select(. >= 0 and . < 100)'
10
$ echo 100 | jq 'select(. >= 0 and . < 100)'
$ echo 99 | jq 'select(. >= 0 and . < 100)'
99
$ echo 1000 | jq 'select(. >= 0 and . < 100)'

分かるでしょうか。1つ前の例で結果がtrueになっていたものだけ入力値がそのまま出力され、falseだったものは空出力になっています。今ひとつ理解が追いつかない方は、もう少し冗長で直感的な、下記と見比べてみてください。if . >= 0 and . < 100 then . else empty endselect(. >= 0 and . < 100) と等価な関数です。

$ echo 99 | jq 'if . >= 0 and . < 100 then . else empty end'
99
$ echo 1000 | jq 'if . >= 0 and . < 100 then . else empty end'

さてここで絞り込みに戻ります。ここまでの知識がしっかり身についていれば、あとは絞り込みができます。まず[3,-1,6,1000,19]という入力に対して皮むき(配列の展開)して、select関数を適用した後、その結果をArray construction(配列の構築)で配列にすればいいですね。って下記の実例を見てしまうのが早いと思います。

$ echo '[3,-1,6,1000,19]' | jq -c '[.[] | select(. >= 0 and . < 100)]'
[3,6,19]

空出力の値は、Array construction時の要素からは除外されるので、結果的に絞り込みができました。

map関数

初級編でも「上級編な話」として一瞬触れたmap関数について。mapとは日本語で言うと写像です。

mapは、配列に対して、その全ての要素にそれぞれ関数を適用し、結果の配列を得る、という関数です。 つまり[3,6,19]という配列に対するこの関数の写像は? [13,16,29]というわけです。

$ echo '[3,6,19]' | jq -c 'map(. + 10)'
[13,16,29]

おや、そういえば上記の絞り込みの処理は「配列に対して、その全ての要素にそれぞれselect関数を適用し、結果の配列を得る」というものでしたね。これもmap処理じゃないでしょうか? その通りです。前述のフィルター処理はこのように書けます。

$ echo '[3,-1,6,1000,19]' | jq -c 'map(select(. >= 0 and . < 100))'
[3,6,19]

さらにmap関数を|で合成して…

$ echo '[3,-1,6,1000,19]' | jq -c 'map(select(. >= 0 and . < 100)) | map(. + 10)'
[13,16,29]

というふうに、最初の入力値[3,-1,6,1000,19]を次々に変換していきます。

map(function)という記述は[.[] | function]の別の書き方と考えて良いでしょう。

reduce関数

はい、filter・mapと来たら次はもちろんreduceですね。例えば、配列の全要素の総和(Σ)や相乗(Π)を求めたい、という話です。具体的には、[13,16,29] の総和は13+16+29で58です。相乗は13×16×29で6032です。

えーと、これ説明が難しいので、まず答えを示します。ちょっと複雑な関数ですね。

$ echo '[13,16,29]' | jq 'reduce .[] as $item (0; . + $item)'
58
$ echo '[13,16,29]' | jq 'reduce .[] as $item (1; . * $item)'
6032

プログラミングの経験のある方がこれを見れば、.[]によって展開された値を順次$itemに代入しながらループを回すんだな、という理解ができると思います。そして最後の括弧の中身ですが、;の手前にある値(0や1)のことを単位元(identity element)と呼びます。;の後ろにある関数を累積関数(accumulatorやaccumulation function)と呼びます。

単位元とは、行いたい計算の片方の引数に与えたとき、もう一方の引数がそのまま返るような値です。総和では、行いたい計算は「和」つまり足し算です。とある数字aに足した時、結果がそのままaとなるような値は? 0ですね。なので、和の単位元は0です。相乗では、行いたい計算は「積」つまり掛け算です。とある数字aに掛けた時、結果がそのままaとなるような値は? 1ですね。なので積の単位元は1です。

累積関数というのは、配列の各要素を使って作る関数で、「前の累積関数適用結果」を入力値として関数適用を行います。ただし、最初は「前の累積関数適用結果」が存在しません。その時は単位元を利用します。。。わかりにくいですね。。。具体的な動きを下記にまとめました。下記は総積の例です。

  1. まず、初期値を単位元(1)とします。
  2. この値に対して、. * $itemを適用します。最初の適用時には$item13なので、具体的には. * 13を適用することになり、その結果は13です。
  3. 次に、この13に対してさらに、. * $itemを適用します。この時$item16なので、結果は208です。
  4. 最後にこの208に対して、. * 29を適用することになり、最終結果は6032となります。

難しいですかね…。にもかかわらず練習問題。

  1. 「文字列と文字列をつなぐ関数」があったとします。つまり"foo"+"bar"から"foobar"を作る+という関数です。この関数の単位元はなんでしょう?
  2. ["foo", "bar", "baz"]から"foobarbaz"を得る関数を、reduce関数を用いて実装してください。つまり下記です。
$ echo '["foo", "bar", "baz"]' | jq 'reduce 【ここを埋めよ】'
"foobarbaz"

最後に、ここまでのfilter・map・reduceの流れを一気に書くと…。

$ echo '[3,-1,6,1000,19]' | jq -c '
    map(select(. >= 0 and . < 100))
    | map(. + 10)
    | reduce .[] as $item (1; . * $item)'
6032

こんな感じです。いわゆる MapReduce と呼ばれるプログラミングモデルで関数が記述できました!

まとめ

今回は上級編として、jqにおける関数の概念を再確認しました。その上でfilter・map・reduceという、MapReduceプログラミングモデルの構成要素をそれぞれ説明しました。

実は上級編では、AWS CLIの出力を複雑に処理していく実例を示そうと思っていたのですが、これにはfilterとmapをしっかり身につけなければ難しい、ということになり、ついでにreduceまで説明してしまいました。

というわけで、次回こそ最終回【エキスパート実践編】としてお送りする予定です。お楽しみに。