CSV から JSON への変換ガイド:方法、落とし穴、ベストプラクティス
運用チームから CSV エクスポートが届いた。API が受け付けるのは JSON。ファイルを開くと 10,000 行のカンマ区切りデータが並んでいる。データを失わずに CSV を JSON へ変換する最速の方法は何だろうか?
このガイドでは、4 つの変換方法(ブラウザツール、JavaScript、Python、CLI)、逆方向の変換(JSON から CSV)、データを密かに破壊する 5 つの落とし穴、そしてメモリに収まらないほど大きなファイルの処理方法を解説する。
CSV と JSON:使い分けの基準
変換する前に、それぞれのフォーマットが何に適しているかを理解しておこう。
| 観点 | CSV | JSON |
|---|---|---|
| 構造 | フラットなテーブル(行と列) | ネストされた階層構造(オブジェクト、配列) |
| データ型 | すべて文字列 | 文字列、数値、真偽値、null |
| 可読性 | スプレッドシート向き | 開発者向き |
| 主な用途 | データエクスポート/インポート、レポート、ETL | API、設定ファイル、NoSQL ストレージ |
| ファイルサイズ | 小さい(キー名の繰り返しがない) | 大きい(レコードごとにキー名が繰り返される) |
| スキーマ | 暗黙的(ヘッダー行) | 明示的(または JSON Schema を使用) |
経験則: データがテーブル形式で、消費側がスプレッドシートやデータパイプラインなら CSV を使う。データに階層構造があるか、消費側が API なら JSON を使う。変換後の JSON 出力は JSON Formatter で検証すれば、構造上の問題を早期に発見できる。
プロジェクトで JSON5 や JSONC のような緩やかな JSON フォーマットを設定に使っている場合は、JSON5 と JSONC のフォーマットガイドで構文の違いやツール情報を確認してほしい。
CSV を JSON に変換する 4 つの方法
方法 1 — ブラウザベースのツール
単発の変換であれば、ブラウザベースのアプローチが最速だ。オンラインコンバーターに CSV を貼り付けて JSON を取得し、JSON Formatter で結果を検証して構造が正しいことを確認する。
メリットは、データがブラウザの外に出ないことだ。アップロードなし、サーバー処理なし、プライバシーの懸念なし。社内データや、エクスポートに埋め込まれた API キー、サードパーティのサーバーに送りたくないデータを扱うときに重要になる。
最適な用途:小さなファイル(10 MB 未満)、1 回限りの変換、非技術者のチームメンバー。
方法 2 — JavaScript / Node.js
ブラウザ(素の JS):
function csvToJson(csv) {
const lines = csv.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim());
return lines.slice(1).map(line => {
const values = line.split(',');
return headers.reduce((obj, header, i) => {
obj[header] = values[i]?.trim() ?? '';
return obj;
}, {});
});
}
const csv = `name,age,city
Alice,30,New York
Bob,25,London`;
console.log(JSON.stringify(csvToJson(csv), null, 2));
これはクォートされたフィールドのない単純な CSV に対して動作する。値内のカンマ、フィールド内の改行、クォートされた文字列を扱う本番用途では、適切なパーサーを使用すること。
Node.js(csv-parser + ストリーム):
import { createReadStream } from 'fs';
import { parse } from 'csv-parse';
const records = [];
createReadStream('data.csv')
.pipe(parse({ columns: true, trim: true, skip_empty_lines: true }))
.on('data', (row) => records.push(row))
.on('end', () => {
console.log(JSON.stringify(records, null, 2));
});
columns: true オプションは最初の行をキーとして使用する。trim オプションは値から空白を除去する。これによりクォートされたフィールド、エスケープされたカンマ、複数行の値を正しく処理できる。
方法 3 — Python
標準ライブラリ(依存関係ゼロ):
import csv
import json
with open('data.csv', encoding='utf-8') as f:
reader = csv.DictReader(f)
rows = list(reader)
with open('data.json', 'w', encoding='utf-8') as f:
json.dump(rows, f, indent=2, ensure_ascii=False)
csv.DictReader はヘッダー行をキーとして使い、各行を辞書にマッピングする。ensure_ascii=False フラグは、Unicode 文字(日本語、中国語、アクセント付き文字など)を \uXXXX にエスケープせずそのまま保持する。
pandas(データサイエンティスト向けのワンライナー):
import pandas as pd
df = pd.read_csv('data.csv')
df.to_json('data.json', orient='records', indent=2, force_ascii=False)
使い分けの基準:
- csv + json:軽量スクリプト、Lambda 関数、依存関係を最小限にしたいコンテナ環境。
- pandas:変換前にデータのクリーニング、フィルタリング、変換が必要な場合。単なるフォーマット変換以上のことをするなら、pandas のインポートコストに見合う価値がある。
方法 4 — CLI ツール
シェルスクリプトや自動化パイプライン向け:
csvkit:
# Install: pip install csvkit
csvjson data.csv > data.json
Miller (mlr):
# Install: brew install miller (macOS) or apt install miller (Ubuntu)
mlr --csv --json cat data.csv > data.json
jq と組み合わせてフィルタリング:
# Convert and filter in one pipeline
csvjson data.csv | jq '[.[] | select(.age | tonumber > 25)]'
Miller は CSV、JSON、TSV などのフォーマットをネイティブに扱えるため、特に強力だ。変換と同時にデータを変換できる:
# Convert CSV to JSON, rename a field, add a computed field
mlr --csv --json rename name,fullName then put '$age_group = ($age > 30) ? "senior" : "junior"' data.csv
JSON から CSV へ:逆方向の変換
JSON から CSV への変換には、順方向には存在しない課題がある。
ネストされたオブジェクトのフラット化
CSV は本質的にフラットだ。JSON にネストされたオブジェクトがある場合、フラット化の戦略が必要になる:
{
"name": "Alice",
"address": {
"city": "New York",
"zip": "10001"
}
}
これは以下のようになる:
| name | address.city | address.zip |
|---|---|---|
| Alice | New York | 10001 |
ドット記法(address.city)が最も一般的なアプローチだ。Python での例:
import pandas as pd
data = [
{"name": "Alice", "address": {"city": "New York", "zip": "10001"}},
{"name": "Bob", "address": {"city": "London", "zip": "EC1A"}}
]
df = pd.json_normalize(data)
df.to_csv('output.csv', index=False)
# Columns: name, address.city, address.zip
配列の処理
配列フィールドには判断が必要だ:
| 戦略 | 入力例 | CSV 出力 | 最適な用途 |
|---|---|---|---|
| 文字列として結合 | ["admin","editor"] | admin;editor | シンプルなリスト、再インポート可能 |
| 列に展開 | ["admin","editor"] | role_0: admin, role_1: editor | 固定長の配列 |
| 行に展開 | ["admin","editor"] | ロールごとに 2 行 | リレーショナル分析 |
下流の消費側に合わせて選択する。CSV がデータベースに戻される場合、行への展開が最も合理的なことが多い。
型情報の喪失
CSV には型システムがない。JSON から CSV に変換すると:
trueは文字列"true"になる — 真偽値なのか文字列なのか?nullは空のセルになる — 空文字列""と区別できない42は"42"になる — 数値なのか文字列なのか?
ラウンドトリップの忠実性が重要な場合(CSV → 処理 → JSON)、型の規約をヘッダーコメントや付属のスキーマファイルに文書化しておくこと。
よくある 5 つの落とし穴とその回避方法
これらの問題はデータを密かに破壊する。ほとんどのチュートリアルでは省略されている。本番環境で初めて学ぶことのないようにしよう。
1. エンコーディングの地雷
問題: 同僚から受け取った CSV を開くと、é の代わりに é が表示される。あるいは日本語の文字が「文字化け」して読めなくなる。
原因: ファイルがある文字コード(Windows-1252、GBK、Shift_JIS)で保存されたのに、パーサーが UTF-8 を前提としている。日本の環境では、古いシステムやツールが Shift_JIS でファイルを出力することが特に多い。また、Windows 版 Excel は CSV を Windows-1252 で保存したり、UTF-8 BOM(ファイル先頭の不可視バイト \xEF\xBB\xBF、Byte Order Mark)を付加したりすることがある。
解決策:
# Detect encoding first
import chardet
with open('data.csv', 'rb') as f:
result = chardet.detect(f.read(10000))
print(result) # {'encoding': 'Windows-1252', 'confidence': 0.73}
# Then read with the correct encoding
with open('data.csv', encoding=result['encoding']) as f:
reader = csv.DictReader(f)
# ...
Node.js では BOM を明示的に除去する:
import { readFileSync } from 'fs';
let content = readFileSync('data.csv', 'utf-8');
// Strip UTF-8 BOM if present
if (content.charCodeAt(0) === 0xFEFF) {
content = content.slice(1);
}
2. 区切り文字の混乱
問題: パーサーが複数のフィールドではなく、1 つの巨大な列を生成してしまう。
原因: 多くのヨーロッパ圏のロケール(フランス、ドイツ、スペイン)では、カンマが小数点の区切りに使われる(例:3.14 ではなく 3,14)ため、Excel がセミコロン(;)を CSV の区切り文字として使用する。タブ区切りファイル(.tsv)はさらに別のバリアントだ。
解決策: 先頭の数行をサンプリングして区切り文字を自動検出する:
import csv
with open('data.csv') as f:
sample = f.read(8192)
dialect = csv.Sniffer().sniff(sample, delimiters=',;\t|')
f.seek(0)
reader = csv.DictReader(f, dialect=dialect)
3. 先頭のゼロが消える
問題: 郵便番号 00501 が 501 になる。商品コード 007 が 7 になる。
原因: パーサー(または Excel)がフィールドを数値として解釈し、先頭のゼロを除去する。これは郵便番号、電話番号、ID コードで特に危険だ。
解決策: 文字列型を強制する。pandas の場合:
df = pd.read_csv('data.csv', dtype={'zip': str, 'product_code': str})
JavaScript では、元の文字列が数値パース結果と異なるかを確認する:
function preserveLeadingZeros(value) {
if (/^0\d+$/.test(value)) return value; // Keep as string
const num = Number(value);
return isNaN(num) ? value : num;
}
4. 大きな数値の精度損失
問題: ID 9007199254740993 が JSON では 9007199254740992 になる。
原因: JavaScript の Number は 64 ビット浮動小数点数(IEEE 754)だ。Number.MAX_SAFE_INTEGER(2^53 - 1 = 9007199254740991)を超える整数は精度を失う。これはデータベース ID、Snowflake ID、Twitter/X の投稿 ID に影響する。
解決策: JSON では大きな数値を文字列として保持するか、処理コードで BigInt を使用する:
// Parse with string preservation for large numbers
function safeParseNumber(value) {
const num = Number(value);
if (Number.isInteger(num) && !Number.isSafeInteger(num)) {
return value; // Keep as string to preserve precision
}
return isNaN(num) ? value : num;
}
5. 空の値の曖昧さ
問題: CSV に空のセルがある。変換後、元の値が空文字列 ""、null、あるいは単に欠損していたのかを区別できない。
原因: CSV にはこれら 3 つの状態を区別する方法がない。2 つのカンマの間の空フィールド(Alice,,30)はどれを意味していてもおかしくない。
解決策: 規約を定めて一貫して適用する:
def parse_value(value):
if value == '':
return None # or '' — pick one convention
if value == 'NULL' or value == 'null':
return None
return value
データが NULL、N/A、- のようなセンチネル値を使っている場合は、それらを文書化して明示的に処理すること。
大規模ファイルのストリーミング処理
CSV が 100 MB を超える場合、ファイル全体をメモリに読み込むのは現実的ではない。ストリーミングを使おう。
Node.js(ストリームパイプライン):
import { createReadStream, createWriteStream } from 'fs';
import { parse } from 'csv-parse';
import { Transform } from 'stream';
import { pipeline } from 'stream/promises';
let first = true;
const toJsonArray = new Transform({
objectMode: true,
transform(record, encoding, callback) {
const prefix = first ? '[\n' : ',\n';
first = false;
callback(null, prefix + JSON.stringify(record));
},
flush(callback) {
callback(null, '\n]');
}
});
await pipeline(
createReadStream('large.csv'),
parse({ columns: true, trim: true }),
toJsonArray,
createWriteStream('large.json')
);
Python(ジェネレーター):
import csv
import json
def csv_rows(path):
with open(path, encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
yield row
# Stream to JSON Lines format (one JSON object per line)
with open('large.jsonl', 'w', encoding='utf-8') as out:
for row in csv_rows('large.csv'):
out.write(json.dumps(row, ensure_ascii=False) + '\n')
非常に大きなファイルの場合は、単一の JSON 配列ではなく JSON Lines(.jsonl)を検討しよう。各行が独立した JSON オブジェクトであるため、出力ファイルも 1 行ずつ処理できる。ファイル全体をパースする必要がない。
FAQ
CSV と JSON の違いは何ですか?
CSV(Comma-Separated Values)はデータをフラットなテーブルとして格納し、すべての値が文字列になる。JSON(JavaScript Object Notation)はネストされたオブジェクト、配列、型付きの値(文字列、数値、真偽値、null)を持つ構造化データを格納する。CSV はファイルサイズが小さくスプレッドシートとの相性が良い。JSON はより表現力が高く、API との相性が良い。
JavaScript で CSV を JSON に変換するには?
Node.js では csv-parse パッケージを使用する。createReadStream でファイルを読み取り、parse({ columns: true }) にパイプし、結果を収集する。ブラウザでは FileReader でファイルを読み取り、改行で分割し、ヘッダー行をキーとして各行をオブジェクトにマッピングする。
Python で CSV を JSON に変換するには?
標準ライブラリの csv.DictReader で行を辞書として読み取り、json.dump() で JSON 配列として書き出す。変換前にデータ加工が必要な場合は、pandas.read_csv() に続けて df.to_json(orient='records') を使うワンライナーの方法もある。
ネストされた JSON を CSV に変換できますか?
できるが、フラット化の戦略が必要だ。最も一般的なアプローチはドット記法を使う。address.city のようなフィールドが列ヘッダーになる。Python では pandas.json_normalize() がこれを自動的に処理する。配列については追加の判断が必要で、文字列として結合するか、列に展開するか、行に展開するかを選ぶ。
変換後の CSV で文字化けが発生するのはなぜですか?
エンコーディングの不一致が原因だ。ファイルが Windows-1252、GBK、または Shift_JIS で保存されているのに、パーサーが UTF-8 を前提としている可能性が高い。日本語環境では Shift_JIS が原因であるケースが特に多い。Python の chardet のような検出ライブラリでエンコーディングを特定し、読み取り時に明示的に指定すること。また、一部のツールが自動的に付加する UTF-8 BOM も確認しよう。
100 MB を超える CSV ファイルはどう処理すればよいですか?
ファイル全体をメモリに読み込む代わりに、ストリーミングを使用する。Node.js では csv-parse にストリームをパイプする。Python では csv.DictReader をジェネレーターで使って反復処理する。下流の処理を容易にするために、単一の JSON 配列ではなく JSON Lines(.jsonl)フォーマットでの出力を検討しよう。
変換後の JSON が正しいかどうかを検証するには?
出力をオンラインの JSON Formatter に貼り付けて、構文、構造、ネストを確認する。自動検証には、JavaScript の JSON.parse() または Python の json.loads() を使用する。どちらも不正な入力に対して明確なエラーを返す。スキーマレベルの検証には、JSON Schema を定義してプログラムで検証する。
まとめ
- 状況に合った方法を選ぶ — 単発の変換にはブラウザツール、自動化にはコード、パイプラインには CLI。
- エンコーディングの問題に注意する — デフォルトに頼らず、常にエンコーディングを明示的に指定する。
- 型を意図的に保持する — 先頭のゼロ、大きな整数、null 値にはすべて明示的な処理が必要だ。
- 大規模ファイルはストリーミングで処理する — メモリに読み込まない。非常に大きなデータセットには JSON Lines フォーマットを検討する。
- 出力を検証する — 変換した JSON を JSON Formatter に通して、本番に影響する前に構造上の問題を検出する。