NDJSON / JSONL 全解:什么时候比 JSON 数组好用 + 5 个实操踩坑

· 约 4 分钟 NDJSON / JSONL

NDJSON(每行一个 JSON 对象,行间用 \n 分隔)是过去 10 年悄悄成为事实标准的格式:日志收集(Fluentd / Vector)、大数据查询(BigQuery / DuckDB / Athena)、LLM 训练集(OpenAI / HuggingFace fine-tune)全部默认用它。但很多人第一次接触时会把它当成”少了方括号的 JSON 数组”,错过了它真正解决的工程问题。这篇梳理 NDJSON 的设计动机、和 JSON 数组的本质差异,以及 5 个最容易翻车的细节。

NDJSON 解决的核心问题:流式 + 容错

JSON 数组要求整个数据被 [ ] 包裹,逗号分隔。这意味着:

  1. 写入侧:必须知道”是不是最后一条”才能决定要不要加逗号;进程崩溃时少写 ] 整个文件就废了
  2. 读取侧:必须把整个文件加载到内存,调用 JSON.parse 一次性反序列化
  3. 故障:任何一行字符出错(多一个逗号、字符串没闭合)→ 整个文件不可解析

NDJSON 把每条记录独立成一行,绕过了所有这些问题:

维度JSON 数组NDJSON
写入需要维护边界([ 开头、] 结尾、逗号分隔)append + "\n" 即可
读取一次性 parse 全文件按行 parse,常数内存
容错1 个字符错全文件废隔离到行
增量更新需要重写文件或 fseek直接 append
大数据生态不友好原生支持

什么时候选 JSON 数组、什么时候选 NDJSON

不是所有场景都该用 NDJSON。简单的判断:

  • API 响应、配置文件、前端状态 → JSON 对象 / 数组,结构化更重要
  • 日志、事件、数据管道、训练集、超大数据 → NDJSON
  • 数据小(< 10 MB)且一次性消费 → 都可以,JSON 数组更直观
  • 数据大(> 100 MB)或要 append → 必须 NDJSON

流式解析:常数内存读 100 GB

NDJSON 最大的优势是按行读。Node.js 的标准写法:

const rl = readline.createInterface({
  input: fs.createReadStream('events.jsonl'),
  crlfDelay: Infinity,
});
for await (const line of rl) {
  if (!line) continue;
  try {
    const obj = JSON.parse(line);
    process(obj);
  } catch (e) {
    badLines.push({ lineNo: rl.lineNo, line });
  }
}

Python 更简单:

with open('events.jsonl') as f:
    for line in f:
        line = line.strip()
        if not line:
            continue
        try:
            obj = json.loads(line)
            process(obj)
        except json.JSONDecodeError as e:
            bad_lines.append((f.tell(), line))

反模式:把整个文件 readFileSyncsplit("\n") —— 内存里要同时放原始字符串和切分后的数组,100 GB 文件就是 200 GB 占用。

5 个最容易翻车的细节

1. 字符串里的换行没转义

JSON 字符串里的真实换行必须写成 \n(反斜杠 + n 两个字符),不是真换行。JSON.stringify 自动处理;手写 NDJSON 时常忘记。

调试技巧:cat -A file.jsonl | head 看每行结尾是不是 $(LF),中间有没有 ^M(CRLF)或意外换行。

2. CRLF 换行符

Windows 编辑器保存的 NDJSON 是 \r\n。多数 parser 容忍,但严格模式(BigQuery 加载、某些 Java 库)会把 \r 当成行末字符的一部分,导致字段值末尾多个 \r

修复:dos2unix file.jsonlsed -i 's/\r$//' file.jsonl

3. UTF-8 BOM

文件开头的 (3 字节 EF BB BF)—— Windows 记事本存 UTF-8 默认加上。第一行 parse 时变成 {"a":1}JSON.parse 抛 “Unexpected token”。

修复:编辑器选 “UTF-8 without BOM”,或代码里 content.replace(/^/, '')

4. 空行和末尾换行

NDJSON 规范允许末尾有一个换行(也就是空的最后一行),parser 应该忽略空行。但有些工具会把空行当解析错误。

