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

169件のシェア(すこし話題の記事)

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

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

本日のお題は、このような入力値 example.json を利用します。

{
  "foo": {
    "bar": [
      {
        "key": "1-key",
        "value": "1-value"
      },
      {
        "key": "2-key",
        "value": "2-value",
        "option": "2-opt"
      }
    ],
    "baz": [
      {
        "key": "3-key",
        "value": "3-value",
        "option": "3-opt"
      },
      {
        "key": "4-key",
        "value": "4-value"
      }
    ]
  }
}

配列の皮むきとオブジェクトの皮むき

初級編では「配列の展開」として.[]という記述を解説しました。

$ cat example.json | jq '.foo.bar'
[
  {
    "key": "1-key",
    "value": "1-value"
  },
  {
    "key": "2-key",
    "value": "2-value",
    "option": "2-opt"
  }
]
$ cat example.json | jq '.foo.bar[]'
{
  "key": "1-key",
  "value": "1-value"
}
{
  "key": "2-key",
  "value": "2-value",
  "option": "2-opt"
}

このように配列を展開してJSONに展開する技です。どうでも良い話ですが、私は個人的に頭の中で「配列の皮むき」って呼んでます。どうでもいいですね。

で、配列は上記のように剥けるんですが、稀にオブジェクトも皮を剥きたい時があります。

$ cat example.json | jq '.foo.bar[1]'
{
  "key": "2-key",
  "value": "2-value",
  "option": "2-opt"
}

ここから、下記のような出力を得たい時。(オブジェクトの皮をむくと、キー情報は失われます)

"2-key"
"2-value"
"2-opt"

実は、これも配列と同じように .[] でムキムキできます。.foo.bar[1] | .[] ですね。つまり略記で .foo.bar[1][] こう。

さて、ここで練習問題です。example.json から下記のような出力を得るためのクエリを書いてみてください。

{
  "key": "1-key",
  "value": "1-value"
}
{
  "key": "2-key",
  "value": "2-value",
  "option": "2-opt"
}
{
  "key": "3-key",
  "value": "3-value",
  "option": "3-opt"
}
{
  "key": "4-key",
  "value": "4-value"
}

ワンライナー中にjqを複数呼びたい誘惑

さて、ある程度jqの扱いに慣れてくると、1行でコマンドで全ての処理を記述して、一気に得たい結果を得る、所謂ワンライナーを使いはじめると思います。その時、jqコマンドの出力を再びjqコマンドに放り込む、つまり下記のようなコマンドを書いてしまうことがあります。

$ cat example.json 
  | jq '.foo.bar[]' 
  | jq '.key, .value'
"1-key"
"1-value"
"2-key"
"2-value"

jqのプロセスを一度完走させ、あたまの中を整理してあらためて次のプロセスを走らせる、という意味で、決して間違った解法ではありません。が、このような例はそれぞれのクエリを | で繋いでしまえば、1つのjqプロセスで処理できます。

$ cat example.json | jq '.foo.bar[] | .key, .value'

読みやすさにも関連するので、その時々の応じて良さそうな方を採用しましょう。

各行のダブルクオートを取り除きたい

さて、上記の結果は、各行がダブルクオート(")で囲まれています。結果が、文字列の"1"なのか、数値の1なのかは場合によっては重要ですので、このように文字列であればそれを明確に出力するというのは大切な仕様です。

とは言え、要らない時は要らないものです。そんな場合は、jqのコマンドラインオプション-r--raw-output)を使いましょう。思い通りの結果が得られると思います。

$ cat example.json | jq -r '.foo.bar[] | .key, .value'
1-key
1-value
2-key
2-value

ちなみに、-rオプションを利用すると、文字列中のエスケープ文字列(改行等)も実際の改行に置き換わって出力されます。

$ echo '"FOO\nBAR"' | jq '.'
"FOO\nBAR"
$ echo '"FOO\nBAR"' | jq -r '.'
FOO
BAR

JSONを1行で表示したい

1-key
1-value
2-key
2-value

さて、上記の例の結果は、example.json内のセマンティクスでは2行ずつのペアとして意味を持ちます。"1-key""1-value"には深いつながりがあり、"1-value""2-key"の間にはそれほど深いつながりはありません。

このような時、結果を .key, .value としてカンマで並列させるのではなく、Array construction(配列構築)またはObject construction(オブジェクト構築)をすると良いでしょう。

$ cat example.json | jq '.foo.bar[] | [.key, .value]'
[
  "1-key",
  "1-value"
]
[
  "2-key",
  "2-value"
]
$ cat example.json | jq '.foo.bar[] | {key, value}'
{
  "key": "1-key",
  "value": "1-value"
}
{
  "key": "2-key",
  "value": "2-value"
}

さてこの時。要素数の少ない配列やオブジェクトであれば、わざわざこのようにインデントを付けてフォーマットしてくれなくても…と思うこともあります。そんな時は、jqのコマンドラインオプション -c--compact-output) を使いましょう。

$ cat example.json | jq -c '.foo.bar[] | [.key, .value]'
["1-key","1-value"]
["2-key","2-value"]
$ cat example.json | jq -c '.foo.bar[] | {"key": .key, "value": .value}'
{"key":"1-key","value":"1-value"}
{"key":"2-key","value":"2-value"}

コンパクトで見やすくなりましたね!

さらにオブジェクトの方は、"key": .key"value": .value という表現が冗長だなぁと感じます。これは下記のように、dotを使わずにダイレクトにプロパティ名を記述することにより、同じ出力が得られます。

$ cat example.json | jq -c '.foo.bar[] | {key, value}'

値をキーに使いたい

日本語での説明が難しいですw 下記に実例を。

{"key":"1-key","value":"1-value"}
{"key":"2-key","value":"2-value"}

は冗長だなぁ…。

{"1-key":"1-value"}
{"2-key":"2-value"}

という感じがいいなぁ、と思ったとします。そこで下記のようなクエリを書きました。

$ cat example.json | jq -c '.foo.bar[] | {.key: .value}'

error: syntax error, unexpected FIELD
.foo.bar[] | {.key: .value}              1 compile error

oh... ここはこのように、キー側を括弧で括ってやらなきゃいけないようです。

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

{"1-key":"1-value"}
{"2-key":"2-value"}

JSON列を1つのJSONオブジェクトにマージしたい

さて直前で得られた結果はJSONでした。どちらのオブジェクトもキーが重なっていないので、これを1つのオブジェクトにマージした結果が欲しいことがあります。つまり、こう。

{
  "1-key": "1-value",
  "2-key": "2-value"
}

この場合、JSON列をまず配列に変え [.foo.bar[] | {(.key): .value}] 、その後にaddという組み込み関数を使ってオブジェクト同士を足し合わせることで、マージができます。

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

{
  "1-key": "1-value",
  "2-key": "2-value"
}

もう一つ練習問題。先ほどの練習問題で得た結果をさらに加工して、下記の結果を得てみてください。

{
  "1-key": "1-value",
  "2-key": "2-value",
  "3-key": "3-value",
  "4-key": "4-value"
}

練習問題の答え

模範解答はとりあえず書かないでおきます。頑張ってみてください。この流れでもう1エントリー【上級編】を予定していますので、模範解答はそちらで説明しようと思います。