必须用sax流式解析GB级XML:它内存稳定在几MB,需启用命名空间、累积拼接文本、显式设utf8编码、避免字符串拼接和闭包引用,并用writeStream流式输出JSON。

用 sax 而不是 xml2js 或 fast-xml-parser
GB级XML文件一上来就全量解析,内存直接爆掉——xml2js 默认把整个树塞进内存,fast-xml-parser 虽快但仍是同步构建对象,不解决流式问题。必须选真正基于 SAX 的流式解析器。sax 是 Node.js 生态里最轻、最可控的底层 SAX 实现,没有隐藏的缓存层,事件触发即处理,内存占用稳定在几 MB 级别。
实操建议:
- 用
new sax.Parser(true)启用命名空间支持(如果 XML 有xmlns) - 监听
onopentag和onclosetag,而非ontext—— 文本节点常被拆成多次触发,需累积拼接 - 遇到目标节点(比如
<record>)时,用栈结构临时存路径,避免递归嵌套导致的内存泄漏 - 别在
onopentag里直接JSON.stringify,先推到队列,用setImmediate或queueMicrotask批量 flush
fs.createReadStream 必须配 encoding: 'utf8'
XML 文件若含中文或特殊符号,Node.js 默认以 buffer 流式读取,sax 解析器会把多字节字符(如 UTF-8 中的汉字)错切成两段,触发 error 事件并中断流。这不是编码声明的问题(<?xml version="1.0" encoding="UTF-8"?>),而是流解码层缺失。
常见错误现象:sax 报 Invalid character 或 Unexpected close tag,但文件用浏览器打开完全正常。
实操建议:
- 创建流时显式传
{ encoding: 'utf8' }:fs.createReadStream('huge.xml', { encoding: 'utf8' }) - 不要用
pipe()直连sax,先.on('data')捕获 chunk 验证是否乱码(比如打印前 50 字符) - 若源文件是 BOM 开头,
sax不自动跳过,需在第一个datachunk 中手动.replace(/^\uFEFF/, '')
写 JSON 到文件时避免 fs.appendFile 频繁调用
每解析出一个对象就调一次 fs.appendFile,磁盘 I/O 会成为瓶颈,且 Node.js 的 append 操作在 Linux 下本质是 seek + write,GB 文件下性能断崖式下跌。
使用场景:输出为行格式 JSON(NDJSON)、或带根数组的单文件(需前后补 [/])。
实操建议:
- 用
fs.createWriteStream持久化写入,配合writable.write()流式输出 - 如果是 NDJSON,每个对象后加换行:
ws.write(JSON.stringify(obj) + '\n') - 如果是数组格式,首写
'[',中间对象用',\n'分隔,最后'\n]'—— 注意第一个对象前不加逗号,最后一个对象后不加逗号,需用布尔标记 - 务必监听
ws.on('error'),写满磁盘或权限不足时write()不抛异常,只静默失败
内存溢出往往卡在「字符串拼接」和「闭包引用」
很多人以为流式就安全了,结果跑半小时后 RSS 内存涨到 4GB —— 典型原因是:在 onopentag 里把所有属性存进一个全局 currentAttrs = {} 对象,又没及时清空;或在 ontext 回调里用 += 拼接大文本,V8 会不断复制字符串缓冲区。
性能影响:字符串拼接 10MB 文本,可能触发数次堆内存重分配;闭包长期持有已结束标签的上下文,GC 无法回收。
实操建议:
- 用
Array.push()收集文本片段,最后arr.join('')—— 更省内存 - 每次
onclosetag触发后,立刻currentAttrs = null、textContent = [],别依赖 GC - 用
process.memoryUsage().heapUsed在关键节点打点,比如每处理 1000 个<record>就console.log一次,早发现泄漏 - 别在解析器回调里引用外部大对象(比如整个
fs.ReadStream实例),容易意外延长生命周期
真正的难点不在“怎么转”,而在“怎么让状态干净地进、干净地出”。流式不是银弹,它把内存压力从“一次性加载”转成了“状态管理复杂度”,而这个复杂度藏在你删掉的那行 currentText += chunk 里。