实操:写入侧统一用 obj + "\n"(每行末尾一个换行,文件末尾自然多一个空行);读取侧 if (!line.trim()) continue 跳过。

5. 字段集合不一致

NDJSON 没有 schema,每行字段可以不同。第一行 {"a":1,"b":2} 第二行 {"a":1,"c":3} —— 转 CSV 时表头怎么定?

两遍扫描:第一遍取字段并集(按首次出现顺序),第二遍按这个表头输出,缺失字段填空字符串。工具内置这个逻辑,能直接看到”哪些字段只在部分行里出现”——这是数据质量审计的高价值信号。

NDJSON 在大数据生态的位置

主流大数据工具对 NDJSON 的支持远好于 JSON 数组:

工具NDJSON 支持JSON 数组支持
BigQuery原生加载(source_format=NEWLINE_DELIMITED_JSON不支持,必须先转
Athena原生(org.openx.data.jsonserde.JsonSerDe不支持
DuckDBread_json_auto 默认按行读需要 format='array'
Sparkspark.read.json(path) 默认按行需要 multiline=true
Elasticsearch bulk必须 NDJSON不支持
LLM fine-tune(OpenAI / Anthropic / HF)必须 JSONL不支持

结论:如果数据要进数据仓库或训练流程,源头就用 NDJSON,避免中间转换。

实操检查清单

发布或交付 NDJSON 文件前过一遍:

  • 文件是 LF 换行(不是 CRLF),尤其在 Windows 上生成时
  • 没有 UTF-8 BOM
  • 字符串里的换行已转义为 \n 字面量
  • 空行只能在文件末尾出现(如果有)
  • 用流式 parser 抽样验证,不要 JSON.parse 全文件
  • 字段集合统计过一遍,确认”必填字段”覆盖率符合预期
  • 文件大小 > 1 GB 时考虑切片(按 100 MB / 文件分),便于并行处理
  • 目标系统接受 .jsonl 还是 .ndjson 后缀,按需重命名

❓ 常见问题

NDJSON 和 JSON 数组什么时候选哪个?

核心看是不是流式 / 增量场景JSON 数组适合:(1) 整体作为一个值,需要一次性消费;(2) 嵌套层级深、外层就是个对象(API 响应);(3) 数据量小(< 10 MB),全部加载没压力。NDJSON适合:(1) 日志 / 事件流——一条一行,追加写、按行读;(2) 大数据管道——BigQuery / Athena / DuckDB / Spark 原生读 NDJSON 比 JSON 数组快很多;(3) LLM 训练集 / fine-tune 文件——OpenAI、Anthropic、HuggingFace 全部用 JSONL;(4) 断点续传 / 容错——某行损坏只丢那一行,其余照常;(5) 超大文件(GB 级)——内存里放不下整个 JSON 数组。反例:API 响应 / 配置文件 / 前端状态——继续用 JSON 对象或数组,强行 NDJSON 反而麻烦。

大文件怎么流式处理 NDJSON?

按行读,每行单独 JSON.parseNode.jsreadline 模块(createInterface({ input: fs.createReadStream(path) }))逐行触发 line 事件,一行 parse 一次,处理完释放,整个文件 100 GB 也只占常数内存。Pythonwith open(path) as f: for line in f: obj = json.loads(line) —— 文件对象本身就是行迭代器;处理 GB 级文件用 ijsonorjson 更快。浏览器ReadableStream + TextDecoderStream + 自己实现行分割(按 \n 切,最后一段 buffer 留到下次);或 fetch().body.pipeThrough() 拼装。反模式:(1) 把整个文件 readFileSync 然后 split("\n") —— 内存撑爆;(2) 把 NDJSON 当 JSON 数组 JSON.parse —— 直接抛错(每行不是合法的数组元素,外层没有 []);(3) 用 JSON.parse 包整个文件加上 [ ] —— 行内逗号补全很麻烦,不如老老实实流式。

NDJSON 怎么处理换行符在字符串里的情况?

JSON 字符串里的换行必须转义为 \\n 字面量——这是 NDJSON 唯一硬性约束。正确{"text":"第一行\\n第二行"} 占一行,\\n 是两个字符(反斜杠 + n),不是真换行。错误:原始换行字符直接出现在字符串里,让一条记录跨多行 —— NDJSON parser 按物理行切,会把第二行当独立 JSON 解析失败。自动处理:(1) JSON.stringify 默认就把 \\n \\r \\t 转义掉,不会输出原始换行;(2) Python json.dumps 默认 ensure_ascii=True 也会转义;(3) 手写 NDJSON 行最容易出错——比如把 SQL 查询、长文本贴进 JSON 字段。调试:碰到"行号对不上"的报错,先用 cat -A file.jsonl | head 看每行是不是真的以 $ 结尾(\\n),中间有没有 ^M(CRLF)或裸 \\n。Windows 编辑器存的 NDJSON 经常带 CRLF,多数 parser 容忍但 BigQuery / Athena 严格模式会报错。

NDJSON 和 JSONL 有区别吗?

实质相同,命名不同。(1) JSONL(JSON Lines):jsonlines.org 2014 年定义,要求每行一个有效 JSON 值、UTF-8、\\n 换行、可选末尾换行。(2) NDJSON(Newline Delimited JSON):ndjson.org 同期定义,规则几乎一致。(3) 两者都允许行内是任意 JSON 类型(对象、数组、字符串、数字),但实务里 99% 都是对象生态偏好:(1) .jsonl 后缀更主流——OpenAI、HuggingFace、Anthropic fine-tune、大多数 ML 工具链;(2) .ndjson 后缀偏老一点,Elasticsearch bulk API 用、部分日志库用;(3) BigQuery / Athena / DuckDB 都接受两种后缀;(4) HTTP Content-Type 一般是 application/x-ndjsonapplication/jsonl实操:选 .jsonl 兼容性最好,写代码两个后缀都能读,别拘泥细节。

NDJSON 转 CSV 字段不一致怎么办?

先扫一遍取字段并集,再按字段顺序输出问题场景:第一条 {"a":1,"b":2}、第二条 {"a":1,"c":3}、第三条 {"d":4} —— 字段集合不固定,强行写 CSV 表头会丢列。两种策略:(1) 两遍扫描——第一遍只取所有键的并集(保留首次出现顺序或按字母序),第二遍按这个表头逐行输出,缺失字段填空字符串;(2) 单遍 + 动态表头——边读边累积已知字段,但只能在写入完成后回填表头,不适合流式输出。嵌套对象怎么办:(1) 拍平——{"user":{"name":"A"}} → 列名 user.name,值 "A";用 _.flatten / flat-keys 库;(2) JSON 字符串列——保留 user 列存 {"name":"A"},下游再处理;(3) 拒绝转换——嵌套层级深时 CSV 本身不合适,转 Parquet 或保留 NDJSON。陷阱:(1) 数组字段(tags:["a","b"])→ CSV 里要决定用 ; 分隔还是 JSON 字符串,没有标准;(2) null 和空字符串在 CSV 里无法区分。

NDJSON 比 JSON 数组容错性强在哪?

故障隔离到行级JSON 数组的脆弱性:1 个字符出错 → 整个文件 JSON.parse 失败 → 100 万条记录全报废。NDJSON 的隔离:第 50 万行损坏 → parser 跳过这一行(或记下行号继续),其余 99.99% 数据照常入库。实操:(1) 流式读取时 try { JSON.parse(line) } catch { logBadLine(lineNo, line) } —— 错误行号 + 原始内容写到 bad.log,后续人工修复;(2) 数据管道(Fluentd / Logstash / Vector)默认对 NDJSON 用宽松解析,单行错误不阻塞流;(3) 写入侧用 fs.appendFile(path, JSON.stringify(obj) + "\\n") —— 进程崩溃只丢最后一行,前面的全保住。JSON 数组写入侧的痛:(1) 必须维护"是不是第一条要不要加逗号"的状态;(2) 进程崩溃时缺末尾 ] → 整个文件失效,需要补脚本;(3) 追加新数据要重写整个文件或用 fseek 改最后字符,麻烦且不安全。结论:append-only 场景一律选 NDJSON。

打开 NDJSON / JSONL NDJSON/JSONL 与 JSON 数组、CSV 互转·错误行号精确定位·字段集合统计·本地解析