カテゴリー
Jq&AwkコマンドでJSONファイル⇔CSVファイルに相互変換する方法を考察
※ 当ページには【広告/PR】を含む場合があります。
2023/06/28

遡ることだいぶ前の話題で、特定のパターンのテキストデータから「Awkコマンド」だけでゴリゴリとJSON形式のファイルを作る方法を紹介したことがありました。
その際にいろいろなAwkのテクニックを駆使して、ごりごりとJSONファイルへと変換していましたが、少し汎用性に欠ける方法かもしれません。
そこで、今回はもうちょっと使いどころが広がるように、JqとAwkコマンドで、
前置き〜Jqコマンドのテクニックの復習
本題に先立って、Jqコマンドでどちらかというとマイナーな機能である
Jqコマンドの「to_entries」
$ echo '{"a": 1, "b": 2}' | jq 'to_entries'
[
{
"key": "a",
"value": 1
},
{
"key": "b",
"value": 2
}
]
#👇入力が配列の場合、key値は0から始まる番号が割り当てられる
$ echo '[{"a": 1, "b": 2}]' | jq 'to_entries'
[
{
"key": 0,
"value": {
"a": 1,
"b": 2
}
}
]
to_enties
.a
.b
#👇cのVALUEはJSONオブジェクトのまま維持される
$ echo '{"a": 1, "b": 2, "c": {"d": 3}}' | jq 'to_entries'
[
{
"key": "a",
"value": 1
},
{
"key": "b",
"value": 2
},
{
"key": "c",
"value": {
"d": 3
}
}
]
更に深い層のフィールドが、自動で回帰的にKEY-VALUEパターンに展開されることはないことに注意してください。
Jqコマンドの「from_entries」
to_entries
$ echo '[{"key":"a", "value":1}, {"key":"b", "value":2}]' | jq 'from_entries'
{
"a": 1,
"b": 2
}
$ echo '[{"key":"a", "value":1}, {"key":"b", "value":{"c": 3}}]' | jq 'from_entries'
{
"a": 1,
"b": {
"c": 3
}
}
入力の文字列に問題がなければ、KEY-VALUEパターン配列からJSON構造へと復元することができます。
from_entries
#👇[{"a":1}]をKEY-VALUE化したもの
$ echo '[{"key": 0, "value": {"a": 1}}]' | jq 'from_entries'
jq: error (at <stdin>:1): Cannot use number (0) as object key
やろうとしてもkey値には直接数字を指定できないエラーが発生しています。 配列なのに、自由に数字を割り当てられては困るのでエラー扱いされているようです。
この場合、JSONとして配列化したい際には、
$ echo '[{"key": "a", "value": 1}]' | jq 'from_entries | [.]'
[
{
"a": 1
}
]
で戻すことができます。
また先程の
to_enties
#👇{"a":1, "b":{"c": 3}}とはならないことに注意
$ echo '[{"key":"a", "value":1}, {"key":"b", "value":[{"key":"c", "value":3}]}]' | jq 'from_entries'
{
"a": 1,
"b": [
{
"key": "c",
"value": 3
}
]
}
CSVからJSONへ変換する
ここからはCSV形式のテキストからJSON形式へ変換する方法を考えてみます。
CSVから復元できるJSONオブジェクト
まずはCSVテキストをJSONに変換する上で意識しておきたいのは、CSVから抽出できるJSONオブジェクトは「単調なJSONオブジェクトの配列」になるということです。
CSVが保持している情報はどこまでいっても行と列の2次元配列ですので、そこからより複数なデータ構造を持つJSONオブジェクトとして取り出すには、複雑な変換ルールを設ける必要があります。
この記事では面倒なことが一切考えず、CSVからJSONへ変換するのに、シンプルなルールを与えることにします。
1. CSVファイルの1行目(ヘッダ行)に変数名を定義する
2. データはCSVファイルの2行目以降から開始する
3. 空のデータのフィールドは除外し、NULLとは区別する
ということで、例えば、以下のようなCSVファイルがあったとして、
hoge,piyo,fuga,moga,buyo
120,こんにちは,3.14,ごきげんだぜ,false
54,コンバンワ,-8.07,,true
-89,てやんでぇ,1.2E-3,さいあくだ,
👇JSON変換後
[
{
"hoge": 120,
"piyo": "こんにちは",
"fuga": 3.14,
"moga": "ごきげんだぜ",
"buyo": false
},
{
"hoge": 54,
"piyo": "コンバンワ",
"fuga": -8.07,
"buyo": true
},
{
"hoge": -89,
"piyo": "てやんでぇ",
"fuga": 0.0012,
"moga": "さいあくだ"
}
]
になるような変換を目指します。
CSVからJSONへの変換スクリプトを実装する
ここでは完成版を一気に紹介します。
$ CSV_TEXT=$(cat << EOF
hoge,piyo,fuga,moga,buyo
120,こんにちは,3.14,ごきげんだぜ,false
54,コンバンワ,-8.07,,true
-89,てやんでぇ,1.2E-3,さいあくだ,
EOF
)
#👇AwkスクリプトでKEY-VALUEパターンを構築してJSONへ変換させる
$ echo "$CSV_TEXT" | awk -F"," '
#👇ヘッダ行からフィールド名(KEY)を取得
NR == 1 {
rowCount = 0;
#👇keyという配列に値をセット
for (i = 1; i <= NF; i++) {key[i] = $i;}
}
#👇データ行(2行目以降)からデータ値(VALUE)を取得
NR > 1 {
rowCount++;
for (i = 1; i <= NF; i++) {
if ($i != "") {
if ($i ~ /^[+-]?([0-9]+[.]?[0-9]*|[.][0-9]+)([eE][+-]?[0-9]+)?$/) {
#👇数値にマッチした場合
value[rowCount][i] = "{\"key\":\"" key[i] "\",\"value\":" $i "}";
}
else if (tolower($i) ~ /(true|false|null)/) {
#👇BooleanかNULLにマッチした場合
value[rowCount][i] = "{\"key\":\"" key[i] "\",\"value\":" $i "}";
}
else {
#👇その他はすべて文字列として認識
value[rowCount][i] = "{\"key\":\"" key[i] "\",\"value\":\"" $i "\"}";
}
}
}
}
END {
for (j=1;j<=rowCount;j++) {
rslt = "[";
for (i = 1; i <= NF; i++) {
#👇VALUE配列の中身が空だったらスキップ
if (value[j][i] == null) { continue; }
else if (i == NF) { rslt = rslt value[j][i]; }
else { rslt = rslt value[j][i] ","; }
}
#👇最後に不要なコンマが残っていた場合には除去
sub(/,$/, "", rslt);
print rslt "]";
}
}
' | jq 'from_entries' | jq -s '.'
#👇出力結果
[
{
"hoge": 120,
"piyo": "こんにちは",
"fuga": 3.14,
"moga": "ごきげんだぜ",
"buyo": false
},
{
"hoge": 54,
"piyo": "コンバンワ",
"fuga": -8.07,
"buyo": true
},
{
"hoge": -89,
"piyo": "てやんでぇ",
"fuga": 0.0012,
"moga": "さいあくだ"
}
]
Awkスクリプトを駆使して、Jqの
from_entries
Awkスクリプトの細かいポイントはコメントとして残していますので、そちらを参考に読み解いてみてください。
JSONからCSVへ変換
場合によっては、JSONからCSVに変換したい時があります。
後半では、JSONをCSVにするテクニックを考えてみましょう。
CSVに変換できるJSONデータの構造
こちらもJSONからCSVへ変換する前の約束事として、先ほどの節でも説明したように、全てのJSONオブジェクトがそのままCSVに変換できるわけではなく、「単調なJSONオブジェクトの配列」で表現できる時のみCSVに変換できます。
これはCSVが単なる2次元配列のデータでしかないので、より汎用性のあるJSON形式のデータの表現に制限が付くのも仕方ないことです。
もし複雑な構造をそのままデータとして保持したい場合にはJSON形式のままファイルとして保存すべきでしょう。
ここで定義するCSVデータに変換可能な「単調なJSONオブジェクトの配列」とは以下の性質を備えるものです。
1. ルート構造が配列([...])であること
2. その配列のメンバーは同一の構造を持つJSONオブジェクトで構成されいること
3. さらにそのJSONオブジェクトの値(VALUE)はプリミティブ型(文字列/数値/Bool/Null)であること
です。
例えば、
[
{"hoge": 1, "piyo": true, "fuga": "ポヨ田 ムウ夫"},
{"hoge": 4, "piyo": false, "fuga": "ガビ川 ムゲ彦"},
{"hoge": 9, "piyo": false, "fuga": "ペケ山 ムモ子"}
]
👇CSV変換後
hoge,piyo,fuga
1,true,ポヨ田 ムウ夫
4,false,ガビ川 ムゲ彦
9,false,ペケ山 ムモ子
という感じです。
変換がうまくいくポイントは、JSONオブジェクトの構造が完全な2次元配列のデータとして成立していることにあります。
JSONをCSV形式に変換する方法
先程の話を踏まえて、JSON文字列からCSVデータへの変換を考えてみます。
$ JSON_TEXT=$(cat << EOF
[
{"hoge": 1, "piyo": true, "fuga": "ポヨ田 ムウ夫"},
{"hoge": 4, "piyo": false, "fuga": "ガビ川 ムゲ彦"},
{"hoge": 9, "piyo": false, "fuga": "ペケ山 ムモ子"}
]
EOF
)
$ echo "$JSON_TEXT" | jq -r '
#①JSON配列からCSVへ変換する魔法の呪文
(.[0]|to_entries|map(.key)),(.[]|[.[]])|@csv
' | awk -F"," '
#③数字の3桁区切りを処理するオプション関数
function add_(num) {
count = split(num, arr, ".");
integer = arr[1];
if (count > 1) minority = arr[2];
while (match(integer, /[0-9]+/) > 0) {
if (RLENGTH <= 3) break;
else {
num1 = substr(integer, 1, RSTART+RLENGTH-4);
num2 = substr(integer, RSTART+RLENGTH-3);
integer = num1 "_" num2;
}
}
return count > 1 ? integer "." minority : integer;
}
NR == 1 {
#②「"」の処理
gsub(/"/,"",$0);
print $0;
}
NR > 1 {
for (i = 1; i <= NF; i++) {
if ($i ~ /^[+-]?([0-9]+[.]?[0-9]*|[.][0-9]+)([eE][+-]?[0-9]+)?$/) {
#③3桁以上の数字にアンダースコアを追加(例: 10000 --> 10_000)
printf add_($i);
} else {
#②「"」の処理
gsub(/"/,"",$i);
printf $i;
}
if (i < NF) { printf "," }
}
print ""
}
'
#👇出力
1,true,ポヨ田 ムウ夫
4,false,ガビ川 ムゲ彦
9,false,ペケ山 ムモ子
まずJqスクリプトの
①
これは、JSON配列からヘッダ付きのCSVデータを取り出すちょっとしたイディオムになっています。
以下のブログ記事にて一つ一つ何をしているのか解説していらっしゃるので、気になる方はそちらを参考にしてみてください。
このテクニックを使ってJSONからCSVに変換する際の注意点として、Jqコマンドの
「-rオプション」に関しては以前の記事で取り上げたので、ここでは割愛します。
次にAwkコマンドのスクリプト内の
②
-r
「"(ダブルクォーテーション)」
"hoge","piyo","fuga"
1,true,"ポヨ田 ムウ夫"
4,false,"ガビ川 ムゲ彦"
9,false,"ペケ山 ムモ子"
CSVファイルで保存する場合、「"」がいささか要らない場合が多いので、Awk内部で処理するならば
gsub関数
gsubさえも面倒であれば、Sedコマンドで根こそぎ削除するのもありです。
$ echo "$JSON_TEXT" | jq -r '
(.[0]|to_entries|map(.key)),(.[]|[.[]])|@csv
' | sed -r 's/"//g' | ...省略
で、だいたい話の趣旨としてここまでの内容で終わりなのですが、CSVファイルでの数字の表記法として、3桁ずつ
「_(アンダースコア)」
2934 --> 2_934
42755903478709405 --> 42_755_903_478_709_405
アンダースコアに限らず、特定の区切り文字によって大きな桁数で変換しておきたい場合に、Awkスクリプト内に定義した
③
特に数字の拡張表記が不要であれば、特に
③
まとめ
シェルスクリプトからJSONを扱うことに特化したJqコマンドと、CSVを細かく制御できるAwkコマンドを駆使することで、JSON⇔CSVの相互変換を自在に扱うための足がかり的な話をしてみました。
話のレベルとしては、JqコマンドもAwkコマンドも結構使い慣れていないと、すこし難しい内容に感じられてかも知れませんが、業務に積極的にシェルスクリプトを使ってみたい方はコツコツとこの2つのシェルコマンドのお勉強を継続してみましょう。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー