Geonode logo
Geonode Team

Geonode Team

更新于:2026年10月7日

发布于:2026年9月2日

如何在 JavaScript 中解析 XML

浏览器内置了 XML 解析器,而 Node.js 则没有。正是这一差异,解释了围绕该话题的大部分困惑。 在浏览器中,`DOMParser` 无需任何依赖即可处理 XML,但有一个值得注意的特性:遇到格式错误的输入时,它不会抛出异常。而在 Node 中,你需要选择一个库,而这一选择会影响后续所有代码的编写方式。 本指南将同时介绍这两种情况,此外还涵盖了命名空间、XPath 以及常让开发者中招的安全问题。

我们之所以写这篇文章,是因为我们是 Geonode,我们向数据采集者出售代理服务,而其中大量数据正是以 XML 格式传输的——例如网站地图、RSS 和 Atom 源、SOAP 响应以及产品目录。 **需要坦诚指出的是,解析失败几乎从来都不是网络问题。**如果您的 XML 解析器报错,请在修改任何代码之前,先输出接收到的数据的前 200 个字符。十有八九,那其实是一个 HTML 错误页面,而解析器准确地报告了您收到的并非 XML 数据。

在浏览器中:DOMParser

内置功能,无需依赖项。

const parser = new DOMParser();
const doc = parser.parseFromString(xmlString, "application/xml");

const titles = doc.querySelectorAll("item > title");
titles.forEach(t => console.log(t.textContent));

MDN文档 列出的支持的MIME类型包括:text/html

、text/xml

、application/xml

、application/xhtml+xml

和 image/svg+xml

。它会返回一个Document

,该对象“具有一个contentType

属性,其值与给定的mimeType

相匹配”,具体返回的是HTMLDocument

还是XMLDocument

,取决于您的请求内容。

处理 XML 时请使用 application/xml

。若传入 text/html

,则会调用 HTML 解析器,该解析器在某些方面较为宽松,可能会悄无声息地更改您的文档——它不会遵循 XML 的大小写敏感规则,并且会毫无顾忌地接受 XML 禁止的内容。

解析完成后,您将获得一个 DOM。您所了解的关于 DOM 遍历的所有知识均适用:querySelector

、querySelectorAll

、getElementsByTagName

、children

、textContent

。

错误陷阱:它不会抛出异常

这种行为总会在初次使用时让所有人中招。

向 DOMParser 传入格式错误的 XML 时,它并不会抛出异常。MDN 明确指出:“返回的 XMLDocument 将包含一个描述解析错误的 <parsererror> 节点”,且该错误“也可能报告给浏览器的 JavaScript 控制台”。

因此你必须进行以下检查:

const doc = parser.parseFromString(xmlString, "application/xml");
const errorNode = doc.querySelector("parsererror");
if (errorNode) {
  throw new Error(`XML parse failed: ${errorNode.textContent}`);
}

如果不进行此检查,格式错误的文档会生成一个 Document 对象,其中本应包含数据的位置却被错误信息占据。随后调用的 querySelectorAll 将返回空值,而这种现象看起来像是选择器问题,而非解析失败。

用以下代码包裹一次:

function parseXml(text) {
  const doc = new DOMParser().parseFromString(text, "application/xml");
  const err = doc.querySelector("parsererror");
  if (err) {
    throw new Error(
      `XML parse failed: ${err.textContent.trim()}. ` +
      `First 200 chars: ${text.slice(0, 200)}`
    );
  }
  return doc;
}

text.slice(0, 200) 这一部分能节省时间。解析错误会告诉你输入无效;而前 200 个字符则表明这是一个 HTML 错误页面。

在 Node 中:选择库

Node 没有内置的 XML 解析器,因此这取决于所选的依赖项。每周下载量可以大致反映其采用情况——以下数据来自 npm 注册表,截至 2026 年 9 月。

库每周下载量类型
sax

| ~88.9M | 流式处理,基于事件 | | fast-xml-parser

| ~85.0M | 将 XML 转换为纯 JavaScript 对象 | | @xmldom/xmldom

| ~48.7M | 适用于 Node 的 DOM 实现 | | xml2js

| ~44.5M | XML 转对象,支持回调和 Promise API | | xpath

| ~12.2M | XPath 查询,与 xmldom 配合使用 |

**fast-xml-parser

** 是大多数场景下实用的默认选择。它将 XML 转换为普通的 JavaScript 对象,这意味着你可以使用点表示法进行遍历,而非 DOM 方法:

