小技

日々の仕事の中で調べたり発見したりしたことを書き留めていくブログ

jq で CSV 出力する

jq を使って JSON を整形したところから、CSV 出力するまでに意外と手こずることが多いので整理してみました。

CSV 出力自体は、フィルタの末尾に | @csv をつけるだけで容易に行うことができます。 そう思って、下記のような JSON からそのまま CSV 出力しようとすると、エラーになってしまいます。

sample.json

[
  {"name": "foo", "ip": "192.168.0.1"},
  {"name": "bar", "ip": "192.168.0.2"},
  {"name": "baz", "ip": "192.168.0.3"}
]

実行結果

$ jq -r '. | @csv' sample.json
jq: error (at sample.json:5): object ({"name":"fo...) is not valid in a csv row

@csv による CSV 出力

jq のマニュアル を見ると、@csv の入力は配列である必要があることがわかります。

@csv:
The input must be an array, and it is rendered as CSV with double quotes for strings, and quotes escaped by repetition.

例えば、下記のように配列を渡すと、CSV として出力されます。

$ echo '[1, 0, 2]' | jq  -r '. | @csv'
1,0,2

複数行の CSV を出力するには、単純に複数の配列を入力として渡します。

$ echo '[1, 0, 2] [4, 3, 5]' | jq .
[
  1,
  0,
  2
]
[
  4,
  3,
  5
]
$ echo '[1, 0, 2] [4, 3, 5]' | jq -r '. | @csv'
1,0,2
4,3,5

上記は配列の配列とは異なることに注意が必要です。

$ echo '[[1, 0, 2], [4, 3, 5]]' | jq .
[
  [
    0,
    1,
    2
  ],
  [
    3,
    4,
    5
  ]
]

$ echo '[[1, 0, 2], [4, 3, 5]]' | jq -r '. | @csv'
jq: error (at <stdin>:1): array ([1,0,2]) is not valid in a csv row

これはエラーとなります。 外側の配列を CSV の一行として出力しようとしているようです。 下記のように外側の配列を外してやるとうまく出力できるようになります。

$ echo '[[1, 0, 2], [4, 3, 5]]' | jq .[]
[
  1,
  0,
  2
]
[
  4,
  3,
  5
]
$ echo '[[1, 0, 2], [4, 3, 5]]' | jq -r '.[] | @csv'
1,0,2
4,3,5

つまり、@csv の入力は(1 つまたは複数の)ネストされていないフラットな配列である必要があります。

CSV 出力するための JSON の変形

冒頭の sample.json の場合、以下 2 点の変更が必要です。

  1. 外側の配列を外す。
  2. CSV の一行がオブジェクトではなく、一つのフラットな配列になるようにする。

1. については容易に行うことができます。

jq .[] sample.json
{
  "name": "foo",
  "ip": "192.168.0.1"
}
{
  "name": "bar",
  "ip": "192.168.0.2"
}
{
  "name": "baz",
  "ip": "192.168.0.3"
}

2. については、それぞれのオブジェクトをキー name および ip の値の配列とする必要があります。 これも下記のように行うことができます。

jq '.[] | [.name, .ip]' sample.json
[
  "foo",
  "192.168.0.1"
]
[
  "bar",
  "192.168.0.2"
]
[
  "baz",
  "192.168.0.3"
]

あるいは、最初から CSV での出力が目的なら、整形の段階でオブジェクトではなく、配列として整形したほうがいいのかもしれません。

多数のキーを持つオブジェクトの値の配列化

キーが 2, 3 個なら手書きで列挙することも容易ですが、数が多くなってくるとそうもいきません。 オブジェクトのキーの一覧を取得する関数として keys がありますが、同様に values としてもうまく行きません。

$ jq '.[0] | keys'  sample.json
[
  "ip",
  "name"
]
$ jq '.[0] | values'  sample.json
{
  "name": "foo",
  "ip": "192.168.0.1"
}

結論から言うと、オブジェクトに対してもフィルタ .[] を通してやることで、値を列挙することができます。

$ jq '.[0] | .[]'  sample.json
"foo"
"192.168.0.1"

(2023/1/22 追記) jq のマニュアル によると、.[] はオブジェクトに対しても使うことができ、オブジェクトに含まれる全ての値を返す、とあります。

Array/Object Value Iterator: .
If you use the .[index] syntax, but omit the index entirely, it will return all of the elements of an array. Running .
with the input [1,2,3] will produce the numbers as three separate results, rather than as a single array.
You can also use this on an object, and it will return all the values of the object.

この時の出力は配列になっていないので、@CSV に渡す場合は出力を [] で囲んで配列にする必要があります。

$ jq '.[0] | [.[]]'  sample.json
[
  "foo",
  "192.168.0.1"
]

まとめ

JSONCSV として出力するために @csv を使う場合、各行をフラットな配列として渡してやる必要があります。

下記のような JSON はそのままでは CSV として出力できません。

sample.json

[
  {"name": "foo", "ip": "192.168.0.1"},
  {"name": "bar", "ip": "192.168.0.2"},
  {"name": "baz", "ip": "192.168.0.3"}
]

以下のようにフィルタを通してやる必要があります。

$ jq -r '.[] | [.[]] | @csv'  sample.json
"foo","192.168.0.1"
"bar","192.168.0.2"
"baz","192.168.0.3"

参考