mygems

JSONをCSVに変換する方法:データを失わないために

JSONはツリー、CSVは表。変換ツールは必ず何かを選びます。その判断と、入れ子のJSONを平らにする方法、向いているツールを紹介します。

執筆: mygems ·

JSONはツリーで、CSVは長方形の表です。一方から他方へ変換するには、ツリーをどう平らにするかを決める必要があります。どの変換ツールもその判断を代わりに行いますが、何を決めたのかは言わないことがほとんどです。同じファイルを3つのオンライン変換ツールに通すと、3通りの表ができることがあるのはそのためです。

3つの判断を知っておけば、思いどおりに判断してくれるツールや設定を選べます。

先に結論

  • オブジェクトのフラットな配列([{"id":1,"name":"Ada"}, …]):どの変換ツールでも問題ありません。jq、Python、DuckDB、Excel、アプリのどれでも使えます。
  • 入れ子のオブジェクト("address": {"city": "Leeds"}):address.city のようにドット区切りの列へ展開します。Pythonの json_normalize なら1行です。
  • 各レコードの中にある配列("tags": ["a","b"]):要素ごとに列を作る、要素ごとに行を作る、1つのセルに連結する、のどれかを選びます。正解はなく、変換後の使い道に合うものが正解です。
  • レコードが最上位にない場合: まず配列を探し(たいてい data、items、results)、その配列を変換します。

判断1:どの配列を行にするか

CSVには、似たもののリストが1行に1件ずつ必要です。実際のJSONで最初からそうなっているものはほとんどありません。APIのレスポンスは次のような形です。

{
  "status": "ok",
  "page": 1,
  "data": {
    "items": [
      { "id": 1, "name": "Ada", "address": { "city": "Leeds" }, "tags": ["admin"] },
      { "id": 2, "name": "Lin", "address": { "city": "Hue" }, "tags": [] }
    ]
  }
}

行になるのは data.items です。ルートを指定した変換ツールはオブジェクトが1つだけと見なし、status、page、そしてエスケープされたJSONが詰まった列を持つ1行を返します。「変換ツールが壊れている」という苦情で最も多いのがこれで、原因は変換ツールではなくパスの指定です。

判断2:入れ子のオブジェクトをどうするか

address は各レコードの中にあるオブジェクトです。たいていの用途では、ドット区切りの列名に展開するのが普通で、正しい選択です。

idnameaddress.city
1AdaLeeds
2LinHue

もう1つの方法は、オブジェクト全体をJSON文字列として1つのセルに入れることです。情報はすべて残りますが、並べ替えもフィルタもできない列になります。

判断3:レコード内の配列をどうするか

tags はリストで、リストは1つのセルには収まりません。方法は3つあります。

方法結果向いている用途
インデックス列tags[0]、tags[1]、…最大数が分かっている短いリスト
行に展開タグごとに1行、他の列は繰り返しタグでの集計やフィルタ
1つのセルに連結admin;editor読むだけで分析しない場合

展開すると行数が変わります。タグが3つある人は3行になります。「各タグを持つ人は何人か」を調べるなら正しく、「全部で何人いるか」を調べるなら間違いです。選ぶ前に、そのCSVでどんな問いに答えたいのかを決めておきましょう。

そのほかに失われるもの

完璧に平らにしても、CSVでは表現できないものが失われます。

  • 型。 "42" も 42 も 42 になります。true は文字列の true になります。
  • nullと空。 null、""、キーがない場合は、どれも空のセルになります。
  • まばらなレコードの列。 10,000件のうち1件だけに coupon フィールドがある場合、最初のレコードのキーからヘッダーを作る変換ツールはその列を落とします。良い変換ツールは、全レコードのキーの和集合を使います。

jqの場合

最上位がフラットな配列のとき:

jq -r '(.[0] | keys_unsorted) as $k | $k, (.[] | [.[$k[]]]) | @csv' in.json > out.csv

これは最初のレコードからヘッダーを作るので、最初のレコードにないキーは落ちます。上で触れた「まばらなレコード」の問題です。配列がもっと深い場所にある場合は、最初の . を .data.items のようなパスに置き換えます。入れ子のデータもjqで処理できますが、式が読めなくなってきます。そのときはPythonを使いましょう。

Pythonの場合

pandas.json_normalize は3つの判断を1回の呼び出しで行い、キーの和集合も取ってくれます。

import json, pandas as pd

data = json.load(open("in.json"))
df = pd.json_normalize(data["data"]["items"], sep=".")
df.to_csv("out.csv", index=False)

入れ子のオブジェクトは address.city になります。リストは1つのセルの中にリストのまま残ります。行に展開したいときは、保存の前に df = df.explode("tags") を加えます。

