在 JavaScript 中使用 XPath 的簡介

本文件介紹了在 JavaScript 中使用 XPath 的介面。Mozilla 實現了很多 DOM 3 XPath,這意味著 XPath 表示式可以針對 HTML 和 XML 文件執行。

使用 XPath 的主要介面是 evaluate 函式,該函式屬於 document 物件。

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 物件。

返回值

返回 xpathResult,它是型別為 指定的 resultType 引數的 XPathResult 物件。XPathResult 介面定義 此處

實現預設名稱空間解析器

我們使用 document 物件作為名稱空間解析器。

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

然後將 nsResolver 變數作為 namespaceResolver 引數傳遞給 document.evaluate

注意:XPath 將沒有字首的 QName 定義為僅匹配空名稱空間中的元素。XPath 中無法獲取應用於常規元素引用的預設名稱空間(例如,對於 xmlns='http://www.w3.org/1999/xhtml'p[@id='_myid'])。要匹配非空名稱空間中的預設元素,您要麼必須使用諸如 ['namespace-uri()='http://www.w3.org/1999/xhtml' and name()='p' and @id='_myid'] 這樣的形式來引用特定元素(此方法 適用於名稱空間可能未知的動態 XPath),要麼使用帶字首的名稱測試,並建立一個將字首對映到名稱空間的名稱空間解析器。如果您希望採用後一種方法,請閱讀有關 如何建立使用者定義的名稱空間解析器 的更多資訊。

描述

使任何 DOM 節點能夠解析名稱空間,以便可以輕鬆地相對於節點在文件中出現的上下文評估 XPath 表示式。此介面卡的工作方式類似於 DOM Level 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 物件允許以三種主要不同的型別返回節點集

迭代器

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'。其中,// 是遞迴下降運算子,它匹配文件樹中任何位置的節點名稱為 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 函式引用具有預設名稱空間的元素

匹配非空名稱空間中的預設元素的另一種方法(並且在名稱空間可能未知的動態 XPath 表示式中效果很好)涉及使用諸如 [namespace-uri()='http://www.w3.org/1999/xhtml' and name()='p' and @id='_myid'] 的形式引用特定元素。這避免了 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。