csv-parse 导入后 a 变成愀:UTF-16LE BOM 与 trim 的顺序问题
csv-parse7.0.3自动识别UTF-16LE BOM后没有同步重建裁剪字节,开启trim可能静默改变字符。确认输入协议后提前指定encoding,并对照字段原值及导入记录。
CSV 导入没有抛异常,原来的 a、b 两个字段却变成了“愀”“戀”。如果输入是带 BOM 的 UTF-16LE,使用 csv-parse 7.0.3,并同时开启 bom: true 与 trim: true,要检查自动检测编码后留下的裁剪状态。
下面是用于说明输入字节的最小例子,不涉及读取真实业务文件:
import { parse } from 'csv-parse/sync';
const input = Buffer.from('\ufeff a , b ', 'utf16le');
const records = parse(input, { bom: true, trim: true });
这条路径可能静默改变字符。导入任务正常结束、列数正确,都不能证明字段值仍然正确;在核实前保留源文件,停止把这批结果继续用于覆盖已有数据。
编码已经切换,trim 仍拿着旧字节
7.0.3 初始化时,会按当时的 encoding 编码一组空白字符,供 ltrim 等裁剪逻辑使用。随后识别 UTF-16LE BOM 时,解析器重新规范化 options,却没有同步重建这份状态。
于是,字段按双字节编码读取,左侧空白判断仍可能按单字节识别并丢掉其中一个字节,剩余字节错位后形成另一个合法字符。trim 包含左裁剪,单独使用 ltrim 也要检查。它与“选错编码后直接解码失败”不同,不能靠捕获异常发现全部错误。
已确认 UTF-16LE,初始化时就传入 encoding
对于文件协议明确为带 BOM 的 UTF-16LE 的入口,把编码提前交给解析器,使裁剪状态从一开始就按同一编码建立:
import { parse } from 'csv-parse/sync';
function parseUtf16leCsvWithBom(raw) {
if (raw.length < 2 || raw[0] !== 0xff || raw[1] !== 0xfe) {
throw new Error('预期为带 BOM 的 UTF-16LE CSV');
}
if (raw.length % 2 !== 0) {
throw new Error('UTF-16LE 输入的字节长度异常');
}
return parse(raw, {
encoding: 'utf16le',
bom: true,
trim: true,
});
}
raw 是按原样取得的 Buffer。示例拒绝没有对应 BOM 的输入,是这个入口的明确约定,不是说所有 UTF-16LE 文件必须带 BOM;偶数字节长度也只是结构检查,不保证整份文件与业务内容都有效。
若同一导入入口接收 UTF-8 与 UTF-16LE,应在解析器创建前按已约定的文件头和来源规则选择编码,再创建相应实例。没有 BOM 也没有外部编码约定时,应报告未知输入,而不是按“来自 Windows”或字段里有中文就猜成 UTF-16LE。
原来的 columns、delimiter、cast、记录长度和错误处理选项仍应合并保留。不能为了避免裁剪错位而随意关闭全部 trim:这可能让字段前后空格进入标识符,改变匹配和去重结果。
对照字符内容,再决定是否重新导入
截至 2026 年 10 月 25 日,正式 7.0.3 的 BOM 分支仍只更新 options,没有重建上述裁剪状态。显式编码处理的是这条初始化顺序,不等于修复了所有 CSV 编码问题。
验证应覆盖带前后空格的 ASCII 字段、非 ASCII 字符、引号包住的字段,以及应用实际允许的空白。对照字段原值、行列数和业务键,避免只看第一条记录不报错。
已经写入错误结果时,从保留的原始字节重新解析,按应用原有事务和去重规则修复。不要把“愀”再做一次字符串替换来猜回 a;真实文件中同样的字符可能本来就合法,替换无法恢复丢掉的字节边界。


