用 StreamReader
逐行读取 JSONL 文件最稳
JSONL(也叫 NDJSON)本质就是每行一个合法 JSON 对象,不能当完整 JSON 数组去解析。直接用
JsonSerializer.Deserialize<list>>()</list>会报
JsonException: The input does not contain any JSON tokens或中途崩溃——因为第一行之后的换行符会让反序列化器误判流已结束。
正确做法是按行读,每行单独解析:
用StreamReader打开文件,调用
ReadLine()逐行获取字符串 对每一行非空字符串,传给
JsonSerializer.Deserialize<t>()</t>,别用
DeserializeAsync——它不适用于碎片化输入 遇到某行解析失败(比如字段缺失、类型错),建议记录该行号和原始内容,而不是直接抛异常中断整个读取
示例关键片段:
using var reader = new StreamReader("data.jsonl");
string? line;
int lineNumber = 0;
while ((line = await reader.ReadLineAsync()) != null)
{
lineNumber++;
if (string.IsNullOrWhiteSpace(line)) continue;
try
{
var item = JsonSerializer.Deserialize<MyRecord>(line);
// 处理 item
}
catch (JsonException ex)
{
Console.WriteLine($"Line {lineNumber} parse failed: {ex.Message}");
}
}
写入 JSONL 必须手动控制换行,不能依赖 JsonSerializerOptions.WriteIndented
开启
WriteIndented = true会让每个对象带缩进和多行格式,彻底破坏 JSONL 格式(要求每行严格一个对象,且无换行)。结果是:后续工具(如 jq、pandas.read_json(lines=True))全读不出来,或只读到第一行。
写入时要确保:
每个对象序列化后是单行纯文本,结尾加\n(不是
\r\n,除非明确目标环境只认 Windows 换行) 用
StreamWriter,并显式调用
WriteLine()——它自动补
\n,且不加空格或缩进 避免用
Console.WriteLine()或拼接字符串写入,容易混入不可见字符
示例:
using var writer = new StreamWriter("output.jsonl");
foreach (var item in data)
{
string json = JsonSerializer.Serialize(item, new JsonSerializerOptions
{
WriteIndented = false, // 必须关掉
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
});
await writer.WriteLineAsync(json); // WriteLine 自动加 \n
}
处理大文件时,StreamReader.ReadLineAsync
比同步版更安全
读取几百 MB 甚至 GB 级 JSONL 文件时,同步
ReadLine()会阻塞线程,拖慢整个应用响应;而
ReadLineAsync()配合
ConfigureAwait(false)能释放线程资源,尤其在 ASP.NET Core 后端或长时间运行服务中很关键。
但要注意:
必须在async方法里用,别用
.Result或
.Wait()强制同步——可能死锁
StreamReader默认缓冲区是 1024 字节,如果某一行超长(比如嵌入了 base64 图片),可能触发
ArgumentException: Buffer too small;此时应显式传入更大缓冲区,如
new StreamReader(stream, Encoding.UTF8, true, 65536)不要用
File.ReadLines()——它底层仍是同步读,且无法控制缓冲区大小
别忽略 BOM 和编码问题,UTF-8 with BOM 会让首行解析失败
Windows 记事本保存的 UTF-8 文件常带 BOM(
EF BB BF),导致第一行开头出现非法字符,
JsonSerializer.Deserialize()直接报
InvalidDataException: Invalid prefix或类似错误,但错误信息不提示 BOM。
稳妥做法:
读取前用new StreamReader(stream, Encoding.UTF8, true),第三个参数
detectEncodingFromByteOrderMarks = true会自动跳过 BOM 写入时用
new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))确保不写 BOM 如果必须支持带 BOM 的文件,可在读取后对首行做
line.TrimStart('\uFEFF'),但不如让 StreamReader自动处理干净
真正麻烦的是混合编码文件(比如部分行是 GBK),这种 JSONL 已违反规范,得先统一转码再处理——没有银弹,只能前置清洗。
