JavaScript 中 XPath 的使用简介

本文档介绍了在 JavaScript 中使用 XPath 的接口。使用 XPath 的主要接口是 document 对象的 evaluate 函数。

document.evaluate()

此方法根据基于 XML 的文档(包括 HTML 文档)评估 XPath 表达式,并返回一个 XPathResult 对象,该对象可以是一个节点或一组节点。此方法的现有文档位于 document.evaluate,但目前对于我们的需求来说过于简略;下面将提供更全面的说明。

js
const xpathResult = document.evaluate(
  xpathExpression,
  contextNode,
  namespaceResolver,
  resultType,
  result,
);

参数

evaluate() 方法总共接受五个参数

  • xpathExpression:一个包含要评估的 XPath 表达式的字符串。

  • contextNode:文档中的一个节点,xpathExpression 将根据该节点及其所有子节点进行评估。document 节点是最常用的节点。

  • namespaceResolver:一个函数,它将被传递 xpathExpression 中包含的任何命名空间前缀,并返回一个表示与该前缀关联的命名空间 URI 的字符串。这使得 XPath 表达式中使用的前缀与文档中可能使用的不同前缀之间可以进行转换。该函数可以是

    • 一个 Node,它提供了一个 Node.lookupNamespaceURI 方法来解析命名空间前缀。
    • null,可用于 HTML 文档或未使用命名空间前缀时。请注意,如果 xpathExpression 包含命名空间前缀,这将导致抛出代码为 NAMESPACE_ERRDOMException
    • 一个自定义的用户定义函数。有关详细信息,请参阅附录中的“使用用户定义的命名空间解析器”部分。
  • resultType:一个 常量,指定评估后要返回的所需结果类型。最常传递的常量是 XPathResult.ANY_TYPE,它将以最自然的类型返回 XPath 表达式的结果。附录中包含 可用常量 的完整列表。它们将在下面的“指定返回类型”部分中进行解释。

  • result:如果指定了现有的 XPathResult 对象,它将被重用以返回结果。指定 null 将创建一个新的 XPathResult 对象。

返回值

返回一个类型为 resultType 参数中指定XPathResult 对象。

实现默认命名空间解析器

我们使用 document 对象作为命名空间解析器。

js
const nsResolver =
  contextNode.ownerDocument === null
    ? contextNode.documentElement
    : contextNode.ownerDocument.documentElement;

然后将 nsResolver 变量作为 namespaceResolver 参数传递给 document.evaluate

