用axios或内置fetch请求XML接口拿到response,再用fast-xml-parser或xml2js把字符串转成JSON对象,处理完之后就别再转回XML了,保持JSON结构贯穿业务逻辑。这是目前Node.js生态里最主流、代码量最少的处理路径,无论你是对接Java老系统、第三方支付回调,还是政府数据接口,这条链路基本够用。
先搞清楚你拿到的是什么:字符串,不是对象
很多新手卡在第一步,是因为看到控制台打印出XML就以为拿到了“XML对象”,Node.js里所有HTTP响应体到了你手里,默认都是字符串(除非设置了responseType: 'stream'或arraybuffer),你打印出来的缩进和标签,只是字符串的排版效果。
这一点想通了,后面的问题就变成:怎么把这个字符串变成JavaScript对象,记住这个核心转变,剩下的都是工具选择问题。
请求XML接口的两种主流姿势
用axios,设置responseType为’text’
axios默认会把响应体按JSON解析,遇到XML会直接抛错,所以请求XML接口时,必须显式指定responseType: 'text',代码长这样:
import axios from 'axios';
const response = await axios.get('https://api.example.com/data.xml', {
responseType: 'text',
headers: { 'Accept': 'application/xml' }
});
// 此时response.data是纯字符串
const xmlString = response.data;
要点是responseType必须和Accept头一起设,有些服务端很死板,你不声明Accept: application/xml它就不给你XML,反而返回JSON或者HTML错误页。
用内置fetch,Node.js 18+无脑首选
Node.js 18以后内置了fetch,不需要装任何依赖就能发请求,处理XML时的坑是:res.text()才能拿到字符串,res.json()会直接炸,正确姿势:
const response = await fetch('https://api.example.com/data.xml', {
headers: { 'Accept': 'application/xml' }
});
const xmlString = await response.text();
如果服务端返回的XML编码是GBK或GB2312(国内某些老接口很常见),response.text()会乱码,这时候需要拿arrayBuffer再手动解码:
const buffer = await response.arrayBuffer();
const xmlString = new TextDecoder('gbk').decode(buffer);
这条路径看着绕,但就是很多国内开发者会遇到的实际场景。
解析XML的两种方案,选型看你的项目阶段
fast-xml-parser:快、轻、配置直观
如果你只需要把XML变成对象,不做DOM操作,不关心节点顺序,那就用这个,好处是体积小、解析速度快、API极其简单:
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
ignoreAttributes: false, // 保留属性,默认是丢弃的
attributeNamePrefix: '@_', // 属性名的前缀标记
trimValues: true, // 去掉首尾空格
parseTagValue: false // 防止“123”被自动转成数字
});
const result = parser.parse(xmlString);
有个小坑必须提醒:parseTagValue默认是true,会把<age>25</age>解析成数字25,把<active>true</active>解析成布尔值,大多数情况下这不是问题,但如果你要做的接口里面XML节点值带前导零(比如订单号00123),就会被转成数字123,然后你就 debug 到怀疑人生,直接设成false,拿到的全是字符串,自己手动转换。
xml2js:老牌稳定,但回调地狱和性能问题并存
xml2js在npm上的周下载量依然很高,很多老项目在用,它的问题有两个:一是默认API是回调式,用promisify包一下才能await;二是遇到大文件时内存占用明显比fast-xml-parser高,代码长这样:
import { parseStringPromise } from 'xml2js';
const result = await parseStringPromise(xmlString, {
explicitArray: false, // 单一节点不包数组
trim: true
});
对比一下两个库的解析结果结构:
| 对比维度 | fast-xml-parser | xml2js |
|---|---|---|
| 解析速度 | 快 | 慢,大文件差距明显 |
| 属性处理 | 需要ignoreAttributes控制 |
自带符号标记 |
| 数组策略 | 默认自动判定 | 需要explicitArray控制 |
| 依赖体积 | 小 | 中等 |
| 维护活跃度 | 高 | 较低,更新频率低 |
行业共识认为,新项目直接选fast-xml-parser,老项目如果已经在用xml2js也别急着重构,等真要动这块代码时再顺手换掉。
解析后必做的三个检查
解析完XML拿到JSON对象后,别急着用,先跑下面三件事:
- 检查根节点是否变成数组,有些XML结构里子节点只有一个,解析器会把它变成对象,但后续接口又可能返回多个节点,导致程序直接崩,最好的办法是封装一个
toArray函数:
const toArray = (data) => Array.isArray(data) ? data : [data];
-
检查命名空间,XML里带
xmlns的命名空间前缀(比如<ns:Response>),fast-xml-parser默认把它们当普通标签处理,结果你的data.result可能变成data['ns:result'],遇到这种情况,用removeNSPrefix: true配置项一键去掉。 -
检查CDATA内容,XML里的
<![CDATA[ ... ]]>块,fast-xml-parser默认会转成字符串,但如果CDATA里塞的是HTML或另一段XML,解析后可能被转义,看实际输出,必要时用cdataPropName配置单独提取。
带XML的POST请求:请求体序列化才是大头
接收XML只是半个问题,你的服务器可能不仅需要接收,还要向外发XML请求,这时候真正麻烦的是把JS对象序列化成XML。
比如你要向某个支付接口提交订单,对方要求请求体必须是:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<order>
<orderId>2026001</orderId>
<amount>99.5</amount>
<items>
<item>
<sku>ABC123</sku>
<quantity>2</quantity>
</item>
</items>
</order>
直接用模板字符串手拼不是不行,但容易漏转义特殊字符(比如金额里的&、订单备注里的<),正确做法是使用xml2js的Builder或者fast-xml-parser的XMLBuilder反向序列化:
import { XMLBuilder } from 'fast-xml-parser';
const builder = new XMLBuilder({
ignoreAttributes: false,
format: true, // 输出带缩进,方便调试
});
const xmlBody = builder.build({
order: {
orderId: '2026001',
amount: 99.5,
items: {
item: {
sku: 'ABC123',
quantity: 2
}
}
}
});
注意Builder不会自动生成<?xml ...?>声明头,如果服务端严格要求带声明,手动在前面拼一行就完事。
接一个真实的爬虫场景来走通全流程
假设你需要爬取某个政府公开的天气XML数据接口,这个接口返回结构比较复杂,有属性、有嵌套、有命名空间,完整流程如下:
第一步:请求数据
const xmlString = await fetch('http://www.weather.com.cn/data/cityinfo/101010100.xml', {
headers: { 'Accept': 'application/xml' }
}).then(res => res.text());
第二步:解析并提取目标字段
import { XMLParser } from 'fast-xml-parser';
const parser = new XMLParser({
ignoreAttributes: false,
removeNSPrefix: true,
trimValues: true
});
const data = parser.parse(xmlString);
// 假如结构是 <resp><city id="101010100">北京</city><temp>23</temp></resp>
const city = data.resp.city; // 北京
const temp = data.resp.temp; // 23
第三步:清洗数据
const result = {
cityName: String(city),
temperature: Number(temp),
fetchedAt: new Date().toISOString()
};
然后这个result就可以直接存数据库或返回到你的前端接口,行业里很多爬虫框架(比如Apify、Puppeteer)内部也是这套逻辑。
处理CDATA陷阱和非法XML字符
XML标准和JSON最大的区别之一就是转义要求极其严格,JSON里字符串可以塞任何Unicode字符,但XML里有些字符是连转义都救不了的。
最常见的坑是控制字符,比如0x00到0x1F这些ASCII控制字符,在XML 1.0标准里根本不允许出现,如果服务端返回的XML里混入了这些字符(别觉得奇怪,某些老系统导出数据时就会带),fast-xml-parser会直接抛Invalid character错误,解决办法是解析前先清洗:
const cleanXml = xmlString.replace(/[x00-x08x0Bx0Cx0E-x1F]/g, '');
CDATA也不省心,当CDATA块里包含]]>时,XML标准规定必须拆分成两个CDATA块,但很多半吊子实现不遵守,你要做好心理准备,多写一个正则兜底。
关于性能的一个忠告:别在大循环里重复new解析器
很多人写爬虫时会把XMLParser放在循环里每次new一个,这是完全没必要的开销。XMLParser是纯函数式解析器,实例本身是无状态的,完全可以全局只建一个,复用到底。
你只需要注意:parser.parse()方法接收字符串,返回新对象,不会修改原字符串,也不会保留上次解析的残留状态,所以放心复用,性能能提升一个量级。
为什么推荐你先用JSON网关挡一道
做后端架构时,很多人会直接把XML接口暴露给前端调用,这是大忌,前端处理XML的能力参差不齐,而且XML体积通常比JSON大30%到50%,比较好的做法是:你的Node.js服务作为中转层,接收XML、解析成JSON、再以JSON格式转发给前端。
这样做的额外好处是:如果上游XML结构变了,你只需要改Node.js层,前端完全无感知,很多大厂在实际项目中也是这么做的,比如对接微信支付API(虽然它现在已经提供了JSON版本)或者银行接口时,中间必有一层XML转JSON的代理服务。
Q&A
node.js怎么解析xml数据比用正则更稳定?
正则解析XML只是看着能用,XML的嵌套层级、属性顺序、自闭合标签、CDATA、注释,每一个都够正则喝一壶,用fast-xml-parser这类专门库,解析逻辑是状态机驱动的,天然处理嵌套和转义,你只需要关心解析后的JSON结构,不用操心标签匹配。
node.js 爬虫 服务器返回xml怎么处理多层级数组节点?
多层级数组是XML转JSON里的经典大坑,关键策略是:解析前用isArray配置项显式声明哪些路径一定是数组,比如isArray: (tagName) => tagName === 'item',这样不管上游返回单个还是多个item节点,你拿到的都是数组,遍历就不会炸,别依赖解析器的自动判断,那个太不靠谱。
上游XML接口变了导致程序崩溃,怎么提升容错性?
核心做法是加防御性解析大纲,解析后立即检查关键字段是否存在,比如用可选链或者解构默认值,同时包一层try-catch,捕获解析错误后记录原始XML字符串,方便事后排查,更进一步,可以给请求加超时和自动重试机制,注意容错和隐蔽错误是两回事,日志一定要打完整。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/749014.html





