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