XML 解析器

XML 解析常见的方式有三种,分别是:

  • DOM(Document Object Model)
  • SAX(Simple API for XML)
  • StAX(Streaming API for XML),JDK 6.0 开始支持。
DOM

DOM 是基于树形结构的 XML 解析方式,它会将整个 XML 文档读入内存并构建一个DOM树,基于这棵树形结构对各个节点(Node)进行操作。

XML 文档中的每个成分都是一个节点:整个文档是一个文档节点,每个XML 标签对应一个元素节点,包含在XML 标签中的文本是文本节点,每一个 XML 属性是一个属性节点,注释属于注释节点。

DOM 解析方式优点是易于编程,可以根据需求在树形结构的各节点之间导航。例如导航到当前节点的父节点、兄弟节点、子节点等都是比较方便的,这样就可以轻易地获取到自己需要的数据,也可以很容易地添加和修改树中的元素。因为要将整个 XML 文档加载到内存中并构造树形结构,当 XML 文档的数据量较大时,会造成较大的资源消耗。

SAX

SAX 是基于事件模型的 XML 解析方式,与 DOM 解析方式不同的是,不需要将整个 XML 文档加载到内存中,只需将 XML 文档的一部分加载到内存中,即可开始解析,在处理过程中并不会在内存中记录 XML 中的数据,所以占用的资源比较小。当程序处理过程中满足条件时,也可以立即停止解析过程,这样就不必解析剩余的XML 内容。

SAX 的解析过程采用 “推模式”,当 SAX 解析器解析到某类型节点时,会触发注册在该类型节点上的回调函数,开发人员可以根据自己感兴趣的事件注册相应的回调函数,通过这种回调的方式,实现解析。

SAX 的缺点也非常明显,因为不存储 XML 文档的结构,所以需要开发人员自己负责维护业务逻辑涉及的多层节点之间的关系。当XML 文档非常复杂时,维护节点间关系的复杂度较高,工作量也就会比较大。另一方面,因为是流式处理,所以处理过程只能从 XML 文档开始向后单向进行,无法像 DOM 方式那样,自由导航到之前处理过的节点上重新处理,也无法支持XPath。SAX 没有提供写 XML 文档的功能。

StAX

StAX 解析方式与 SAX 解析方式类似,它也是把 XML 文档作为一个事件流进行处理,不同之处在于 StAX 采用的是“拉模式”。所谓“拉模式”是应用程序通过调用解析器推进解析的进程,应用程序控制着整个解析过程的推进,可以简化应用处理 XML 文档的代码,并且决定何时停止解析,而且 StAX 可以同时处理多个 XML 文档。

StAX 实际上包括两套处理 XML 文档的 API,分别提供了不同程度的抽象。一种是基于指针的 API,这是一种低层 API,效率高但抽象程度较低。另一种是基于迭代器的API,它允许应用程序把 XML 文档作为一系列事件对象来处理,效率较略低但抽象程度较高。

XPath

XPath 是一种为查询 XML 文档而设计的语言,它可以与 DOM 解析方式配合使用,实现对 XML 文档的解析。XPath 之于 XML 就好比 SQL 语言之于数据库。

XPath 使用路径表达式来选取 XML 文档中指定的节点或者节点集合,常用的表达式如下

表达式

含义

nodename

选取指定节点的所有子节点

/

从根节点选取指定节点

//

根据指定的表达式,在整个文档中选取匹配的节点,这里并不会考虑匹配节点在文档中的位置

.

选取当前节点


选取当前节点的父节点

@

选取属性

*

匹配任何元素节点

@*

匹配任何属性节点

node()

匹配任何类型的节点

text()

匹配文本节点

|

选取若干个路径

[]

指定某个条件,用于查找某个特定节点或包含某个指定值的节点

运算符

运算符

描述

实例

返回值

|

计算两个节点集

//book | //cd

返回所有拥有 book 和 cd 元素的节点集

+

加法

6 + 4

10

-

减法

6 - 4

2

*

乘法

6 * 4

24

div

除法

8 div 4

2

=

等于

price=9.80

如果 price 是 9.80,则返回 true 否则返回 false。

!=

不等于

price!=9.80

