跳至內容

處理大型 JSON 檔案

當 JSON 大到無法整批塞進記憶體時的策略:串流解析器、NDJSON、分塊、壓縮,以及不會卡住的工具。

5 MB 的 JSON 沒問題。500 MB 的 JSON 若用處理 5 MB 的方式對待,會把 你的編輯器、JSON.parse 與記憶體預算一起搞掛。本文是大型 JSON 的 求生包:為什麼它慢、該選哪種解析器、何時該換格式,以及哪些工具能 撐住 10 萬筆紀錄的陣列。

為什麼大 JSON 慢

每個主流語言的預設解析器——JSON.parsejson.loadsjson.Unmarshal——都是 整份文件、整批載入記憶體。它把整個檔案 讀成字串,再配置一份對應的物件圖。兩項成本會隨檔案大小增長:

  • 記憶體——解析後的結構通常是檔案大小的 3–5 倍(物件 header、hash bucket、boxed number)。500 MB 的檔案常常變成 2 GB heap。
  • 延遲——解析是單一流程、大致序列。500 MB 的 JSON 在效能不錯的硬體上仍需數秒,期間解析執行緒不可用。

幾 MB 以下用整批解析器是對的;超過就需要不同策略。

串流解析器

串流解析器在走訪輸入時 發出事件,而非建構樹。經典介面是 SAX 風格: onStartObjectonKeyonValueonEndArray。你看到每筆紀錄就決定 要不要留。

函式庫:

  • JavaScript——clarinetstream-jsonJSONStream
  • Python——ijson
  • Go——encoding/jsonDecoderToken()
  • Rust——serde_json::Deserializer::from_reader

stream-json 從巨型頂層陣列抽出紀錄,全程不持有整份文件:

import fs from "node:fs";
import { parser } from "stream-json";
import { streamArray } from "stream-json/streamers/StreamArray.js";

fs.createReadStream("orders.json")
  .pipe(parser())
  .pipe(streamArray())
  .on("data", ({ value }) => {
    // value 是一筆紀錄,處理後丟掉
    if (value.total > 100) console.log(value.id);
  })
  .on("end", () => console.log("done"));

不論檔案多大,記憶體都是平的。

NDJSON / 行分隔 JSON

最簡單的「串流友善」格式是 每行一個 JSON 值

{"id":"1","total":42}
{"id":"2","total":85}
{"id":"3","total":17}

這就是 NDJSON(或 JSON Lines、.ndjson.jsonl)。所有雲端 日誌都這樣寫,所有分析平台都這樣吃。解析就是每行一次 JSON.parse, 易於串流、易於拆給 worker、shell 也能 grep

若你能控制大資料的產出端,請改用 NDJSON 而非巨型陣列。下游會 感謝你。

import readline from "node:readline";
import fs from "node:fs";

const rl = readline.createInterface({ input: fs.createReadStream("orders.jsonl") });
for await (const line of rl) {
  if (!line.trim()) continue;
  const record = JSON.parse(line);
  // ...
}

分塊與分頁

若產出端堅持發出單份巨型文件,兩種模式可救:

  • 伺服端分頁——?page=1&pageSize=1000 cursor。每頁是小文件。記得帶 next cursor 或 total 讓客戶端知道何時停。
  • 客戶端分塊——從磁碟讀時使用串流解析器,每處理 N 筆才寫進資料庫/網路/輸出。

要隨手探索巨大文件,/zh-Hant/json/viewer 採用 延遲展開子樹,不會把整張圖物化。

線上壓縮

JSON 壓縮率很高——gzip 通常省 80–90%,Brotli 與 Zstandard 更好。 重複的鍵與空白正是壓縮演算法最擅長的模式。

對外提供大型 JSON 的 HTTP endpoint:

  • 設定 Content-Encoding: gzip(或 brzstd),於來源端預壓或即時壓縮。
  • 把靜態大型 JSON 在 build 階段預壓,直接提供 .json.gz.json.br
  • 不必為了壓縮先 minify——壓縮會把空白省回來。

更多權衡見 壓縮與美化 JSON

不會卡住的工具

多數編輯器與 JSON viewer 為了語法高亮會把整份文件解析成 DOM 樣結構, 這就是它們在大檔案上會卡住的原因。較好的策略:

  • JSON Viewer——延遲展開樹;子樹按需解析。
  • JSON Formatter——超過 ~1.5 MB 改用 Web Worker,UI 不會卡。
  • 終端機用 jq -c 逐行處理。
  • less 做原始檢視——絕不要用 Vim 開 500 MB。

什麼時候該離開 JSON

JSON 是文字。文字冗長。對於吞吐量或成本敏感的工作流,二進位格式是 正解:

  • Protocol Buffers / FlatBuffers——schema 驅動、很快、很小。
  • MessagePack——資料模型像 JSON、二進位編碼,多數 JSON 程式碼可直接替換。
  • Avro / Parquet——欄狀,分析工作的最佳解。
  • CBOR——IETF 的二進位 JSON,用於 IoT、WebAuthn。

若你常在數百萬筆紀錄上跑相同的聚合查詢,請改用 Parquet。

實務流程

「我拿到一份 2 GB 的 JSON」的固定流程:

  1. head 看前 1000 行確認形狀(單一文件 vs NDJSON)。
  2. 若是 NDJSON,串流即可。
  3. 若是紀錄陣列,使用串流解析器框住該陣列。
  4. 若是深度巢狀文件,用 JSONPath 抽出感興趣的部分再串流。
  5. 要把結果交付他人,展平成 CSV——見 JSON 轉 CSV:攤平巢狀資料

延伸閱讀