2026年に大きなJSONファイルをクラッシュせずにパースする方法
JavaScriptとPythonで大きなJSONファイル(100MB以上)を扱うための完全ガイド。メモリエラーを回避するためのストリーミング、プログレッシブパース、チャンキング技術を学びます。
Big JSON Team
• Technical WriterExpert in JSON data manipulation, API development, and web technologies. Passionate about creating tools that make developers' lives easier.
# 2026年に大きなJSONファイルをクラッシュせずにパースする方法
はじめに
大きなJSONファイルをパースしようとして「FATAL ERROR: JavaScript heap out of memory」エラーに遭遇したことはありませんか?あなただけではありません。大きなJSONファイル(100MB以上)の処理は、現代のWeb開発で最も一般的な課題の1つです。
この包括的なガイドでは、メモリを使い果たしたりアプリケーションをクラッシュさせることなく、大きなJSONファイルを効率的にパースするための実証済みの技術を学びます。
なぜJSON.parse()は大きなファイルで失敗するのか
標準のJSON.parse()メソッドは、ファイル全体を一度にメモリにロードします:
// ⚠️ 大きなファイル(>100MB)には悪い
const fs = require('fs');
const data = fs.readFileSync('huge-file.json', 'utf8');
const json = JSON.parse(data); // 大量のメモリを消費!
なぜ失敗するのか:
- 文字列全体をメモリに保存(ファイルサイズの2-3倍)
- 完全なJavaScriptオブジェクトを作成(さらにファイルサイズの2-5倍)
- 完了するまでメモリの解放がない
- 合計: 500MBのファイルが2GB以上のメモリを使用する可能性があります!
解決策1: JSONStreamによるストリーミング (Node.js)
大きなJSONファイルに対する最良のアプローチはストリーミングです - データを少しずつ処理します。
実践的な例
const fs = require('fs');
const JSONStream = require('JSONStream');
// オブジェクトの配列をパース
const stream = fs.createReadStream('large-dataset.json', { encoding: 'utf8' });
const parser = JSONStream.parse(''); // 配列要素には''
let count = 0;
parser.on('data', (item) => {
// 一度に1つのアイテムを処理
console.log(レコード${++count}を処理中:, item.id);
// ここにロジック - DBに挿入、変換など
if (item.price > 1000) {
saveToDatabase(item);
}
});
parser.on('end', () => {
console.log(${count}レコードを正常に処理しました);
});
parser.on('error', (err) => {
console.error('パースエラー:', err);
});
stream.pipe(parser);
ネストされたJSONパスのパース
// 用途: { "users": [ {...}, {...} ] }
const parser = JSONStream.parse('users.');
// 用途: { "data": { "items": [...] } }
const parser = JSONStream.parse('data.items.');
// 用途: 深い構造や複雑な条件
const parser = JSONStream.parse(['data', 'nested', { emitKey: true }]);
パフォーマンス比較
| メソッド | ファイルサイズ | メモリ使用量 | 時間 |
|---------|-------------|------------|-----|
| JSON.parse() | 500MB | ~2.1GB 💥 | 8秒 |
| JSONStream | 500MB | ~45MB ✅ | 12秒 |
| stream-json | 500MB | ~38MB ✅ | 10秒 |
ベンチマーク: Node.js 20, Mac M1, 16GB RAM解決策2: stream-json (より高速)
stream-jsonはJSONStreamより高速で、より多くの制御を提供します:
const { chain } = require('stream-chain');
const { parser } = require('stream-json');
const { streamArray } = require('stream-json/streamers/StreamArray');
const pipeline = chain([
fs.createReadStream('huge-array.json'),
parser(),
streamArray()
]);
pipeline.on('data', ({ key, value }) => {
console.log(インデックス${key}:, value.name);
// ここで処理
processRecord(value);
});
pipeline.on('end', () => console.log('完了!'));
JSONオブジェクト用(配列以外)
const { streamObject } = require('stream-json/streamers/StreamObject');
const pipeline = chain([
fs.createReadStream('huge-object.json'),
parser(),
streamObject()
]);
pipeline.on('data', ({ key, value }) => {
console.log(キー: ${key}, value);
});
解決策3: チャンク読み込み (ブラウザ)
ブラウザでは、FileReaderを使用してファイルを段階的に処理します:
async function parseHugeJSON(file) {
const chunkSize = 1024 1024; // 1MB
let offset = 0;
let buffer = '';
let results = [];
while (offset < file.size) {
const chunk = file.slice(offset, offset + chunkSize);
const text = await chunk.text();
buffer += text;
// 完全なオブジェクトをパースしようと試みる
const lines = buffer.split('\n');
buffer = lines.pop(); // 不完全な行を保持
for (const line of lines) {
if (line.trim()) {
try {
const obj = JSON.parse(line);
results.push(obj);
// メモリビルドアップを避けるためにバッチ処理
if (results.length >= 1000) {
await processBatch(results);
results = [];
}
} catch (e) {
console.warn('無効な行をスキップ:', line.substring(0, 50));
}
}
}
offset += chunkSize;
// 進捗を報告
const progress = (offset / file.size 100).toFixed(1);
console.log(進捗: ${progress}%);
}
// 最後のバッチを処理
if (results.length > 0) {
await processBatch(results);
}
}
// 使用方法
document.getElementById('fileInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
await parseHugeJSON(file);
});
解決策4: Web Workers (ノンブロッキング)
UIの凍結を防ぐため、パースをWeb Workerに移動します:
// worker.js
self.onmessage = async function(e) {
const { file } = e.data;
const chunkSize = 1024 1024;
let processed = 0;
for (let offset = 0; offset < file.size; offset += chunkSize) {
const chunk = file.slice(offset, offset + chunkSize);
const text = await chunk.text();
// チャンクを処理
const lines = text.split('\n').filter(l => l.trim());
const objects = lines.map(line => {
try {
return JSON.parse(line);
} catch {
return null;
}
}).filter(Boolean);
// 結果を送り返す
self.postMessage({
type: 'progress',
data: objects,
percent: (offset / file.size 100)
});
processed += objects.length;
}
self.postMessage({ type: 'complete', total: processed });
};
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
if (e.data.type === 'progress') {
console.log(処理済み: ${e.data.percent.toFixed(1)}%);
updateUI(e.data.data);
} else if (e.data.type === 'complete') {
console.log(完了! 合計: ${e.data.total});
}
};
// 処理を開始
const fileInput = document.getElementById('upload');
fileInput.addEventListener('change', (e) => {
worker.postMessage({ file: e.target.files[0] });
});
解決策5: APIのページネーション
APIからJSONを取得する場合は、ページネーションを使用します:
async function fetchAllData(apiUrl) {
let allData = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(${apiUrl}?page=${page}&limit=100);
const data = await response.json();
// ページをすぐに処理
await processPage(data.items);
allData.push(...data.items);
hasMore = data.hasNextPage;
page++;
// レート制限を避けるために遅延を追加
await new Promise(resolve => setTimeout(resolve, 100));
}
return allData;
}
// 最適化版: すべてを保存しない
async function processAllData(apiUrl) {
let page = 1;
let hasMore = true;
let totalProcessed = 0;
while (hasMore) {
const response = await fetch(${apiUrl}?page=${page}&limit=100);
const data = await response.json();
// 保存せずに処理
for (const item of data.items) {
await processItem(item); // DBに保存、変換など
totalProcessed++;
}
hasMore = data.hasNextPage;
page++;
console.log(これまでに${totalProcessed}アイテムを処理...);
}
return totalProcessed;
}
解決策6: ijsonを使用したPython
サーバーサイド処理には、Pythonのijsonが優れています:
import ijson
def process_large_json(filename):
with open(filename, 'rb') as file:
# オブジェクトの配列をパース
objects = ijson.items(file, 'item')
count = 0
for obj in objects:
# 各要素を処理
if obj.get('price', 0) > 1000:
save_to_database(obj)
count += 1
if count % 1000 == 0:
print(f"{count}レコードを処理しました...")
print(f"合計: {count}レコード")
# ネストされた構造用
def process_nested(filename):
with open(filename, 'rb') as file:
# 特定のキーのみを抽出
for user_id in ijson.items(file, 'users.item.id'):
print(f"ユーザーID: {user_id}")
最適化戦略
1. 個別ではなくバッチ処理
let batch = [];
const BATCH_SIZE = 1000;
parser.on('data', async (item) => {
batch.push(item);
if (batch.length >= BATCH_SIZE) {
await insertBatch(batch); // 1回のバッチ挿入
batch = [];
}
});
parser.on('end', async () => {
if (batch.length > 0) {
await insertBatch(batch); // 最後のバッチを処理
}
});
2. 早期データフィルタリング
const { pick } = require('stream-json/filters/Pick');
const { streamArray } = require('stream-json/streamers/StreamArray');
const pipeline = chain([
fs.createReadStream('data.json'),
parser(),
pick({ filter: 'users' }), // "users"プロパティのみ
streamArray()
]);
3. ファイル圧縮
const zlib = require('zlib');
const pipeline = chain([
fs.createReadStream('data.json.gz'),
zlib.createGunzip(), // オンザフライで解凍
parser(),
streamArray()
]);
一般的なシナリオと解決策
シナリオ1: JSONからのデータベースインポート
const { chain } = require('stream-chain');
const { parser } = require('stream-json');
const { streamArray } = require('stream-json/streamers/StreamArray');
const { MongoClient } = require('mongodb');
async function importToMongo(filename, collectionName) {
const client = await MongoClient.connect('mongodb://localhost:27017');
const collection = client.db('mydb').collection(collectionName);
let batch = [];
const BATCH_SIZE = 1000;
const pipeline = chain([
fs.createReadStream(filename),
parser(),
streamArray()
]);
for await (const { value } of pipeline) {
batch.push(value);
if (batch.length >= BATCH_SIZE) {
await collection.insertMany(batch);
console.log(${batch.length}ドキュメントを挿入);
batch = [];
}
}
if (batch.length > 0) {
await collection.insertMany(batch);
}
await client.close();
}
シナリオ2: JSONをCSVに変換
const { Transform } = require('stream');
const csvWriter = require('csv-write-stream');
const jsonToCSV = new Transform({
objectMode: true,
transform(chunk, encoding, callback) {
// JSONオブジェクトをCSV行に変換
callback(null, {
id: chunk.id,
name: chunk.name,
email: chunk.email
});
}
});
const writer = csvWriter();
writer.pipe(fs.createWriteStream('output.csv'));
pipeline
.pipe(jsonToCSV)
.pipe(writer);
シナリオ3: 改行区切りJSON (NDJSON)の処理
const readline = require('readline');
async function processNDJSON(filename) {
const fileStream = fs.createReadStream(filename);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
let count = 0;
for await (const line of rl) {
if (line.trim()) {
const obj = JSON.parse(line);
await processObject(obj);
count++;
}
}
console.log(${count}行を処理しました);
}
トラブルシューティング
エラー: JavaScript heap out of memory
解決策:# Node.jsでメモリ制限を増やす
node --max-old-space-size=4096 script.js
しかし、もっと良いのは: 代わりにストリーミングを使用してください!
エラー: チャンク読み込みで無効なJSON
問題: チャンクがJSONオブジェクトを分割する可能性があります。 解決策: 行区切りJSON (NDJSON)を使用します:// まずJSONをNDJSONに変換
const items = require('./data.json'); // 一度だけ小さく読み込む
const stream = fs.createWriteStream('data.ndjson');
items.forEach(item => {
stream.write(JSON.stringify(item) + '\n');
});
stream.end();
エラー: 処理が遅すぎる
最適化:JSONStreamの代わりにstream-jsonを使用(2倍速い)推奨ツール
JSON検証用
BigJSON.onlineを使用して、完全にロードせずに大きなJSONファイルを迅速に検証および検査します。ストリーミングとリアルタイム検証をサポートしています。ライブラリ
- Node.js:
stream-json(最速)、JSONStream(最もシンプル) - Python:
ijson(最高) - ブラウザ: FileReader API + Web Workers
まとめ
クイック推奨事項:| ユースケース | 最適な解決策 | メモリ使用量 |
|----------|-----------|-----------|
| Node.js (大きな配列) | stream-json | 非常に低い |
| Node.js (単純な構造) | JSONStream | 低い |
| ブラウザ (< 50MB) | チャンク読み込み | 中程度 |
| ブラウザ (> 50MB) | Web Workers | 低い |
| APIダウンロード | ページネーション + ストリーム処理 | 非常に低い |
| Python | ijson | 非常に低い |
重要なポイント:- ✅ 50MB以上のファイルには常にストリーミングを使用
- ✅ DB挿入にはバッチ処理
- ✅ メモリ使用量を減らすために早期フィルタリング
- ❌ 大きなファイルにはJSON.parse()を使用しない
- ❌ すべてを一度にメモリにロードしない
最新のストリーミングパッケージを使用すれば、メモリを使い果たしたりクラッシュすることなく、GB サイズのJSONファイルを簡単に処理できます。
その他のリソース
このガイドは役に立ちましたか?大きなJSONファイルに苦労している開発者仲間と共有してください!