大型 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_map、map_key、end_map、start_array、end_array、string、number、boolean、null。
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 工具(
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 回應(大型) | 有逾時風險 | 邊接收邊串流 |
| 即時資料串流 | 不適用 | 必須使用 |
| 簡單的一次性讀取 | 推薦 | 不必要 |
效能比較
處理 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)。
- 批次處理:批量執行資料庫插入和 API 呼叫,而非逐筆處理。
- 優雅處理錯誤:串流處理中,單一格式錯誤的記錄不應導致整個管線崩潰。
- 監控記憶體:使用效能分析工具確認串流確實將記憶體控制在合理範圍。
如需格式化和驗證較小的 JSON 檔案,我們的 JSON 格式化工具 可處理高達 100 MB 的文件,提供即時格式化。
常見問題
串流解析器可以搭配 JSONPath 使用嗎?
部分串流函式庫支援基於路徑的篩選。Python 的 ijson.items 支援在串流過程中依路徑篩選。但複雜的 JSONPath 查詢(跨層級的篩選器、萬用字元)通常需要將完整文件載入記憶體。有關路徑查詢的詳細資訊,請參閱我們的 JSONPath 指南。
如何將大型 JSON 陣列轉換為 JSON Lines?
對於可放入記憶體的檔案,使用 jq -c '.[]' input.json > output.jsonl。對於真正的大型檔案,使用串流轉換器:用串流解析器讀取陣列,並將每個元素寫成一行。
相關資源
- JSON 格式化工具 — 格式化和驗證 JSON 檔案
- JSONPath 查詢指南 — 高效擷取 JSON 資料
- JSON 編輯器技巧 — 處理大型 JSON 文件