JSON処理のコマンドラインツールであるjqは、JSONテキストを1行でちゃくっと解析、変換するときに使うものです。複雑な分岐や制御が絡んできたら、sedやawkなど他の文字列処理ユーティリティと組み合わせるのが通例です。単体で複雑なことはしません。
とは言え、jqにも変数定義、forやifなどの制御構造、関数定義などプログラミング言語らしき機能が備わっているので、「やればできんじゃねぇ」と野望を募らせてしまうこともあります。
ここではTSV(Tab-Separated Values)およびCSV(Comma-Separated values)データの解析に挑戦します。具体的には、ファイル先頭行のヘッダ(見出し)をキーに、以降のデータ行をそれぞれ値としたオブジェクトを生成します。
たとえば、次のTSVファイル
X Y Z
0001 0002 0003
0004 0005 0006
を、次のようなオブジェクトの配列にします。
[
{"X": "0001", "Y":"0002", "Z":"0003"},
{"X": "0004", "Y":"0005", "Z":"0006"},
]
これで行位置とキー名から一意に値を取得できるようになります。たとえば、(0からカウントして)1行目のXの値は次の要領で取得します。
$ echo '[{"X":"0004", "Y":0005, "Z":0006}, {"X":"0004", "Y":0005, "Z":0006}]' | jq '.[1]["X"]'
"0004"
オリジナルのお題はarc279氏の「tsv を 1行1件ずつの json にする」です。彼の解法は、ヘッダ部分を読むのにbashのビルトインコマンドreadを使い、得られたヘッダを$ENVを用いてjq内で参照します。Unixらしいツールの連携です。
TSV(タブ区切り)がCSV(カンマ区切り)という以外、本ページとほとんど同じことはshigekimono氏が「[jq] jqでcsvをjsonへ変換」で1年前にやっていました…。というわけで、タブでもカンマでも区切れるように、
splitで正規表現を用いるよう変更しました。
しかし、ここはjqをむだづかいする場所です。強引は承知ですべてをjqで記述します。
コード
コード(フィルタファイル)はtsv.jqで、次のGithubから取得できます(短いのでコピペでも十分ですが)。
https://github.com/stoyosawa/jqDoc-public/
フィルタファイルの改行はUnixスタイルのLFだけでなければならないので、保存時に注意してください。Windows流にCRLFだと次のエラーが報告されます。
jq: error: syntax error, unexpected INVALID_CHARACTER (Unix shell quoting issues?)
at <top-level>, line 1:
フィルタファイルの使い方
これらファイルをjqから呼び出すときは、次のオプションを指定します。
-
-fオプション(--from-file)ーフィルタファイルを読み込みます。 -
-Rオプション(--raw-input)ー入力行をJSONテキストではなく、単一の文字列として読み込みます。 -
-sオプション(-slurp)ー入力JSONテキストをまとめて処理します。これを-Rと組み合わせることで、入力が大きな1文字列として扱われます。
まとめると、次のようになります。
$ jq -sRf tsv.jq
TSVのオブジェクト配列化
入力データはオリジナルのお題に倣って、printfとseqとpasteから生成します。
$ { printf "c1\nc2\nc3\n"; seq -f '%04g' 30; } | paste - - -
c1 c2 c3
0001 0002 0003
0004 0005 0006
0007 0008 0009
⋮
CSVにするには、
pasteに-d,オプションを加えます。
コードは次の通りです。
split("\n") |
map(select(. != "")) |
map(split("[\\t,]"; "g")) |
.[0] as $header |
[
.[1:][] |
[$header, .] |
transpose |
map({(.[0]):.[1]}) |
add
]
行順に説明します(以下、TSVデータはファイルaに収容されているものとします)。
0 -sRオプションで入力されたTSVは次のような1文字列になります。ひとまとめにしないと、個々の行がそれぞれ独立したJSONテキストとして扱われてしまうからです(行をまたいだ構造化がされない)。
$ cat a | jq -sR '.'
"c1\tc2\tc3\n0001\t0002\t0003\n0004\t0005\t0006\n0007\t0008\t0009\n...
1 splitを用いて行単位に分解し、それぞれ要素とした配列を生成します。
$ cat a | jq -sR 'split("\n")'
[
"c1\tc2\tc3",
"0001\t0002\t0003",
"0004\t0005\t0006",
⋮
""
]
2 余計な空行があるので、それをselectで削除します。配列が入力されるので、その個々の要素を順次処理するのにmapを用います。
$ cat a | jq -sR 'split("\n") | map(select(. != ""))'
[
"c1\tc2\tc3",
"0001\t0002\t0003",
"0004\t0005\t0006",
⋮
"0028\t0029\t0030"
]
3 タブまたはカンマ区切りを、正規表現を用いたsplitでばらします。ここも配列要素単位の処理なので、mapを併用します。
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("[\\t,]"; "g"))'
[
[
"c1",
"c2",
"c3"
],
[
"0001",
"0002",
"0003"
],
⋮
]
4 ヘッダは上記配列の0番目の要素なので、.[0]に格納されています。これを変数$headerに退避します。変数定義の... as $xxxはパイプライン処理に影響を与えません。
5 以降で出力されるオブジェクト(たとえば{"c1":"0001", "c2":"0002", "c3":"0003"})を配列に収容するために、配列化の[]で処理全体をくくります。
6 配列スライス.[1:]で1行目以降のデータを取り出します。続く[]でそれぞれの要素をイテレートします。
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) |
.[0] as $header | [ .[1:][] ]'
[
[
"0001",
"0002",
"0003"
],
[
"0004",
"0005",
"0006"
],
⋮
]
7 $headerに退避したヘッダ行(の配列)とその時点のデータ要素(.)で構成された配列の配列を生成します。
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) |
.[0] as $header | [ .[1:][] | [$header, .] ]'
[
[
[
"c1",
"c2",
"c3"
],
[
"0001",
"0002",
"0003"
]
],
⋮
]
8 transposeを用いて、2行3列の配列の配列を3行2列に変換します(これは考え付かなかった!!)
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) |
.[0] as $header | [ .[1:][] | [$header, .] | transpose]'
[
[
[
"c1",
"0001"
],
[
"c2",
"0002"
],
[
"c3",
"0003"
]
],
⋮
]
9 mapを使って、[ヘッダ, 値]の配列からオブジェクトを生成します。外側の{}がオブジェクト化を意味します。キー側の.[0]はカッコ(())でくくらなければなりません。
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) |
.[0] as $header | [ .[1:][] | [$header, .] | transpose | map({(.[0]):.[1]}) ]'
[
[
{
"c1": "0001"
},
{
"c2": "0002"
},
{
"c3": "0003"
}
],
⋮
]
10 あとは、これらオブジェクトをひとつにまとめます。
$ cat a | jq -sR 'split("\n") | map(select(. != "")) | map(split("\t")) |
.[0] as $header | [ .[1:][] | [$header, .] | transpose | map({(.[0]):.[1]}) | add ]'
[
{
"c1": "0001",
"c2": "0002",
"c3": "0003"
},
{
"c1": "0004",
"c2": "0005",
"c3": "0006"
},
⋮
]
できあがりです。全体を流します。
$ { printf "c1\nc2\nc3\n"; seq -f '%04g' 30; } | paste - - - | jq -sRf tsv.jq
[
{
"c1": "0001",
"c2": "0002",
"c3": "0003"
},
{
"c1": "0004",
"c2": "0005",
"c3": "0006"
},
{
"c1": "0007",
"c2": "0008",
"c3": "0009"
},
{
"c1": "0010",
"c2": "0011",
"c3": "0012"
},
{
"c1": "0013",
"c2": "0014",
"c3": "0015"
},
{
"c1": "0016",
"c2": "0017",
"c3": "0018"
},
{
"c1": "0019",
"c2": "0020",
"c3": "0021"
},
{
"c1": "0022",
"c2": "0023",
"c3": "0024"
},
{
"c1": "0025",
"c2": "0026",
"c3": "0027"
},
{
"c1": "0028",
"c2": "0029",
"c3": "0030"
}
]
おわりに
Unixユーティリティを使えば、コードはもっと見やすくなります(分割統治というやつです)。でも、jq魂がうずいてしまったものは仕方ありません。よいお題を提供してくださったarc279氏に感謝。
参考
- tsv を 1行1件ずつの json にする - オリジナルのお題。
- [jq] jqでcsvをjsonへ変換 - このページと同じことは、1年前に実装されていました…。
-
./jq -
jqのオフィシャルサイトです(英文)。 -
Qiita jq tag - Qiita掲載の
jq関連の記事一覧です。 -
jqマニュアル(開発バージョン) - Yuji Okazawa氏が(非公式に)訳しておられる
jqマニュアルの日本語版です。和訳マニュアルは他にもあるので、検索してください。 - The JavaScript Object Notation (JSON) Data Interchange Format(RFC 8259) - JSONの仕様です(英文)。しばしばRFC 7159が参照されますが、そちらはobsoleteになりました。
- JSON(JavaScript Object Notation)データ交換フォーマット - 上記の非公式の和訳です。
-
jqハンドブックーNetOps/DevOps必携のJSONパーザ - ここで用いた
--slurpコマンドラインオプションやtransposeなどの関数の詳細を説明した書籍です。ご購入はこちらから: 【出版社 | honto | amazon.co.jp | ヨドバシカメラ】。
