JSON 工具箱

JSONL 檔案實操:開啟、轉換與壞行排錯

結論:整份 .jsonl 丟給 JSON.parse 卻報錯,多半不是檔案壞了,而是讀取方式錯了。這篇給出檢視與逐行讀取的標準寫法、與 JSON 陣列互轉的指令、六類壞行的排查表,以及串流介面裡逐行消費的正確姿勢。

實戰約 9 分鐘發佈於 2026-11-06更新於 2026-11-06
JSONL 檔案逐行讀取與壞行排查實操封面

結論:JSONL 的每一行都是獨立的合法 JSON,但整個檔案不是一份 JSON。整檔案丟給 JSON.parse 或 json.load 報錯,十有八九不是檔案壞了,而是讀取方式用錯了。

一行一條記錄、行間沒有逗號、沒有外層陣列——JSONL 用「放棄整體結構」換來了附加寫入、串流讀取和壞一行不毀全量。這三樣好處,全都要靠「逐行處理」才能兌現,也全都會在「整檔案一把 parse」的時候變成報錯。

一、先分清三個名字:JSONL、NDJSON、JSON Lines

同一件事有三個叫法,剛接觸的人常以為它們有語法差別,其實沒有:

名字 常見場景 後綴 / 類型
JSON Lines 資料工程、ML 資料集(pandas、Spark、Hugging Face) .jsonl
NDJSON HTTP 串流介面、即時資料 .ndjson、application/x-ndjson
JSONL 日常口語,兩者混稱 —

任何能讀其中一個的工具都能讀另一個,選名字的唯一標準是下游生態的約定:給 Hugging Face 餵資料寫 .jsonl,給串流 API 設回應標頭就用 application/x-ndjson。可攜的 JSONL 檔案只要滿足三條:UTF-8 編碼(無 BOM)、每行一個完整的 JSON 值、行間用 \n 分隔,檔案末尾保留一個換行符是強烈建議而非強制。格式本身的協議細節與設計動機,見《JSON Lines 與 NDJSON 串流資料》,本篇只解決「拿到檔案之後怎麼辦」。

二、開啟與檢視:先 head,再 grep,最後才是編輯器

雙擊用編輯器開啟 500MB 的 .jsonl 是最常見的錯誤動作:3 百萬行文字、語法高亮、撤銷緩衝,三樣東西加起來足以卡死任何編輯器,而這與 JSON 本身無關——解析 45MB 檔案在 Node 22 上只要 208 毫秒,卡的是渲染。行式格式的正確檢視姿勢是用行工具,因為記錄與實體行天然對齊:

# 先看規模:多少條記錄、多大體積,不讀內容
wc -l events.jsonl && du -h events.jsonl

# 看第一條記錄長什麼樣(確認欄位與結構)
head -n 1 events.jsonl | jq .

# 直接找目標記錄:行工具直接可用,這是 JSONL 的特權
grep '"level":"error"' events.jsonl | head -n 5
tail -n 100 events.jsonl | jq 'select(.latency_ms > 500)'

注意最後一條:tail 出來的每一行仍是獨立 JSON,可以直接進 jq,不需要先拼陣列。這套姿勢對 GB 級檔案同樣成立,記憶體佔用基本為零——這正是行式格式的核心收益,與大 JSON 為什麼會拖慢頁面是同一個原理的兩個側面。

三、逐行讀取的三種標準寫法

逐行讀取要處理三件事:跳過空行(尤其檔案末尾的換行)、報錯帶行號、壞行不終止整個處理。三種語言的標準寫法都圍繞這三點。

Python 用標準庫,不用裝任何套件:

import json

with open("events.jsonl", encoding="utf-8") as f:
    for line_number, line in enumerate(f, start=1):
        if not line.strip():          # 跳過空行
            continue
        try:
            record = json.loads(line) # 每行獨立解析
        except json.JSONDecodeError as e:
            print(f"第 {line_number} 行解析失敗: {e}")  # 報行號,不終止
            continue                  # 壞行隔離,繼續讀
        handle(record)