DuckDBの場合

大きなファイルなら、DuckDBはJSONを直接読み込み、全体をメモリに載せずにCSVを書き出せます。

duckdb -c "COPY (SELECT * FROM read_json_auto('in.json')) TO 'out.csv'"

フラットな配列なら、これが最速の方法です。入れ子のオブジェクトはDuckDB独自のstruct表記で1つの列に出力されます。SELECT の中で展開するか、入れ子のデータはPythonを使ってください。

Excelの場合

データ → データの取得 → ファイルから → JSONから でPower Queryが開きます。テーブルへ をクリックし、列見出しの展開アイコン(矢印が2つ)で、レコードやリストを1階層ずつ展開します。クリックだけで操作でき、判断の結果をその場で確認できます。そのあとCSVとして保存します。書き出し時の型変換には注意してください。指定しない限り、ExcelはCSVのデータを書き換えます。

オンライン変換ツール

使えますし、公開データなら問題ありません。注意点は2つです。多くのツールは3つの判断を見せないので、出力の行数と列数が想定と合っているか確認してください。また、APIのJSONにはトークン、メールアドレス、顧客データが含まれることがよくあります。アップロードせずブラウザ内で動く変換ツールを選びましょう。

iPhoneとiPadの場合

Smart Jsonは「レコードモード」と呼ぶ方式でJSONをCSVに変換します。オブジェクトの配列が行になり、入れ子の値は address.city、tags[0] のようなドット区切りの列に展開され、ヘッダーは全レコードのキーの和集合になります。最初のオブジェクト配列は自動で見つけ、別の配列を選ぶことも、$.data.items のようなJSONPathを入力することもできます。区切り文字は変更できます。オブジェクトの配列がないドキュメントでは、役に立たない2列のダンプを出す代わりに、その旨を表示します。

YAML、CSV、TOML、XMLのタブ、形式で保持できないものについての注記、変換後ドキュメントのプレビューが表示された「変換」シート
書き出す前に、各形式が保持できないものを表示します

プレビューは無料なので、CSVがどう仕上がるかを正確に確認できます。結果のコピーと保存はProの機能です。

JSON Editor: Smart JsoniPhone・iPad

JSONをCSVとしてプレビューし、入れ子の値をドット区切りの列に展開します。レコードの配列はJSONPathで選べ、CSVで保持できないものもはっきり表示します。

できること

CSVにできたら、スマートフォンで開くことが次の小さな問題になります。そちらのガイドで説明しています。

結果を確認する

どのツールを使う場合でも、確認は3つです。

  1. 行数。 配列のレコード数と一致していますか(リストをあえて展開したなら、それより多くなります)。
  2. 列数。 一部のレコードにしかないフィールドが、ヘッダーに入っていますか。
  3. 入れ子の値を1つ。 入れ子のオブジェクトを持つレコードを選び、その値が正しいドット区切りの列に入っているか確かめます。

よくある質問

入れ子のJSONをCSVに変換するには?
入れ子のオブジェクトを address.city のようなドット区切りの列名に展開します。Pythonなら pandas.json_normalize が1回の呼び出しで行い、ExcelのPower Queryでは展開ボタンで行えます。自動で行うJSONアプリもあります。レコード内の配列は別の判断が必要です。インデックス列、要素ごとに1行、連結した1セルのどれかを選びます。
JSONからCSVへの変換ツールで1行しか出力されないのはなぜ?
レコードがドキュメントの最上位にないためです。APIのレスポンスは、たとえば data.items のように、レコードを何かで包んでいるのが普通です。ルートのオブジェクトではなく、その配列をパスで指定して変換してください。
ExcelでJSONファイルを開けますか?
はい。データ → データの取得 → ファイルから → JSONから、でPower Queryが開きます。結果をテーブルに変換し、展開ボタンで入れ子のレコードを平らにしてから、CSVとして保存します。
JSONをCSVに変換すると何が失われますか?
型(数値、真偽値、文字列はすべてテキストになります)、nullと空と欠落の区別、そして入れ子の構造そのものです。一部のレコードにしかないフィールドは、最初のレコードからヘッダーを作る変換ツールでは落とされることもあります。
コマンドラインでJSONをCSVに変換するには?
フラットな配列なら、jqの @csv でも、DuckDBの COPY … TO 'out.csv' でも、1行で変換できます。入れ子のJSONは、pandas.json_normalize を使った短いPythonスクリプトのほうが確実で読みやすくなります。

mygems — 入れ子のJSONを平らにしてCSVに書き出すSmart Jsonと、その結果を読み込むSmart CSV Viewerを作っています。