注意:XPath 定义的没有前缀的 QNames 仅匹配空命名空间中的元素。在 XPath 中,无法像常规元素引用(例如,p[@id='_my-id'] 对于 xmlns='http://www.w3.org/1999/xhtml')那样获取默认命名空间。要匹配非空命名空间中的默认元素,您必须使用诸如 ['namespace-uri()='http://www.w3.org/1999/xhtml' and name()='p' and @id='_my-id'] 形式(这种方法对于命名空间可能未知

描述

使任何 DOM 节点适应解析命名空间,以便可以根据 XPath 表达式在文档中出现的节点上下文轻松评估 XPath 表达式。此适配器的工作方式类似于 DOM 3 级方法 lookupNamespaceURI,它使用 lookupNamespaceURI 调用时节点层次结构中可用的当前信息,从给定前缀解析 namespaceURI。它还正确解析隐式 xml 前缀。

指定返回类型

document.evaluate 返回的变量 xpathResult 可以由单个节点(简单类型)或节点集合(节点集类型)组成。

简单类型

resultType 中指定的所需结果类型是以下之一时

  • NUMBER_TYPE - 双精度浮点数
  • STRING_TYPE - 字符串
  • BOOLEAN_TYPE - 布尔值

我们通过分别访问 XPathResult 对象的以下属性来获取表达式的返回值。

  • numberValue
  • stringValue
  • booleanValue
示例

以下使用 XPath 表达式 count(//p) 获取 HTML 文档中 <p> 元素的数量

js
const paragraphCount = document.evaluate(
  "count(//p)",
  document,
  null,
  XPathResult.ANY_TYPE,
  null,
);

console.log(
  `This document contains ${paragraphCount.numberValue} paragraph elements.`,
);

尽管 JavaScript 允许我们将数字转换为字符串进行显示,但如果请求 stringValue 属性,XPath 接口不会自动转换数字结果,因此以下代码会工作

js
const paragraphCount = document.evaluate(
  "count(//p)",
  document,
  null,
  XPathResult.ANY_TYPE,
  null,
);

console.log(
  `This document contains ${paragraphCount.stringValue} paragraph elements.`,
);

相反,它将返回一个代码为 NS_DOM_TYPE_ERROR 的异常。

节点集类型

XPathResult 对象允许以 3 种主要不同类型返回节点集

迭代器

resultType 参数中指定的所需结果类型是以下之一时

  • UNORDERED_NODE_ITERATOR_TYPE
  • ORDERED_NODE_ITERATOR_TYPE

返回的 XPathResult 对象是一个匹配节点的节点集,它将作为迭代器运行,允许我们通过使用 XPathResultiterateNext() 方法访问其中包含的单个节点。

一旦我们遍历了所有单个匹配节点,iterateNext() 将返回 null

但是请注意,如果在迭代之间文档发生更改(文档树被修改),迭代将失效,并且 XPathResultinvalidIteratorState 属性将设置为 true,并抛出 NS_ERROR_DOM_INVALID_STATE_ERR 异常。

js
const iterator = document.evaluate(
  "//phoneNumber",
  documentNode,
  null,
  XPathResult.UNORDERED_NODE_ITERATOR_TYPE,
  null,
);

try {
  let thisNode = iterator.iterateNext();

  while (thisNode) {
    console.log(thisNode.textContent);
    thisNode = iterator.iterateNext();
  }
} catch (e) {
  console.error(`Error: Document tree modified during iteration ${e}`);
}
快照

resultType 参数中指定的所需结果类型是以下之一时

  • UNORDERED_NODE_SNAPSHOT_TYPE
  • ORDERED_NODE_SNAPSHOT_TYPE

返回的 XPathResult 对象是一个匹配节点的静态节点集,它允许我们通过 XPathResult 对象的 snapshotItem(itemNumber) 方法访问每个节点,其中 itemNumber 是要检索的节点的索引。可以通过 snapshotLength 属性访问包含的节点总数。

快照不会随文档更改而改变,因此与迭代器不同,快照不会失效,但它可能不对应于当前文档,例如,节点可能已被移动,它可能包含不再存在的节点,或者可能已添加新节点。

js
const nodesSnapshot = document.evaluate(
  "//phoneNumber",
  documentNode,
  null,
  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
  null,
);

for (let i = 0; i < nodesSnapshot.snapshotLength; i++) {
  console.log(nodesSnapshot.snapshotItem(i).textContent);
}
第一个节点

resultType 参数中指定的所需结果类型是以下之一时

  • ANY_UNORDERED_NODE_TYPE
  • FIRST_ORDERED_NODE_TYPE

返回的 XPathResult 对象是仅第一个匹配 XPath 表达式的节点。可以通过 XPathResult 对象的 singleNodeValue 属性访问此节点。如果节点集为空,此属性将为 null

请注意,对于无序子类型,返回的单个节点可能不是文档顺序中的第一个节点,但对于有序子类型,您保证会获得文档顺序中的第一个匹配节点。

js
const firstPhoneNumber = document.evaluate(
  "//phoneNumber",
  documentNode,
  null,
  XPathResult.FIRST_ORDERED_NODE_TYPE,
  null,
);

console.log(
  `The first phone number found is ${firstPhoneNumber.singleNodeValue.textContent}`,
);

ANY_TYPE 常量

resultType 参数中的结果类型指定为 ANY_TYPE 时,返回的 XPathResult 对象将是表达式评估自然产生的任何类型。

它可以是任何简单类型(NUMBER_TYPE、STRING_TYPE、BOOLEAN_TYPE),但是,如果返回的结果类型是节点集,那么它将UNORDERED_NODE_ITERATOR_TYPE

要在评估后确定该类型,我们使用 XPathResult 对象的 resultType 属性。此属性的常量值在附录中定义。

示例

在 HTML 文档中

以下代码旨在放置在 HTML 文档中或链接到 HTML 文档的任何 JavaScript 片段中,XPath 表达式将针对该文档进行评估。

要使用 XPath 提取 HTML 文档中的所有 <h2> 标题元素,xpathExpression"//h2"。其中,// 是递归下降运算符,它匹配文档树中任意位置的 nodeName 为 h2 的元素。完整的代码如下:链接到 XPath 入门文档

js
const headings = document.evaluate(
  "//h2",
  document,
  null,
  XPathResult.ANY_TYPE,
  null,
);

请注意,由于 HTML 没有命名空间,我们已为 namespaceResolver 参数传递了 null

由于我们希望在整个文档中搜索标题,我们已将 document 对象本身用作 contextNode

此表达式的结果是一个 XPathResult 对象。如果希望知道返回结果的类型,我们可以评估返回对象的 resultType 属性。在这种情况下,它将评估为 4,即 UNORDERED_NODE_ITERATOR_TYPE。这是 XPath 表达式结果为节点集时的默认返回类型。它一次提供对单个节点的访问,并且可能不按特定顺序返回节点。要访问返回的节点,我们使用返回对象的 iterateNext() 方法

js
let thisHeading = headings.iterateNext();

let alertText = "Level 2 headings in this document are:\n";

while (thisHeading) {
  alertText += `${thisHeading.textContent}\n`;
  thisHeading = headings.iterateNext();
}

一旦我们迭代到一个节点,我们就可以访问该节点上的所有标准 DOM 接口。在迭代完表达式返回的所有 h2 元素之后,任何对 iterateNext() 的进一步调用都将返回 null

附录

实现用户定义的命名空间解析器

这仅为说明目的的示例。此函数需要从 xpathExpression 中获取命名空间前缀,并返回与该前缀对应的 URI。例如,表达式

'//xhtml:td/mathml:math'

将选择作为 (X)HTML 表格数据单元格元素子项的所有 MathML 表达式。

为了将 mathml: 前缀与命名空间 URI http://www.w3.org/1998/Math/MathML 关联,并将 xhtml: 与 URI http://www.w3.org/1999/xhtml 关联,我们提供了一个函数

js
function nsResolver(prefix) {
  const ns = {
    xhtml: "http://www.w3.org/1999/xhtml",
    mathml: "http://www.w3.org/1998/Math/MathML",
  };
  return ns[prefix] || null;
}

我们对 document.evaluate 的调用将如下所示

js
document.evaluate(
  "//xhtml:td/mathml:math",
  document,
  nsResolver,
  XPathResult.ANY_TYPE,
  null,
);

为 XML 文档实现默认命名空间

如前文“实现默认命名空间解析器”中所述,默认解析器不处理 XML 文档的默认命名空间。例如,对于此文档

xml
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <entry />
    <entry />
    <entry />
</feed>

doc.evaluate('//entry', doc, nsResolver, XPathResult.ANY_TYPE, null) 将返回一个空集,其中 nsResolver 是任何 Node。传递 null 解析器也无法更好地工作。

一种可能的解决方法是创建一个自定义解析器,返回正确的默认命名空间(在本例中为 Atom 命名空间)。请注意,您仍然需要在 XPath 表达式中使用一些命名空间前缀,以便解析器函数能够将其更改为所需的命名空间。例如,

js
function resolver() {
  return "http://www.w3.org/2005/Atom";
}
doc.evaluate("//myns:entry", doc, resolver, XPathResult.ANY_TYPE, null);

请注意,如果文档使用多个命名空间,则需要更复杂的解析器。

下一节将介绍一种可能更有效的方法(并且允许命名空间无需提前知道)。

使用 XPath 函数引用具有默认命名空间的元素

另一种匹配非空命名空间中的默认元素的方法(对于命名空间可能未知

获取特定命名空间元素和属性,无论前缀如何

如果希望通过不一定要求在使用特定前缀来查找命名空间元素或属性时提供命名空间的灵活性(正如它们所预期的那样),则必须使用特殊技术。

虽然可以调整上一节中的方法来测试命名空间元素,无论选择的前缀如何(结合使用 local-name()namespace-uri() 而不是 name()),但是,如果希望在谓词中获取具有特定命名空间属性的元素(考虑到 XPath 1.0 中缺少与实现无关的变量),则会出现更具挑战性的情况。

例如,可能会尝试(错误地)按以下方式获取具有命名空间属性的元素:const xpathLink = someElements[local-name(@*)="href" and namespace-uri(@*)='http://www.w3.org/1999/xlink'];

如果其中一个属性的本地名称为 href,但它是具有目标 (XLink) 命名空间的不同属性(而不是 @href),则可能会无意中获取一些元素。

为了准确获取具有 XLink @href 属性的元素(同时不受命名空间解析器中预定义前缀的限制),可以按以下方式获取它们

js
const xpathEls =
  'someElements[@*[local-name() = "href" and namespace-uri() = "http://www.w3.org/1999/xlink"]]'; // Grabs elements with any single attribute that has both the local name 'href' and the XLink namespace
const thisLevel = xml.evaluate(xpathEls, xml, null, XPathResult.ANY_TYPE, null);
let thisItemEl = thisLevel.iterateNext();

XPathResult 定义常量

结果类型定义常量 描述
ANY_TYPE 0 一个结果集,包含表达式评估自然产生的任何类型。请注意,如果结果是节点集,则 UNORDERED_NODE_ITERATOR_TYPE 始终是结果类型。
NUMBER_TYPE 1 包含单个数字的结果。例如,这在使用 count() 函数的 XPath 表达式中很有用。
STRING_TYPE 2 包含单个字符串的结果。
BOOLEAN_TYPE 3 包含单个布尔值的结果。例如,这在使用 not() 函数的 XPath 表达式中很有用。
UNORDERED_NODE_ITERATOR_TYPE 4 一个结果节点集,包含所有匹配表达式的节点。这些节点不一定按它们在文档中出现的顺序排列。
ORDERED_NODE_ITERATOR_TYPE 5 一个结果节点集,包含所有匹配表达式的节点。结果集中的节点按它们在文档中出现的相同顺序排列。
UNORDERED_NODE_SNAPSHOT_TYPE 6 一个结果节点集,包含所有匹配表达式的节点的快照。这些节点不一定按它们在文档中出现的顺序排列。
ORDERED_NODE_SNAPSHOT_TYPE 7 一个结果节点集,包含所有匹配表达式的节点的快照。结果集中的节点按它们在文档中出现的相同顺序排列。
ANY_UNORDERED_NODE_TYPE 8 一个结果节点集,包含任何匹配表达式的单个节点。该节点不一定是文档中第一个匹配表达式的节点。
FIRST_ORDERED_NODE_TYPE 9 一个结果节点集,包含文档中第一个匹配表达式的节点。

另见

原始文档信息

  • 基于 James Graham 的原始文档。
  • 其他贡献者:James Thompson。