如果 price 不是 9.80,则返回 true 否则返回 false。

<

小于

price<9.80

如果 price 小于 9.80,则返回 true 否则返回 false。

<=

小于或等于

price<=9.80

如果 price 小于等于 9.80,则返回 true 否则返回 false。

>

大于

price>9.80

如果 price 大于 9.80,则返回 true 否则返回 false。

>=

大于或等于

price>=9.80

如果 price 大于等于 9.80,则返回 true 否则返回 false。

or


price=9.80 or price=9.70

如果 price 等于 9.80 或者等于 9.70,则返回 true 否则返回 false。

and


price>9.00 and price<9.90

如果 price 大于 9.00 且小于 9.90,则返回 true 否则返回 false。

mod

计算除法的余数

5 mod 2

1

实战

首先,构造一个 XML 文件,可以自行往里面添加数据,文件放在

<inventory>
    <book year="2000">
        <title>Snow Crash</title>
        <author>Neal Stephenson</author>
        <publisher>Spectra</publisher>
        <isbn>0553380958</isbn>
        <price>14.95</price>
    </book>
    <book year="2005">
        <title>Burning Tower</title>
        <author>Larry Niven</author>
        <author>Jerry Pournelle</author>
        <publisher>Pocket</publisher>
        <isbn>0743416910</isbn>
        <price>5.99</price>
    </book>
    <book year="1995">
        <title>Zodiac</title>
        <author>Neal Stephenson</author>
        <publisher>Spectra</publisher>
        <isbn>0553573862</isbn>
        <price>7.50</price>
    </book>
</inventory>

解析的过程可以分为三步

public class XPathTest {

    public static void main(String[] args) throws Exception {
        // 第一步:使用 DOM 解析器加载 XML 文件
        Document doc = load("inventory.xml");
        // 第二步:构建XPath 对象
        // 创建 XPathFactory
        XPathFactory factory = XPathFactory.newInstance();
        // 创建 XPath 对象
        XPath xpath = factory.newXPath();
        // 第三步:构建 XPathExpression 表达式并执行
        printTitleByAuthor(xpath, doc, "Neal Stephenson");
        printTitleAfterYear(xpath, doc, 1997);
        printDescrAfterYear(xpath, doc, 1997);
    }
}

第一步:使用 DOM 解析器加载 XML 文件,这部分代码如下,返回一个树型结构的 Document 对象。
第二步:创建 XPath 对象,过程很简单,不熬述。

private static Document load(String filename) throws Exception {
    DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
    // 开启验证
    documentBuilderFactory.setValidating(true);
    documentBuilderFactory.setNamespaceAware(false);
    documentBuilderFactory.setIgnoringComments(true);
    documentBuilderFactory.setIgnoringElementContentWhitespace(false);
    documentBuilderFactory.setCoalescing(false);
    documentBuilderFactory.setExpandEntityReferences(true);
    // 创建DocumentBuilder
    DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();
    // 设置异常处理
    builder.setErrorHandler(new ErrorHandler() {
        public void warning(SAXParseException exception) throws SAXException {
            System.out.println("WARN:" + exception.getMessage());
        }
        public void error(SAXParseException exception) throws SAXException {
            System.out.println("error:" + exception.getMessage());
        }
        public void fatalError(SAXParseException exception) throws SAXException {
            System.out.println("fatalError:" + exception.getMessage());
        }
    });
    // 加载文档
    return builder.parse(ClassLoader.getSystemResourceAsStream(filename));
}

第三步:构建 XPathExpression 表达式并执行,本文提供了三个表达式示例

  • 示例1:打印指定作者的图书的标题
    表达式为 //book[author='%s']/title/text() 其中 %s 会被传入的名称字符串替换,
    通过 XPathcompile 方法进行编译生成一个 XPathExpression 对象,
    然后再通过 XPathExpressionevaluate 方法执行查询,
    这个方法的第一个参数用于指定查询的范围,本例中传入的是整个 XML 文档,
    第二个参数用于指定查询返回的结果类型,
    XPathConstants 类中提供了 NODESETBOOLEANNUMBERSTRINGNODE 五种类型
/**
 * 打印指定作者的图书的标题
 * @param author 作者名
 */
