2. 简单的 XPath 解析器

import jakarta.xml.namespace.NamespaceContext;
import jakarta.xml.parsers.DocumentBuilder;
import jakarta.xml.parsers.DocumentBuilderFactory;
import jakarta.xml.parsers.ParserConfigurationException;
import jakarta.xml.xpath.XPath;
import jakarta.xml.xpath.XPathConstants;
import jakarta.xml.xpath.XPathExpressionException;
import jakarta.xml.xpath.XPathFactory;

import org.w3c.dom.Document;

public class DefaultParser {
    
    private File file;

    public DefaultParser(File file) {
        this.file = file;
    }
}

现在我们深入分析 DefaultParser 中的核心组件:

FileInputStream fileIS = new FileInputStream(this.getFile());
DocumentBuilderFactory builderFactory = newSecureDocumentBuilderFactory();
DocumentBuilder builder = builderFactory.newDocumentBuilder();
Document xmlDocument = builder.parse(fileIS);
XPath xPath = XPathFactory.newInstance().newXPath();
String expression = "/Tutorials/Tutorial";
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);

拆解上述代码:

为生成 XML 文档的 DOM 树,我们使用 newSecureDocumentBuilderFactory() 创建 builderFactory 实例。此方法内部会配置一个安全的 DocumentBuilderFactory 实例:

该方法通过禁用外部实体和 DTD 相关的危险特性来增强 XML 解析安全性。处理不可信来源的 XML 时,这是防止 XML 漏洞的最佳实践。

DocumentBuilderFactory builderFactory = newSecureDocumentBuilderFactory();
DocumentBuilder builder = builderFactory.newDocumentBuilder();

有了 DocumentBuilder 实例后,即可从多种输入源解析 XML 文档,如 InputStreamFileURLSAX

Document xmlDocument = builder.parse(fileIS);

Document 表示整个 XML 文档,是文档树的根节点,提供数据访问入口:

XPath xPath = XPathFactory.newInstance().newXPath();

通过 XPath 对象执行表达式,从文档中提取所需信息:

xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);

可编译字符串形式的 XPath 表达式,并指定期望返回的数据类型(如 NODESET、NODE 或 String)。

3. 开始实战

掌握基础组件后,我们通过一个简单 XML 示例深入实践:

<?xml version="1.0"?>
<Tutorials>
    <Tutorial tutId="01" type="java">
        <title>Guava</title>
        <description>Introduction to Guava</description>
        <date>04/04/2016</date>
        <author>GuavaAuthor</author>
    </Tutorial>
    <Tutorial tutId="02" type="java">
        <title>XML</title>
        <description>Introduction to XPath</description>
        <date>04/05/2016</date>
        <author>XMLAuthor</author>
    </Tutorial>
</Tutorials>

3.1 获取基础元素列表

使用 XPath 表达式从 XML 中提取节点列表:

FileInputStream fileIS = new FileInputStream(this.getFile());
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = builderFactory.newDocumentBuilder();
Document xmlDocument = builder.parse(fileIS);
XPath xPath = XPathFactory.newInstance().newXPath();
String expression = "/Tutorials/Tutorial";
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);

通过上述表达式可获取根节点下的教程列表。也可使用 "//Tutorial",但此表达式会返回文档中所有 <Tutorial> 节点(无论层级)。

通过指定 NODESET 返回有序节点集合,可通过索引访问节点。

3.2 通过 ID 获取特定节点

通过过滤条件基于 ID 查找元素:

DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = builderFactory.newDocumentBuilder();
Document xmlDocument = builder.parse(this.getFile());
XPath xPath = XPathFactory.newInstance().newXPath();
String expression = "/Tutorials/Tutorial[@tutId=" + "'" + id + "'" + "]";
node = (Node) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODE);

此类表达式称为 谓词,是定位特定数据的利器,例如:

  • /Tutorials/Tutorial[1]
  • /Tutorials/Tutorial[first()]
  • /Tutorials/Tutorial[position()<4]

3.3 按标签名获取节点

引入轴(axes)概念实现高级定位:

Document xmlDocument = builder.parse(this.getFile());
this.clean(xmlDocument);
XPath xPath = XPathFactory.newInstance().newXPath();
String expression = "//Tutorial[descendant::title[text()=" + "'" + name + "'" + "]]";
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);

上述表达式查找包含特定文本 <title> 的所有 <Tutorial> 元素。

基于示例 XML,可查找包含 "Guava" 或 "XML" 的 <title>,获取完整 <Tutorial> 元素。

轴提供灵活的 XML 导航方式,详见官方文档

3.4 在表达式中操作数据

XPath 支持在表达式中直接操作数据:

XPath xPath = XPathFactory.newInstance().newXPath();
String expression = "//Tutorial[number(translate(date, '/', '')) > " + date + "]";
nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);

此场景中,方法接收 "ddmmyyyy" 格式日期字符串,但 XML 存储为 "dd/mm/yyyy"。使用 XPath 内置函数转换格式后匹配结果。

3.5 处理带命名空间的文档

若 XML 定义命名空间(如 example_namespace.xml),检索规则需调整:

<?xml version="1.0"?>
<Tutorials xmlns="/full_archive">
</Tutorials>

此时 "//Tutorial" 将无法匹配结果,因为所有 <Tutorial> 元素位于 /full_archive 命名空间下。

解决步骤:

首先设置命名空间上下文:

xPath.setNamespaceContext(new NamespaceContext() {
    @Override
    public Iterator getPrefixes(String arg0) {
        return null;
    }
    @Override
    public String getPrefix(String arg0) {
        return null;
    }
    @Override
    public String getNamespaceURI(String arg0) {
        if ("bdn".equals(arg0)) {
            return "/full_archive";
        }
        return null;
    }
});

定义 "bdn" 为命名空间 "/full_archive" 的标识。后续 XPath 表达式需包含该前缀:

String expression = "/bdn:Tutorials/bdn:Tutorial";
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(xmlDocument, XPathConstants.NODESET);

3.6 避免空文本节点陷阱

3.3 节代码中解析 XML 后调用了 this.clean(xmlDocument)

当文档包含空文本节点时,遍历元素或子节点可能遇到意外行为。例如调用 node.getFirstChild() 时可能返回空的 "#Text" 节点而非预期结果。

解决方案:遍历文档并移除空节点:

NodeList childNodes = node.getChildNodes();
for (int n = childNodes.getLength() - 1; n >= 0; n--) {
    Node child = childNodes.item(n);
    short nodeType = child.getNodeType();
    if (nodeType == Node.ELEMENT_NODE) {
        clean(child);
    }
    else if (nodeType == Node.TEXT_NODE) {
        String trimmedNodeVal = child.getNodeValue().trim();
        if (trimmedNodeVal.length() == 0){
            node.removeChild(child);
        }
        else {
            child.setNodeValue(trimmedNodeVal);
        }
    } else if (nodeType == Node.COMMENT_NODE) {
        node.removeChild(child);
    }
}

通过检查节点类型,移除不需要的节点(空文本节点、注释等)。

4. 总结

本文探讨了使用标准 Java JDK 进行 XPath 解析的基础知识。Java 默认提供了强大的 XML/HTML 解析、读取和处理支持。

XPath 表达式不仅限于 Java,还可与 XSLT 结合导航 XML 文档。常用库包括 JDOM、Saxon、XQuery、JAXP、Jaxen 和 Jackson。针对 HTML 解析,可使用 JSoup 等专用库。


原始标题:Intro to XPath with Java | Baeldung