import { XMLParser } from "fast-xml-parser";

const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@" });
const obj = parser.parse(xmlString);
console.log(obj.rss.channel.item[0].title);

需要理解的权衡:对象转换在某一方面存在数据丢失。出现一次的元素会变成一个对象;同一个元素出现两次则会变成一个数组。因此,对于包含三个项目的源,channel.item

是一个数组;而对于仅包含一个项目的源,它则是一个对象——任何假设其为数组的代码在仅有一个项目的情况下都会出错。 大多数库都提供了一项选项,可让命名元素始终生成数组,对于任何需要迭代的内容,在问题出现之前启用此选项都是值得的。

**@xmldom/xmldom

** 可在 Node 中提供真正的 DOM,这在您希望同一段代码在两种环境中都能运行,或者需要使用 XPath 时尤为重要。将其与 xpath

包配合使用:

import { DOMParser } from "@xmldom/xmldom";
import xpath from "xpath";

const doc = new DOMParser().parseFromString(xmlString, "text/xml");
const titles = xpath.select("//item/title/text()", doc);

**sax

** 是一个流式解析器,会在读取时触发事件。对于那些大到无法驻留内存的文档(例如多吉字节级的网站地图索引或批量目录导出),DOM 方法根本无法胜任,而它正是解决这些问题的方案。

**xml2js

** 历史悠久且应用广泛。其 API 虽然略显过时,但完全能满足使用需求,且已有大量代码在使用它。

命名空间:导致真实文档出错的元凶

这是原本能正常工作的选择器突然无法找到任何元素的最常见原因。

许多实际的 XML 格式都会声明命名空间:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url><loc>https://example.com/</loc></url>
</urlset>

该声明 xmlns

将所有元素置于默认命名空间中。在支持命名空间的解析器中,<loc>

并非简单地等同于 loc

—— 它实际上是位于 sitemaps 命名空间中的 loc

,而单纯的 getElementsByTagName("loc")

可能无法找到任何内容。

有三种处理方法,按正确性由低到高排序。

使用支持命名空间的方法:

const NS = "http://www.sitemaps.org/schemas/sitemap/0.9";
const locs = doc.getElementsByTagNameNS(NS, "loc");

使用通配符命名空间,当你不在意具体是哪个命名空间时:

const locs = doc.getElementsByTagNameNS("*", "loc");

配置库以忽略命名空间。 大多数对象映射库都提供了一个选项,用于去除命名空间前缀,从而生成纯 loc

形式的键。这种方法很方便,并且会静默合并两个名称相同但实际不同的元素——对于站点地图来说可以接受,但对于混合了多种词汇表的文档来说则很危险。

请注意,在此情况下 querySelector

的行为与 getElementsByTagNameNS

不同:CSS 选择器有其特有的命名空间语法,这种语法既笨拙又很少使用,因此对于带命名空间的 XML,使用 NS

方法或 XPath 更为可靠。

JavaScript 中的 XPath

在浏览器中可通过 document.evaluate 访问,在 Node 中可通过 xpath 包(配合 xmldom 使用)访问。

const result = doc.evaluate(
  "//item/title/text()",
  doc,
  null,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  null
);
for (let i = 0; i < result.snapshotLength; i++) {
  console.log(result.snapshotItem(i).nodeValue);
}

该 API 语法较为冗长,以至于大多数人只用过一次就不再理会了。但它之所以值得花时间去了解,是因为 XPath 能够表达 CSS 选择器无法实现的功能——例如匹配文本内容、导航至父节点,以及基于同级元素的位置逻辑。

有两个限制。浏览器实现的是 XPath 1.0,因此不支持 matches()、lower-case() 以及 ends-with()。此外,命名空间需要一个解析函数(即第三个参数),用于将前缀映射到命名空间 URI。传递 null 仅适用于不包含命名空间的文档,这会排除大多数真实的 RSS 源。

对于浏览器中的命名空间文档:

