alltools.one
JSON
2025-06-02
8 min
alltools.one Team
JSONStreamingPerformanceLarge FilesMemory

大きなファイルのための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_mapmap_keyend_mapstart_arrayend_arraystringnumberbooleannull があります。

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ツール(grepwcheadtail)で動作
  • ログファイルやストリーミングデータに自然

チャンク処理

標準的な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 GB15秒シンプル
ijsonストリーミング50 MB45秒中程度
JSON Lines10 MB12秒シンプル
チャンク(1000)100 MB20秒中程度

ストリーミングはメモリ使用量がはるかに少ないですが、イベント駆動のオーバーヘッドのためSAXスタイルのパーシングは遅くなります。JSON Linesは各行が独立したパース操作のため、高速かつメモリ効率が良いです。

ベストプラクティス

  1. 可能な場合はJSON Linesを使用: 最もシンプルなストリーミングフォーマットで、標準ツールで動作します。
  2. バッファサイズを設定: 最適なスループットのために読み取りバッファを設定(通常64 KBから1 MB)。
  3. バッチで処理: 1レコードずつ処理するのではなく、データベース挿入やAPI呼び出しをバッチ化。
  4. エラーを適切に処理: ストリーミングでは、1つの不正なレコードがパイプライン全体をクラッシュさせるべきではない。
  5. メモリを監視: プロファイリングを使用してストリーミングが実際にメモリを制限しているか確認。

小さなJSONファイルのフォーマットとバリデーションには、JSONフォーマッターが100 MBまでのドキュメントをリアルタイムフォーマットで処理します。

FAQ

ストリーミングパーサーでJSONPathを使用できますか?

一部のストリーミングライブラリはストリーミング中のパスベースフィルタリングをサポートしています。Python ijson.itemsはストリーミング中のパスによるフィルタリングをサポートします。ただし、複雑なJSONPathクエリ(フィルター、レベル間のワイルドカード)は通常、完全なドキュメントがメモリに必要です。パスベースのクエリについてはJSONPathガイドをご覧ください。

大きなJSON配列をJSON Linesに変換するにはどうすればよいですか?

メモリに収まるファイルには jq -c '.[]' input.json > output.jsonl を使用します。本当に大きなファイルには、ストリーミングコンバーターを使用してください:ストリーミングパーサーで配列を読み取り、各要素を1行として書き出します。

関連リソース

Published on 2025-06-02
JSON Streaming for Large Files: Process Without Loading All | alltools.one