💻

JSON Lines を jq で unnest する

2022/08/06に公開

JSON Lines データを jqで unnest する方法を調べた。本日時点での「答え」は、unnest 対象のプロパティ名が foo であるとき次のようなフィルタを指定すれば良い:

del(.foo) as $x | $x + {foo: .foo[]}

unnest という操作

ここでは以下のような操作を unnest と呼ぶ。

あるプロパティの値が配列になっているデータに対して、その配列の要素それぞれが 1 行になるよう展開する操作

正直、日本語で説明するのが難しいので例を見た方が良いと思う。具体例としては、次のような JSON データを:

before
{"user":"Alice","sns":[{"type":"twitter","id":"@alice"}]}
{"user":"Bob",  "sns":[{"type":"github","id":"bob"},{"type":"twitter","id":"@bob"}]}

次のように変換する処理を unnest と呼ぶ:

after
{"user":"Alice","sns":{"type":"twitter","id":"@alice"}}
{"user":"Bob",  "sns":{"type":"github", "id":"bob"}}
{"user":"Bob",  "sns":{"type":"twitter","id":"@bob"}}
  • 上の例ではオブジェクトの配列を unnest しているけれど、数値の配列や文字列の配列(スカラーの配列)であっても同様に処理できる
  • sns の階層は不要なので中身を引き上げたい(type, iduserと同じ階層のプロパティにしたい)というケースもあると思うけれど、データによってはプロパティ名が重複して問題になるかもしれないため、ここでは汎用的に使える方法を書いている
    • もっとも unnest 後に sns の中身を引き上げるのは簡単なので、もう一度 jq でフィルタしても良いのではと思う

動作確認環境

詳細説明

まず、unnest は次のような処理だと考える:

  1. unnest したいプロパティの値(配列)の各要素を取り出して 1 行 1 行に展開する
  2. その結果に、その他のプロパティを結合する

少し複雑なので、例のデータを使いつつ順を追って解いていく。まずステップ 1 については、そのための機能である .[] (Array/Object Value Iterator) が使える:

$ cat data.json | jq -c '.sns[]'
{"type":"twitter","id":"@alice"}
{"type":"github","id":"bob"}
{"type":"twitter","id":"@bob"}

ただし .[] は要素を取り出すため「unnest 対象のプロパティの値」という形ではなくなっている。これでは後で結合するときに都合が悪いため {} (Object Construction) で元の構造を再現しておく:

$ cat data.json | jq -c '{sns: .sns[]}'
{"sns":{"type":"twitter","id":"@alice"}}
{"sns":{"type":"github","id":"bob"}}
{"sns":{"type":"twitter","id":"@bob"}}

続いてステップ 2。まず「その他のプロパティ」だけのオブジェクトを作るため、unnest 対象のプロパティを del 関数で削除した結果を変数に格納する:

$ cat data.json | jq -c 'del(.sns) as $x | $x'
{"user":"Alice"}
{"user":"Bob"}

これとステップ 1 の結果を、+ (Addition Operator)で結合する:

$ cat data.json | jq -c 'del(.sns) as $x | $x + {sns: .sns[]}'
{"user":"Alice","sns":{"type":"twitter","id":"@alice"}}
{"user":"Bob","sns":{"type":"github","id":"bob"}}
{"user":"Bob","sns":{"type":"twitter","id":"@bob"}}

以上で完成。

参考:数値や文字列値の配列を展開する場合

まったく同じ考え方で処理できる。たとえば次のようなデータがあったとき:

before
{"user":"Alice","interests":["user-interface", "interactive-design"]}
{"user":"Bob",  "interests":["programming", "data-science"]}

次のように処理できる:

cat scalars.json| jq -c 'del(.interests) as $x | $x + {interests: .interests[]}'
{"user":"Alice","interests":"user-interface"}
{"user":"Alice","interests":"interactive-design"}
{"user":"Bob","interests":"programming"}
{"user":"Bob","interests":"data-science"}

Discussion