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

串流處理可以將記憶體使用降至幾 MB。

串流處理方式

SAX 風格(事件驅動)

解析器在遇到 JSON 標記時發出事件:

import ijson

# 逐筆處理——記憶體使用量恆定
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)

更簡單的方式:每行一個 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"}

處理非常簡單——逐行讀取:

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 回應(大型)有逾時風險邊接收邊串流
即時資料串流不適用必須使用
簡單的一次性讀取推薦不必要

效能比較

處理 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. 批次處理:批量執行資料庫插入和 API 呼叫,而非逐筆處理。
  4. 優雅處理錯誤:串流處理中,單一格式錯誤的記錄不應導致整個管線崩潰。
  5. 監控記憶體:使用效能分析工具確認串流確實將記憶體控制在合理範圍。

如需格式化和驗證較小的 JSON 檔案,我們的 JSON 格式化工具 可處理高達 100 MB 的文件,提供即時格式化。

常見問題

串流解析器可以搭配 JSONPath 使用嗎?

部分串流函式庫支援基於路徑的篩選。Python 的 ijson.items 支援在串流過程中依路徑篩選。但複雜的 JSONPath 查詢(跨層級的篩選器、萬用字元)通常需要將完整文件載入記憶體。有關路徑查詢的詳細資訊,請參閱我們的 JSONPath 指南

如何將大型 JSON 陣列轉換為 JSON Lines?

對於可放入記憶體的檔案,使用 jq -c '.[]' input.json > output.jsonl。對於真正的大型檔案,使用串流轉換器:用串流解析器讀取陣列,並將每個元素寫成一行。

相關資源

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