NDJSON(每行一个 JSON 对象,行间用 \n 分隔)是过去 10 年悄悄成为事实标准的格式:日志收集(Fluentd / Vector)、大数据查询(BigQuery / DuckDB / Athena)、LLM 训练集(OpenAI / HuggingFace fine-tune)全部默认用它。但很多人第一次接触时会把它当成”少了方括号的 JSON 数组”,错过了它真正解决的工程问题。这篇梳理 NDJSON 的设计动机、和 JSON 数组的本质差异,以及 5 个最容易翻车的细节。
NDJSON 解决的核心问题:流式 + 容错
JSON 数组要求整个数据被 [ ] 包裹,逗号分隔。这意味着:
- 写入侧:必须知道”是不是最后一条”才能决定要不要加逗号;进程崩溃时少写
]整个文件就废了 - 读取侧:必须把整个文件加载到内存,调用
JSON.parse一次性反序列化 - 故障:任何一行字符出错(多一个逗号、字符串没闭合)→ 整个文件不可解析
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))
反模式:把整个文件 readFileSync 再 split("\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.jsonl 或 sed -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) | 不支持 |
| DuckDB | read_json_auto 默认按行读 | 需要 format='array' |
| Spark | spark.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后缀,按需重命名