Node 用內建 readline,大檔案記憶體佔用恆定:

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

const input = fs.createReadStream("events.jsonl", "utf8");
const lines = readline.createInterface({ input, crlfDelay: Infinity });
let lineNumber = 0;
for await (const line of lines) {
  lineNumber += 1;
  if (!line.trim()) continue;
  try {
    handle(JSON.parse(line));
  } catch (e) {
    console.error(`Line ${lineNumber}: ${e.message}`);
  }
}

Go 1.27 起 encoding/json/v2 已預設啟用,逐值串流解碼正合適;要嵌入 jq 表達式則選 gojq。套件選型上別急著裝包:Python 的 jsonlines 報錯自帶行號與原始行文字,orjsonl 能直接讀寫 gzip/zstd 壓縮檔;Node 的 ndjson 套件 2020 年後就沒發版,用內建 readline 就夠了。

四、與 JSON 陣列互轉:方向不同,指令不同

互轉是最常見的需求,方向搞反指令就全錯。記住一個口訣:陣列轉 JSONL 是 map(每條記錄壓成一行),JSONL 轉陣列是 join(行收集成列表)。

# JSON 陣列 → JSONL:-c 壓平每個元素
jq -c '.[]' records.json > records.jsonl

# JSONL → JSON 陣列:-s 把整個輸入收成一個陣列
jq -s '.' events.jsonl > records.json

# JSONL 逐行過濾:不需要 -s,jq 天然按輸入流處理
jq -c 'select(.active == true)' events.jsonl > active.jsonl

Python 一行式等價寫法:陣列轉 JSONL 對每條記錄 json.dumps(r, ensure_ascii=False) + "\n";反向則逐行 json.loads 後 list() 收集。兩個方向各有一個陷阱:轉過去(陣列→JSONL)時,記錄內部若含真實換行必須由 json.dumps 轉義成 \n,否則會把一條記錄撕成多行;轉回來(JSONL→陣列)時別用 "".join(lines) 拼成一個大字串再 parse——行數一多就是一次不必要的全量記憶體拷貝。聚合類操作(求和、分組、平均)在 jq 裡必須 -s 全量載入,GB 級檔案請換 awk 或 Python 串流累計。

五、六類壞行與排查順序

JSONL 的報錯幾乎都發生在「行」這一層,逐行校驗、報行號,是唯一靠譜的排查方式。按出現頻率排:

現象 根因 修法
行尾報 Extra data 行間加了逗號 刪逗號,每行必須獨立成立
一條記錄被拆成多行 記錄內部有未轉義的真實換行(手工 pretty-print 過) 用 json.dumps/jq -c 重新壓行
首行報 Unexpected token ï 檔案帶 BOM 去 BOM 存為 UTF-8 無簽名
末行報錯但內容正常 末尾缺換行或混入尾隨空格 補 \n,空行按約定跳過
行內字串含換行報錯 字串裡的換行沒轉義成 \n 檢查產生端,別手工拼 JSON 字串
大整數讀出來變了值 解析端過了 double(2^53 截斷) 讀端換保真解析,見大整數精度

排查動作固定三步:先 head -n 1 確認編碼與首行結構;再用逐行腳本把壞行號全部吐出來(而不是碰到第一個就停);最後單獨把壞行複製出來看——一行就是一個完整記錄,單獨看毫無負擔。批次匯入場景的容錯策略建議「隔離而非終止」:壞行連同行號落盤到 errors.jsonl,好記錄繼續入庫,跑完再看壞行占比決定是否重導。

六、串流介面裡的 JSONL:逐行消費與背壓

NDJSON 在 HTTP 上的典型用法是伺服端持續寫、用戶端持續讀。用戶端用 fetch 時別調 response.json()——它會把整個回應緩衝完才解析,串流的意義就沒了。正確姿勢是直接讀 body 串流,按 \n 切分,殘行緩衝到下一輪:

const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += value;
  const lines = buffer.split("\n");
  buffer = lines.pop();               // 最後一段可能是不完整行
  for (const line of lines) {
    if (line.trim()) handle(JSON.parse(line));
  }
}

伺服端兩個必做項:一是 res.write() 回傳 false 時要 await 一次 drain 事件(背壓),否則消費端慢於生產端時記憶體會持續上漲;二是串流路由上關掉 gzip 中介軟體——gzip 必須緩衝完整回應才能壓縮,對 NDJSON 串流等於自廢武功。這兩條是本機與多路英文信源交叉確認過的行為,不是猜測。

七、什麼時候不該用 JSONL

格式選型的反面同樣要清楚,三種場景請留在 JSON 陣列:

需求 為什麼 JSONL 不合適 該用什麼
一次校驗全量結構 沒有整體結構,只能逐行驗,欄位漂移難發現 JSON 陣列 + Schema
隨機存取第 N 條 行不等長,無法二分定位 SQLite / Parquet
深巢狀、記錄間共享參照 每行獨立,跨行參照無處安放 標準 JSON 文件

判斷口訣一句話:**記錄隨時間不斷產生或按行消費(日誌、埋點、匯出、訓練集),用 JSONL;一次產生、一次消費、需要整體語義的小載體,用 JSON 陣列。**帶幀協議的場景(RS 位元組包記錄的 JSON Text Sequences)是另一個話題,見RFC 7464 那篇。

FAQ

JSONL 和 NDJSON 到底有沒有區別?

工程上沒有。兩者都是「每行一個完整 JSON 值、無外層包裝、行間無逗號」,任何支持其一的工具都支持另一個。差別只在名頭與生態習慣:.jsonl 多見於資料工程與 ML 資料集,.ndjson 與 application/x-ndjson 多見於串流 HTTP API。對接外部系統時以其文件約定的副檔名與媒體類型為準。

.jsonl 檔案可以直接用 JSON.parse 讀嗎?

不能對整個檔案用。JSON.parse 只接受單個 JSON 文字,而 JSONL 是多個獨立值按行排列,整檔案解析必然報「Unexpected token」或「Extra data」。正確做法是逐行切分後對每個非空行單獨 JSON.parse,報錯時帶上行號。

JSONL 檔案裡有一行壞了,整個檔案就廢了嗎?

不會,這正是這個格式的容錯設計。壞行只影響它自己:逐行處理時跳過壞行、記錄行號、繼續讀後面的行即可。批次匯入建議把壞行隔離到單獨檔案,好記錄照常入庫,跑完統計壞行占比再決定要不要修復重導。

JSONL 怎麼轉成 Excel 或 CSV?

先轉陣列或逐行抽欄位,再走表格轉換:jq -c '.[]' data.jsonl 壓成陣列後交給轉換工具,或 Python 裡逐行 json.loads 進 pandas。注意巢狀欄位展平後欄名會帶點號、陣列欄位只能序列化成儲存格文字,ID 類長數字欄提前宣告成字串避免精度遺失。

參考資料

同系列工具

相關文章

JSON Lines 與 NDJSON 流式資料

一行一條 JSON,換來的是可追加、可流式、壞一行不毀全量;代價是沒有整體結構,校驗要逐行做。

進階約 10 分鐘更新於 2026-10-30

JSON Text Sequences(RFC 7464):RS 分隔的串流 JSON

RFC 7464 用 0x1E 記錄分隔符把多條 JSON 串成可邊寫邊讀的串流,還給了正式媒體類型。本文說明格式規則、與 NDJSON 的差別、按分隔符切分再解析的實作,以及字串裡控制字元的邊界。

進階約 9 分鐘更新於 2026-10-31

大 JSON 的解析、渲染與流式處理

解析往往不是瓶頸,渲染和複製才是。按「解析 → 遍歷 → 渲染 → 傳輸」四層拆開最佳化,才不會白費力氣。

進階約 13 分鐘更新於 2026-10-31