前言
XPath (XML Path Language) 是在 XML 文檔中導航、查詢、提取數據的標準查詢語言。Java 內建 `javax.xml.xpath` 包 (JDK 1.5+) 提供標準 API。本文系統覆蓋 XPath 七大節點類型、路徑表達式、謂語、通配符、軸、運算符、函數,並深入講解 Java 代碼實戰、命名空間陷阱、返回類型處理、效能優化與現代替代方案。
一、XPath 數據模型:七種節點類型
| 節點類型 | 說明 | 範例 |
| 元素節點 | XML 標籤 | <book>...</book> |
| 屬性節點 | 標籤屬性 | <book id="1"> |
| 文本節點 | 標籤內文本 | <title>Harry Potter</title> |
| 命名空間節點 | xmlns 宣告 | xmlns:book="https://example.com/book" |
| 處理指令節點 | ... ?> | |
| 註釋節點 | <!-- ... --> | <!-- 這是註釋 --> |
| 文檔節點 (根節點) | 整個文檔頂層 | / (斜槓代表文檔節點) |
二、核心語法速查表
1. 基礎路徑表達式
| 表達式 | 含義 | 範例結果 |
nodename | 選取當前節點的所有子元素 (非所有後代) | `bookstore` 選取 bookstore 下所有子元素 |
/ | 從文檔節點 (根) 開始選取 (絕對路徑) | `/bookstore` 選取根元素 bookstore |
// | 從任意位置 選取匹配節點 (遞歸後代),不考慮層級 | `//book` 選取文檔中所有 book 元素 |
. | 當前節點 | 相對路徑起點 |
.. | 當前節點的父節點 | 向上導航 |
@ | 選取屬性 | `//@lang` 選取所有 lang 屬性 |
2. 謂語 - 精準過濾
//book[1] // 第一個 book 子元素 (索引從 1 開始)
/bookstore/book[last()] // 最後一個 book
/bookstore/book[position() < 3] // 前兩個 book
//title[@lang] // 擁有 lang 屬性的 title
//title[@lang='eng'] // lang 屬性值為 'eng' 的 title
/bookstore/book[price > 35.00] // price > 35 的 book
/bookstore/book[price > 35.00]/title // 符合條件 book 的 title
3. 通配符
| 符號 | 匹配 |
* | 任意元素節點 |
@* | 任意屬性節點 |
node() | 任意類型節點 (元素、屬性、文本、註釋...) |
| | 合併多個路徑結果 (聯集) |
//book/title | //book/price // 所有 book 的 title 和 price
//title | //price // 文檔中所有 title 和 price
4. 軸 - 樹關係導航 (進階)
| 軸名稱 | 含義 | 簡寫 |
| child | 子元素 | `book` 等同 `child::book` |
| attribute | 屬性 | `@lang` 等同 `attribute::lang` |
| descendant | 所有後代 (子、孫...) | `//book` 等同 `descendant::book` |
| ancestor | 所有祖先 (父、祖父...) | 無 |
| parent | 父節點 | `..` |
| following-sibling | 後面的同級節點 | 無 |
| preceding-sibling | 前面的同級節點 | 無 |
| self | 當前節點 | `.` |
5. 運算符與函數
- 算術:`+ - * div mod` (注意除法用 `div`,非 `/`)
- 比較:`= != < > <= >=`
- 邏輯:`and or not()`
- 常用函數:`contains()`, `starts-with()`, `substring()`, `string-length()`, `normalize-space()`, `translate()`, `count()`, `sum()`, `floor()`, `ceiling()`, `round()`
三、Java XPath API 實戰核心類
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.*;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import java.io.File;
public class XPathDemo {
public static void main(String[] args) throws Exception {
// 1. 解析 XML 文檔 (建議復用 DocumentBuilder)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true); // 關鍵:啟用命名空間感知
Document doc = dbf.newDocumentBuilder().parse(new File("books.xml"));
// 2. 建立 XPath 對象
XPathFactory xpf = XPathFactory.newInstance();
XPath xpath = xpf.newXPath();
// 3. 編譯表達式 (可復用 XPathExpression)
XPathExpression expr = xpath.compile("//book/title/text()");
// 4. 執行查詢並指定返回類型 (核心!)
// XPath 1.0 四種類型對應 Java 類型:
// node-set -> NodeList, number -> Double, string -> String, boolean -> Boolean
NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
// 5. 遍歷結果
for (int i = 0; i < nodes.getLength(); i++) {
System.out.println(nodes.item(i).getNodeValue()); // 文本節點用 getNodeValue()
}
}
}
返回類型對應表 (必須指定,否則預設 String 丟失節點結構)
| XPath 類型 | Java 類型 (XPathConstants) | 適用場景 |
| node-set | XPathConstants.NODESET → NodeList | 提取多個節點、需進一步遍歷屬性/子元素 |
| node-set | XPathConstants.NODE → Node | 提取單個節點 |
| string | XPathConstants.STRING → String | 提取文本內容、concat 結果 |
| number | XPathConstants.NUMBER → Double | 計算 sum、count 等數值 |
| boolean | XPathConstants.BOOLEAN → Boolean | 條件判斷 `boolean(//book)` |
四、命名空間處理:最易踩的坑
問題現象
XML 帶命名空間:`<bookstore xmlns="https://example.com/book">...`。直接用 `//book` **查無結果**,因為無前綴路徑默認匹配無命名空間的節點。
解決方案:自定義 NamespaceContext
import javax.xml.namespace.NamespaceContext;
import javax.xml.xpath.*;
import java.util.*;
public class CustomNamespaceContext implements NamespaceContext {
private final Map prefixMap = new HashMap<>();
public CustomNamespaceContext(Map prefixMap) {
this.prefixMap.putAll(prefixMap);
// XML 常數
this.prefixMap.put("xml", XMLConstants.XML_NS_URI);
}
@Override public String getNamespaceURI(String prefix) {
return prefixMap.getOrDefault(prefix, XMLConstants.NULL_NS_URI);
}
@Override public String getPrefix(String namespaceURI) {
return prefixMap.entrySet().stream()
.filter(e -> e.getValue().equals(namespaceURI))
.map(Map.Entry::getKey).findFirst().orElse(null);
}
@Override public Iterator getPrefixes(String namespaceURI) {
List list = new ArrayList<>();
prefixMap.forEach((k, v) -> { if (v.equals(namespaceURI)) list.add(k); });
return list.iterator();
}
}
// 使用
Map nsMap = Map.of(
"bk", "https://example.com/book", // 給默認命名空間起別名 bk
"ns", "https://example.com/other"
);
xpath.setNamespaceContext(new CustomNamespaceContext(nsMap));
// 查詢時必須使用前綴
NodeList books = (NodeList) xpath.compile("//bk:book").evaluate(doc, XPathConstants.NODESET);
核心規則:XML 中有命名空間 (哪怕是默認命名空間 `xmlns="..."`),XPath 查詢必須通過 `NamespaceContext` 映射前綴,**無前綴路徑永遠匹配不到有命名空間的節點**。
五、效能優化與最佳實踐
- 編譯一次,多次執行:`XPathExpression expr = xpath.compile(...)` 耗時較大,應在初始化階段編譯緩存,運行期復用 `expr.evaluate(...)`。
- 避免 `//` 深度遞歸:`//book` 掃描整棵樹。若結構固定,用 `/bookstore/book` 絕對路徑或相對路徑 `bookstore/book`。
- 啟用 `NamespaceAware` 但僅在必要時處理 NS:解析階段 `setNamespaceAware(true)` 有微小開銷,但查詢時無 NS 則無額外成本。
- 大文檔改用流式處理:DOM 將整個 XML 載入記憶體。> 50MB 文檔建議用 StAX (XMLStreamReader) + XPath (需第三方庫如 Saxon) 或 SAX 事件驅動。
- 考慮 Saxon (XPath 2.0/3.0/3.1):JDK 內建僅支援 XPath 1.0。Saxon 提供正則匹配、高階函數、JSON 支援、更優化的查詢引擎。Maven: `net.sf.saxon:Saxon-HE`。
六、現代替代方案對比
| 方案 | 優點 | 缺點 | 適用場景 |
| JDK javax.xml.xpath | 零依賴、標準 API、XPath 1.0 完整 | 僅 XPath 1.0、DOM 記憶體佔用大、命名空間繁瑣 | 中小 XML、無額外依賴限制 |
| Saxon-HE | XPath 3.1、XQuery、XSLT 3.0、流式處理、極快 | 體積 ~3MB、LGPL 授權 | 複雜查詢、大文檔、需 XPath 2.0+ 特性 |
| VTD-XML | 非提取式解析、記憶體效率高、支援 XPath、可修改 XML | 開發停止、非標準 API | 高效能修改/查詢大 XML |
| Jackson XML / JAXB | 物件綁定、類型安全、開發效率高 | 不適合臨時/動態路徑查詢 | 固定結構數據綁定首選 |
七、完整範例:解析帶命名空間的複雜 XML
// books.xml
Effective Java
45.00
Addison-Wesley
Java 核心技術
58.00
機械工業出版社
// Java 查詢:價格 > 50 的書名及出版社
XPathExpression expr = xpath.compile(
"//bk:book[bk:price > 50]/bk:title/text() | //bk:book[bk:price > 50]/pub:publisher/text()"
);
NodeList result = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
// 輸出: Java 核心技術, 機械工業出版社
總結
XPath 是 XML 處理的瑞士軍刀。掌握 路徑表達式、謂語、軸、返回類型映射、命名空間上下文 五大核心,即可應對 95% 的 XML 查詢需求。生產環境注意:緩存編譯表達式、避免濫用 `//`、大文檔用流式處理、複雜查詢升級 Saxon。若 XML 結構固定,優先考慮 JAXB/Jackson 物件綁定,代碼更健壯可維護。