private static void printTitleByAuthor(XPath xpath, Document doc, String author) throws Exception {
    System.out.println(String.format("打印作者%s的图书的标题:", author));
    String expression = String.format("//book[author='%s']/title/text()", author);
    // 编译 XPath 表达式
    XPathExpression expr = xpath.compile(expression);
    // 通过 XPath 在 DOM 文档中进行查询,
    // 第一个参数指定 XPath 表达式进行查询的目标上下文节点;
    // 第二个参数指定 XPath 表达式的返回类型。
    NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
    for (int i = 0; i < nodes.getLength(); i++) {
        System.out.println(nodes.item(i).getNodeValue());
    }
}

结果

打印作者Neal Stephenson的图书的标题:
Snow Crash
Zodiac
  • 示例2:打印指定年份之后的图书的标题
/**
 * 打印指定年份之后的图书的标题
 * @param year 年份
 */
private static void printTitleAfterYear(XPath xpath, Document doc, Integer year) throws Exception {
    System.out.println(String.format("查询 %d 年之后的图书的标题:", year));
    // 编译 XPath 表达式
    String expression = String.format("//book[@year>%d]/title/text()", year);
    XPathExpression expr = xpath.compile(expression);
    // 通过 XPath 在 DOM 文档中进行查询,
    NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
    for (int i = 0; i < nodes.getLength(); i++) {
        System.out.println(nodes.item(i).getNodeValue());
    }
}

结果

查询 1997 年之后的图书的标题:
Snow Crash
Burning Tower
  • 示例3:打印指定年份之后的图书的详细信息
/**
 * 打印指定年份之后的图书的详细信息
 * @param year 年份
 */
private static void printDescrAfterYear(XPath xpath, Document doc, Integer year) throws Exception {
    System.out.println(String.format("查询 %d 年之后的图书的属性和标题:", year));
    // 编译 XPath 表达式
    String expression = String.format("//book[@year>%d]/@*|//book[@year>%d]/title/text()", year, year);
    XPathExpression expr = xpath.compile(expression);
    // 通过 XPath 在 DOM 文档中进行查询,
    NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
    for (int i = 0; i < nodes.getLength(); i++) {
        System.out.println(nodes.item(i).getNodeValue());
    }
}

结果

查询 1997 年之后的图书的属性和标题:
2000
Snow Crash
2005
Burning Tower

如果XPath 表达式只使用一次,可以跳过编译步骤直接调用 XPath 对象的 evaluate 方法进行查询。
但是如果同一个XPath 表达式要重复执行多次,则建议先进行编译,然后进行查询,这样性能会好一点。

表达式示例:
以下面的文档为例

<bookstore>
    <book>
      <title lang="eng">Harry Potter</title>
      <price>29.99</price>
    </book>
    <book>
      <title lang="eng">Learning XML</title>
      <price>39.95</price>
    </book>
</bookstore>

表达式

描述

bookstore

选取 bookstore 元素的所有子节点。

/bookstore

选取根元素 bookstore。

注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径!

bookstore/book

选取属于 bookstore 的子元素的所有 book 元素。

//book

选取所有 book 子元素,而不管它们在文档中的位置。

bookstore//book

选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。

//@lang

选取名为 lang 的所有属性。

/bookstore/book[1]

选取属于 bookstore 子元素的第一个 book 元素。

/bookstore/book[last()]

选取属于 bookstore 子元素的最后一个 book 元素。

/bookstore/book[last()-1]

选取属于 bookstore 子元素的倒数第二个 book 元素。

/bookstore/book[position()❤️]

选取最前面的两个属于 bookstore 元素的子元素的 book 元素。

//title[@lang]

选取所有拥有名为 lang 的属性的 title 元素。

//title[@lang=‘eng’]

选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。

/bookstore/book[price>35.00]

选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。

/bookstore/book[price>35.00]/title

选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。

/bookstore/*

选取 bookstore 元素的所有子元素。

//*

选取文档中的所有元素。

//title[@*]

选取所有带有属性的 title 元素

//book/title | //book/price

选取 book 元素的所有 title 和 price 元素。

//title | //price

选取文档中的所有 title 和 price 元素。

/bookstore/book/title | //price

选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。