大きなファイルのためのJSONストリーミング:全体をロードせずに処理
標準的なJSONパーシングはドキュメント全体をメモリにロードし、完全なデータ構造を構築してからアクセスを提供します。10 MBのファイルなら問題ありません。10 GBのファイルでは、プロセスがメモリ不足でクラッシュします。ストリーミングパーサーはJSONを逐次的に処理することでこれを解決します — ドキュメント全体をメモリに保持することなく、データが到着するに従って読み取り、処理します。
標準パーシングの問題
import json
# ファイル全体をメモリにロード
with open('huge.json') as f:
data = json.load(f) # 10 GBファイル = 10 GB以上のRAM
# フルロード後に処理
for item in data['records']:
process(item)
100万レコード(各10 KB)のファイルの場合、標準パーシングには以下が必要:
- ファイルサイズ: 約10 GB
- パーシング用メモリ: 約10 GB(生の文字列)
- データ構造用メモリ: 約15-20 GB(PythonオブジェクトはJSON生データより大きい)
- 合計: 約25-30 GBのRAM
ストリーミングはこれをメガバイト単位に削減します。
ストリーミングアプローチ
SAXスタイル(イベントベース)
パーサーはJSONトークンに遭遇するとイベントを発行します:
import ijson
# アイテムを1つずつ処理 - 一定のメモリ使用量
with open('huge.json', 'rb') as f:
for record in ijson.items(f, 'records.item'):
process(record) # 各レコードは個別にパースされる
# 前のレコードはガベージコレクションされる
イベントには start_map、map_key、end_map、start_array、end_array、string、number、boolean、null があります。
JSON Lines(JSONL / NDJSON)
よりシンプルなアプローチ:1行に1つのJSONオブジェクト。各行は完全で有効なJSONドキュメントです:
{"id": 1, "name": "Alice", "email": "alice@example.com"}
{"id": 2, "name": "Bob", "email": "bob@example.com"}
{"id": 3, "name": "Charlie", "email": "charlie@example.com"}
処理は簡単 — 1行ずつ読み取り:
with open('data.jsonl') as f:
for line in f:
record = json.loads(line)
process(record)
JSON Linesの利点:
- 各行が独立してパース可能(並列処理)
- 追記に適している(新しい行を追加するだけ)
- 標準的なUnixツール(
grep、wc、head、tail)で動作 - ログファイルやストリーミングデータに自然
チャンク処理
標準的なJSON配列の場合、処理をチャンクに分割:
import ijson
def process_in_chunks(filename, chunk_size=1000):
chunk = []
with open(filename, 'rb') as f:
for record in ijson.items(f, 'item'):
chunk.append(record)
if len(chunk) >= chunk_size:
process_batch(chunk)
chunk = []
if chunk:
process_batch(chunk)
言語別の実装
Python(ijson)
import ijson
# ファイルからストリーミング
with open('large.json', 'rb') as f:
parser = ijson.parse(f)
for prefix, event, value in parser:
if prefix == 'records.item.name':
print(value)
# HTTPレスポンスからストリーミング
import urllib.request
response = urllib.request.urlopen('https://api.example.com/data')
for record in ijson.items(response, 'records.item'):
process(record)
JavaScript(Node.js)
const { createReadStream } = require('fs');
const { parser } = require('stream-json');
const { streamArray } = require('stream-json/streamers/StreamArray');
const pipeline = createReadStream('large.json')
.pipe(parser())
.pipe(streamArray());
pipeline.on('data', ({ value }) => {
process(value);
});
pipeline.on('end', () => {
console.log('Done processing');
});
コマンドライン(jq)
# ストリームモード - オブジェクトを個別に処理
jq --stream 'select(length == 2) | .[1]' large.json
# JSON Linesの処理
cat data.jsonl | jq -c 'select(.age > 30)'
# 配列をJSON Linesに変換
jq -c '.[]' large_array.json > data.jsonl
ストリーミングを使うべきとき
| シナリオ | 標準 | ストリーミング |
|---|---|---|
| 100 MB未満のファイル | 推奨 | 過剰 |
| 100 MBから1 GBのファイル | RAM次第 | 推奨 |
| 1 GB超のファイル | 不可能 | 必須 |
| 大きなHTTPレスポンス | タイムアウトのリスク | 受信時にストリーミング |
| リアルタイムデータフィード | 該当なし | 必須 |
| シンプルな1回読み取り | 推奨 | 不要 |
パフォーマンス比較
100万レコード(1 GBファイル)の処理:
| アプローチ | メモリ使用量 | 処理時間 | 複雑さ |
|---|---|---|---|
| json.load() | 3-5 GB | 15秒 | シンプル |
| ijsonストリーミング | 50 MB | 45秒 | 中程度 |
| JSON Lines | 10 MB | 12秒 | シンプル |
| チャンク(1000) | 100 MB | 20秒 | 中程度 |
ストリーミングはメモリ使用量がはるかに少ないですが、イベント駆動のオーバーヘッドのためSAXスタイルのパーシングは遅くなります。JSON Linesは各行が独立したパース操作のため、高速かつメモリ効率が良いです。
ベストプラクティス
- 可能な場合はJSON Linesを使用: 最もシンプルなストリーミングフォーマットで、標準ツールで動作します。
- バッファサイズを設定: 最適なスループットのために読み取りバッファを設定(通常64 KBから1 MB)。
- バッチで処理: 1レコードずつ処理するのではなく、データベース挿入やAPI呼び出しをバッチ化。
- エラーを適切に処理: ストリーミングでは、1つの不正なレコードがパイプライン全体をクラッシュさせるべきではない。
- メモリを監視: プロファイリングを使用してストリーミングが実際にメモリを制限しているか確認。
小さなJSONファイルのフォーマットとバリデーションには、JSONフォーマッターが100 MBまでのドキュメントをリアルタイムフォーマットで処理します。
FAQ
ストリーミングパーサーでJSONPathを使用できますか?
一部のストリーミングライブラリはストリーミング中のパスベースフィルタリングをサポートしています。Python ijson.itemsはストリーミング中のパスによるフィルタリングをサポートします。ただし、複雑なJSONPathクエリ(フィルター、レベル間のワイルドカード)は通常、完全なドキュメントがメモリに必要です。パスベースのクエリについてはJSONPathガイドをご覧ください。
大きなJSON配列をJSON Linesに変換するにはどうすればよいですか?
メモリに収まるファイルには jq -c '.[]' input.json > output.jsonl を使用します。本当に大きなファイルには、ストリーミングコンバーターを使用してください:ストリーミングパーサーで配列を読み取り、各要素を1行として書き出します。
関連リソース
- JSONフォーマッター — JSONファイルのフォーマットとバリデーション
- JSONPathクエリガイド — JSONからのデータ抽出を効率的に
- JSONエディターのコツ — 大きなJSONドキュメントでの作業