const resolver = prefix => ({ sm: "http://www.sitemaps.org/schemas/sitemap/0.9" }[prefix] || null);
const result = doc.evaluate("//sm:loc/text()", doc, resolver, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

请注意,无论文档实际使用什么前缀,您都需要自行指定前缀(此处为 sm),因为 XPath 1.0 没有默认命名空间的概念。

将 XML 转换为 JSON,以及会丢失什么

这实际上是人们最常需要的功能,但需要明白的是,这种转换并非无损的。

XML 和 JSON 具有不同的数据模型。XML 包含属性、元素、文本节点、注释、处理指令、命名空间和顺序关系;而 JSON 包含对象、数组、字符串、数字、布尔值和 null。有四类内容在转换过程中无法完整保留。

属性与子元素的区别。 <item id="1"><name>x</name></item> 同时包含一个属性与一个子元素,而 JSON 并不区分二者。库通过在属性键前添加前缀(通常为 @ 或 $)来处理此问题,您需要自行配置并牢记这些前缀:

const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@" });
// { item: { "@id": "1", name: "x" } }

重复元素转换为数组时结果不一致。 虽然上文已提及,但值得再次强调,因为这是该领域最常见的错误:出现一次会生成对象,出现两次则生成数组。对于所有打算迭代的元素,请为库设置“always array”选项。

混合内容无法得到干净的表示。 <p>Hello <b>world</b>!</p> 将文本和元素交织在一起。转换为对象后,文本片段及其相对于子元素的位置难以表示,大多数库要么将文本拼接起来,要么舍弃其中的一部分。 如果您的 XML 包含散文标记,对象转换并非正确的方法——请保留 DOM。

顺序无法保证。 JSON 对象键在数据模型中没有定义的顺序,因此对于元素顺序具有语义的文档,该信息将丢失。数组会保留顺序;而名称不同的同级元素则不会。

除非您特别指定,否则一切都会被转换为字符串。 XML 没有数据类型,因此 <price>42.50</price> 会被视为文本。大多数库都提供数值强制转换功能,这虽然方便,但会将以零开头的产品代码转换为数字,并将版本字符串转换为浮点数。对于任何作为标识符而非数量的数据,请关闭强制转换功能。

实用建议:对象转换适用于数据型 XML(如数据源、产品目录、配置、API 响应),其中元素是记录和字段。对于文档型 XML(即标记嵌入在正文中且结构承载语义的情况),请使用 DOM 进行处理。

安全:XXE 与注入

两种截然不同的风险,但都切实存在。

XML 外部实体处理。 XML 可以声明引用外部资源的实体,包括本地文件和网络 URL。 能够解析这些实体的解析器可能会被利用,从而读取服务器上的文件或代表攻击者发起请求。这是一种经典且至今仍很常见的漏洞类型。

缓解措施是在您使用的任何解析器中禁用外部实体和 DTD 处理。 浏览器 DOMParser 不解析外部实体,因此默认情况下浏览器是安全的。Node 库各不相同,这一点值得核查而非想当然——如果您在 Node 中解析来自不可信来源的 XML,请在发布前确认解析器的实体处理机制。

重新插入 DOM 时的注入。 MDN 警告称,parseFromString“是一个‘注入接收器’,如果输入来自攻击者,则可能成为 XSS 攻击的潜在载体”。 这里有一个关键区别:使用 text/html 时,“<script> 元素会被标记为不可执行,且事件处理程序不会被调用”——但如果“解析后的文档随后被注入到可见的 DOM 中”,脚本“仍会运行”。

因此,解析过程是安全的;但将结果插入 DOM 则不安全。 MDN的建议是:传递TrustedHTML对象而非字符串,通过CSP的require-trusted-types-for指令强制执行受信任的类型,并使用DOMPurify等库通过TrustedTypePolicy进行数据净化。

简单规则:切勿在未进行数据净化处理的情况下,将已解析的不可信标记插入到实时DOM中;当仅需文本内容时,应优先使用textContent而非innerHTML。

实用模式

解析 RSS 或 Atom 源:

const doc = parseXml(await res.text());
const items = [...doc.getElementsByTagNameNS("*", "item")].map(item => ({
  title: item.getElementsByTagNameNS("*", "title")[0]?.textContent?.trim(),
  link:  item.getElementsByTagNameNS("*", "link")[0]?.textContent?.trim(),
  date:  item.getElementsByTagNameNS("*", "pubDate")[0]?.textContent?.trim(),
}));

通配符命名空间无需分支即可同时处理 RSS 和 Atom,而可选的链式调用可处理缺少字段的源——这在绝大多数源中都很常见。

解析站点地图(包括索引文件):

const doc = parseXml(xml);
const isIndex = doc.documentElement.localName === "sitemapindex";
const locs = [...doc.getElementsByTagNameNS("*", "loc")].map(n => n.textContent.trim());
// if isIndex, these are sitemap URLs to fetch; otherwise they are page URLs

检查文档元素的 localName

属性是区分两者的可靠方法,因为两者都包含 <loc>

元素,仅包装元素不同。

处理大型文档 —— 使用流式解析器而非构建 DOM:

import sax from "sax";
const stream = sax.createStream(true, { trim: true });
let current = null;
stream.on("opentag", node => { if (node.name === "loc") current = ""; });
stream.on("text", t => { if (current !== null) current += t; });
stream.on("closetag", name => { if (name === "loc") { emit(current); current = null; } });

无论文档大小如何,内存占用保持恒定,代价是需要编写一个小型状态机。

大家还问

如何在 JavaScript 中解析 XML?

在浏览器中,使用内置的 DOMParser:new DOMParser().parseFromString(xml, "application/xml") 会返回一个 Document,你可以使用 DOM 方法对其进行查询。在 Node 中没有内置解析器,因此需要安装一个——fast-xml-parser 用于对象转换,@xmldom/xmldom 用于真正的 DOM。

为什么 DOMParser 在遇到无效 XML 时不抛出异常?

这是设计使然。它不会抛出异常,而是会在返回的文档中包含一个描述错误的 <parsererror> 节点。你必须通过 doc.querySelector("parsererror") 显式检查该节点,否则格式错误的文档将悄无声息地返回空查询结果。

Node.js 是否有内置的 XML 解析器?

没有。与 JSON 不同,XML 需要依赖第三方库。常用的选项包括:fast-xml-parser 和 xml2js(用于转换为对象)、@xmldom/xmldom(用于 DOM 实现),以及 sax(用于流式处理超大文档)。

为什么我的 XML 选择器找不到任何内容?

通常是命名空间的问题。声明 xmlns 的文档会将所有元素置于该命名空间中,而简单的 getElementsByTagName 可能无法匹配。请使用 getElementsByTagNameNS 并指定命名空间 URI,或使用 "*" 作为通配符,或者配置您的库以忽略命名空间。

如何在 JavaScript 中使用 XPath 处理 XML?

在浏览器中,使用 document.evaluate 并指定 XPathResult 类型——其输出足够详细,只需包裹一次即可。在 Node 中,可使用 xpath 包配合 @xmldom/xmldom。请注意,浏览器仅实现 XPath 1.0,且命名空间文档需要一个将前缀映射到 URI 的解析函数。

在 JavaScript 中解析 XML 是否存在安全风险?

有两个风险。 XML 外部实体的处理可能会导致解析器读取本地文件或发出请求——浏览器中的 DOMParser 不会解析外部实体,但 Node 库的情况各不相同,应加以核查。此外,将解析后的不可信标记插入到实时 DOM 中可能会执行脚本,因此插入前应进行净化处理。

如何解析非常大的 XML 文件?

请使用流式解析器(如 sax),该解析器在读取时会触发事件,而非在内存中构建文档。这种方式可在恒定内存占用下处理任意大小的文件,但需要编写一个小型状态机来跟踪当前位置。

为什么我解析的 XML 有时返回数组,有时返回对象?

这是因为对象映射库仅在元素重复出现时才会生成数组。包含三个项的源数据会返回数组;而包含一个项的相同源数据则会返回对象。大多数库都提供了一个选项,可将命名元素始终映射为数组——对于任何需要迭代的内容,请启用此选项。

总结

这在很大程度上取决于运行环境。在浏览器中,你使用的是 DOMParser,且无需依赖任何库;而在 Node 中,你需要选择一个库,而这一选择将决定后续所有代码的编写方式。

有两种行为占用了人们大部分的时间。DOMParser 会通过 parsererror 节点报告失败,而不是抛出异常,因此格式错误的文档会返回空结果,看起来像是选择器问题——请检查该节点,并记录输入的前 200 个字符,因为这通常是 HTML 错误页面。 此外,命名空间会悄无声息地破坏对普通标签名的查找,而这恰恰发生在你最想解析的文档上:网站地图、RSS 源和 SOAP 响应都会声明命名空间。

除此之外,还要根据文档规模选择合适的工具。 对于普通文档,使用对象映射;当需要 XPath 或共享浏览器与服务器代码时,使用真正的 DOM;当文件过大无法加载时,则使用流式解析器。如果源文件不可信,在发布前请检查 Node 解析器对外部实体的处理机制——这方面的问题二十年来一直被归类为安全漏洞,至今依然如此。