这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
XML 简介
XML 的应用范围主要体现在以下几个方面:
- 存储数据:内存中的数据需要存储到文件中,才能在关闭系统或系统掉电之后,通过文件进行恢复。现如今,用数据库存储数据这种方式使用得最为广泛。因为数据库管理系统不仅能存储数据,而且提供了众多的管理数据的功能,尤其对大量数据的操作,通常都使用数据库。XML 与数据库相比,最大的优势就是简单、通用。
- 系统配置:如今,许多系统的配置文件都使用 XML 文档。使用 XML 文档进行系统配置,配置修改时不需要重新编译,灵活性强。例如接下来要学习的 Servlet,需要在
web.xml文件中进行配置,Spring 的默认配置文件是applicationContext.xml等。 - 数据交换:在各个分散的应用系统里,因为其平台、系统、数据库、编程语言的差异,保存起来的数据往往只能被本系统调用,形成一个个信息孤岛。整合各个系统的数据信息,或者在两个或多个系统中进行数据交换,往往让 IT 人员非常烦躁。现在利用 XML 交互性好的特点,可以将各个信息孤岛的数据转换成标准的 XML 文件,通过这个标准的 XML 文件进行导入和导出,以达到交换数据的目的。
接下来先看一个 XML 文档,这个文档存放的是租车系统车辆信息,详见程序清单 vehicles.xml :
<?xml version = "1.0" encoding="UTF-8"?>
<!DOCTYPE vehicles [
<!ELEMENT vehicles (cars,trucks)>
<!ELEMENT cars (car*)>
<!ELEMENT trucks (truck+)>
<!ELEMENT car (name,oil,loss,brand)>
<!ATTLIST car id CDATA #REQUIRED>
<!ELEMENT truck (name,oil,loss,load)>
<!ATTLIST truck id CDATA #REQUIRED>
<!ELEMENT name (#PCDATA)>
<!ELEMENT oil (#PCDATA)>
<!ELEMENT loss (#PCDATA)>
<!ELEMENT brand (#PCDATA)>
<!ELEMENT load (#PCDATA)>
]>
<vehicles>
<cars>
<car id="1">
<name>战神</name>
<oil>20</oil>
<loss>0</loss>
<brand>长城</brand>
</car>
<car id="2">
<name>跑得快</name>
<oil>40</oil>
<loss>20</loss>
<brand>红旗</brand>
</car>
</cars>
<trucks>
<truck id="3">
<name>大力士</name>
<oil>20</oil>
<loss>0</loss>
<load>5吨</load>
</truck>
<truck id="4">
<name>大力士二代</name>
<oil>30</oil>
<loss>30</loss>
<load>10吨</load>
</truck>
</trucks>
</vehicles>
通过这个文档可以看出,XML 文档的标签(如、、、等)可以是自定义的,具有可扩展性,这和之后将要学的 HTML 的标签是固定的不同。另外,HTML 的主要作用是通过标签和属性,更好地显示数据,而 XML 是用来存储或交换数据用的,不记录数据的表现形式。
XML 文档总是以 XML 声明开始,即告知处理程序,本文档是一个 XML 文档。在 XML 声明中,通常包括版本、编码等信息,以 <? 开始,以 ?> 结尾。
XML 文档由元素组成,一个元素由一对标签来定义,包括开始和结束标签,以及其中的内容。元素之间可以嵌套(但不能交叉),也就是说元素的内容里还可以包含元素。
标签可以有属性(属性值要加引号),例如 car 标签和 truck 标签都有 id 这个属性。属性是对标签的进一步描述和说明,一个标签可以有多个属性,每个属性都有自己的名字和值,属性是标签的一部分。
DOM 解析 XML
上一小节提到过,XML 文档的应用范围主要有存储数据、系统配置和数据交换。也就是说,作为程序员,需要编写程序读取 XML 文档中的数据,或将数据写入 XML 文档。目前最常用的 XML 解析技术有 DOM 和 SAX。JDK 提供了 JAXP 来使用 DOM 和 SAX,其中 org.w3c.dom 是 W3C 推荐的用于使用 DOM 解析 XML 文档的接口,org.xml.sax 是使用 SAX 解析 XML 文档的接口,javax.xml.parsers 提供处理 XML 文档的类,支持 DOM 和 SAX。本章重点介绍 DOM 解析,对 SAX 解析方法仅进行简要介绍,不展开讲解。
上一小节提到过,XML 文档的应用范围主要有存储数据、系统配置和数据交换。也就是说,作为程序员,需要编写程序读取 XML 文档中的数据,或将数据写入 XML 文档。目前最常用的 XML 解析技术有 DOM 和 SAX。JDK 提供了 JAXP 来使用 DOM 和 SAX,其中 org.w3c.dom 是 W3C 推荐的用于使用 DOM 解析 XML 文档的接口,org.xml.sax 是使用 SAX 解析 XML 文档的接口,javax.xml.parsers 提供处理 XML 文档的类,支持 DOM 和 SAX。本章重点介绍 DOM 解析,对 SAX 解析方法仅进行简要介绍,不展开讲解。
DOM(Document Object Model)是 XML 文档的应用程序接口,它定义了对 XML 文档进行随机访问与操作的方法。DOM 是一个与语言无关、与平台无关的标准接口规范。利用 DOM,程序开发人员可以动态地创建 XML 文档,遍历文档结构,添加、修改、删除文档内容,改变文档的显示方式等。可以这样说,文档代表的是数据,而 DOM 则代表了如何去处理这些数据。
DOM 把一个 XML 文档映射成一个分层对象模型,而这个层次的结构,是一棵根据 XML 文档生成的节点树。DOM 在对 XML 文档进行分析之后,不管这个文档有多简单或多复杂,其中的信息都会被转化成一棵对象节点树。在这棵节点树中,有一个根节点,其他所有的节点都是根节点的子节点。节点树生成之后,就可以通过 DOM 接口访问、修改、添加、删除树中的节点或内容了。 对 DOM 树的操作,主要通过以下几个接口:
Node 接口
Node 接口在整个 DOM 树中具有举足轻重的地位,DOM 接口中有很大一部分接口是从 Node 接口继承过来的,例如 Document(根节点)、Element(元素)、Attr(属性)、Comment(注释)、Text(元素或属性的文本内容)等接口都是从 Node 继承过来的。在 DOM 树中,Node 接口代表了树中的一个节点。Node 接口的常用方法如下。
NodeList getChildNodes():返回此节点的所有子节点的NodeList。Node getFirstChild():返回此节点的第一个子节点。Node getLastChild():返回此节点的最后一个子节点。Node getNextSibling():返回此节点之后的节点。Node getPreviousSibling():返回此节点之前的节点。Document getOwnerDocument():返回与此节点相关的Document对象。Node getParentNode():返回此节点的父节点。short getNodeType():返回此节点的类型。String getNodeName():根据此节点类型,返回节点名称。String getNodeValue():根据此节点类型,返回节点值。
前面已经提到,DOM 中很多接口都是从 Node 接口继承的,所以 Node 接口拥有的方法这些接口都可以使用。但是这些从 Node 接口继承下来的接口又都各有特性,所以 Node 接口拥有的方法在各个子接口上的返回值含义不尽相同。例如,Element(元素接口)的 getNodeType() 的返回值为 Node.ELEMENT_NODE 常量,getNodeName() 的返回值为标签名称,getNodeValue() 的返回值为 null。下表列出了 nodeName、 nodeValue 和 attributes 的值将根据接口类型的不同而不同,这对于 XML 解析的初学者而言是个难点,请大家务必结合后面的例子,深刻理解。
| Interface | nodeName | nodeValue | attributes |
|---|---|---|---|
| Attr | 与 Attr.name 相同 | 与 Attr.value 相同 | null |
| CDATASection | "#cdata-section" | 与 CharacterData.data 相同:CDATA 节的内容 | null |
| Comment | "#comment" | 与 CharacterData.data 相同: 该注释的内容 | null |
| Document | "#document" | null | null |
| DocumentFragment | "#document-fragment" | null | null |
| DocumentType | 与 DocumentType.name 相同 | null | null |
| Element | 与 Element.tagName 相同 | null | NamedNodeMap |
| Entity | entity name | null | null |
| EntityReference | 引用的实体名称 | null | null |
| Notation | notation name | null | null |
| ProcessingInstruction | 与 ProcessingInstruction.target 相同 | 与 ProcessingInstruction.data 相同 | null |
| Text | "#text" | 与 CharacterData.data 相同: 该文本节点的内容 | null |
String getTextContent():返回此节点的文本内容。void setNodeValue(String nodeValue):根据此节点类型,设置节点值。void setTextContent(String textContent):设置此节点的文本内容。Node appendChild(Node newChild):将节点 newChild 添加到此节点的子节点列表的末尾。Node insertBefore(Node newChild,Node refChild):在现有子节点 refChild 之前插入节点 newChild。Node removeChild(Node oldChild):从子节点列表中移除 oldChild 所指示的子节点,并将其返回。Node replaceChild(Node newChild, oldChild):将子节点列表中的子节点 oldChild 替换为 newChild,并返回 oldChild 节点。
Document 接口
Document 接口表示 DOM 树中的根节点,即对 XML 文档进行操作的入口节点。通过 Document 节点,可以访问到文档中的其他节点。Document 接口的常用方法如下:
Element getDocumentElement():返回代表这个 DOM 树根节点的Element对象。NodeList getElementsByTagName(String tagname):按文档顺序返回包含在文档中且具有给定标记名称的所有Element的NodeList。
NodeList 接口
NodeList 接口提供了对节点集合的抽象定义,包含了一个或多个节点(Node)的有序集合。NodeList 接口的常用方法如下。
int getLength():返回有序集合中的节点数。Node item(int index):返回有序集合中的第 index 个项。
使用 DOM 解析 XML,需要经过以下几个步骤。
- 创建解析器工厂,即
DocumentBuilderFactory对象。 - 通过解析器工厂获得 DOM 解析器,即
DocumentBuilder对象。 - 解析指定 XML 文档,得到 DOM 节点树。
- 对 DOM 节点树进行操作,完成对 XML 文档的增、删、改、查。
下面使用 DOM 对之前编写的用于存放租车系统车辆信息的 vehicles.xml 文档进行解析,并输出 租车系统中有几种类型的车, 租车系统中有几辆卡车,并详细输出每辆卡车的 id 属性及详细信息,程序运行结果如下所示:
具体代码如程序清单 TestDOM.java(需要大家认真阅读代码中的注释,理解含义):
import java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
public class TestDOM {
public static void main(String[] args) {
try {
//创建解析器工厂
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
//通过解析器工厂获得DOM解析器
DocumentBuilder db = dbf.newDocumentBuilder();
//解析指定XML文档,得到DOM节点树
//本例中的vehicles.xml文件与src目录处于同一级目录中
Document doc = db.parse("vehicles.xml");
//得到根节点
NodeList vehicles = doc.getChildNodes();
System.out.println(" `租车系统` 中共有" + vehicles.getLength() + "种类型的车!");
//得到所有<truck>节点列表信息
NodeList truckList = doc.getElementsByTagName("truck");
System.out.println(" `租车系统` 中共有" + truckList.getLength() + "辆卡车!");
//遍历所有卡车
for (int i = 0; i < truckList.getLength(); i++) {
//获取索引为i的卡车
Node truck = truckList.item(i);
//获取卡车属性值并显示
Element element = (Element) truck;
String idValue = element.getAttribute("id");
//以下通过属性名获得属性节点,再通过getNodeValue()获得属性值
//Node attr = element.getAttributeNode("id");
//String idValue = attr.getNodeValue();
System.out.println("id为" + idValue + "的卡车信息为:");
//获取索引为i的卡车详细信息并输出
for (Node node = truck.getFirstChild(); node != null; node = node.getNextSibling()) {
//根据节点类型进行判断,显示元素节点信息,如 <oil>20</oil>
if (node.getNodeType() == Node.ELEMENT_NODE){
//元素节点的节点名为标签名,如oil
String name = node.getNodeName();
//元素节点<oil>20</oil>下第一个子节点为文本节点20,得到节点值20
String value = node.getFirstChild().getNodeValue();
System.out.println(" " + name + ":" + value + ";");
}
}
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
编译运行:
javac TestDOM.java
java TestDOM
输出结果:
在上面的代码中,用到了根节点、属性节点、元素节点和文本节点,它们的 nodeName 、nodeValue 和 attributes 的值含义各不相同,需要注意。
SAX 解析 XML
相比于 DOM,SAX(Simple API for XML)是一种速度更快、更有效的解析 XML 文档的方法。它不需要一次性建立一个完整的 DOM 树,而是读取文档时激活事件进行处理。
DOM 是 W3C 标准,提供的是标准的解析方式,但其解析效率一直不尽如人意。这是因为 DOM 解析 XML 文档时,把所有内容一次性装载入内存,并构建一个驻留在内存中的节点树。如果需要解析的 XML 文档过大,或者只对该文档中的一部分内容感兴趣,这种做法就会引起性能问题。
SAX 既是一个接口,也是一个软件包。SAX 在解析 XML 时是事件驱动型的,它的工作原理简单地说就是对文档进行顺序扫描,当扫描到文档开始与结束、元素开始与结束等地方时通知事件处理程序,由事件处理程序做相应动作,然后继续同样的扫描,直至文档结束。SAX 的缺点也很明显,要用 SAX 对 XML 文档进行解析时,就要实现多个事件处理程序,用来处理可能触发的事件,对程序员而言操作起来相对复杂。
为了快速的掌握 SAX 解析方式,现删除 vehicles.xml 中的 trucks 节点,即只保留 cars 节点,然后通过以下代码解析每一个 car 子节点,详见程序清单 SAXParseXML.java :
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.*;
import java.io.*;
public class SAXParseXML extends DefaultHandler {
private String tagName;
//开始解析xml文件 (只执行一次)
@Override
public void startDocument() throws SAXException {
System.out.println("SAX解析开始...");
}
//解析xml文件 结束(只执行一次)
@Override
public void endDocument() throws SAXException {
System.out.println("SAX解析结束...");
}
//开始解析xml元素(执行多次)
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if (qName.equals("car")) {
int id = Integer.parseInt(attributes.getValue(0));
System.out.println(id);
}
this.tagName = qName;
}
//结束 解析xml元素(执行多次)
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (qName.equals("car")) {
System.out.println("一个car标签解析完毕");
}
this.tagName = null;
}
//在startElement、endElement 之间调用多次
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
if (this.tagName != null) {
String data = new String(ch, start, length);//ch[] -> String
if (this.tagName.equals("name")) {
System.out.print(data+"\t");
}
if (this.tagName.equals("oil")) {
System.out.print(Integer.parseInt(data)+"\t");
}
if (this.tagName.equals("loss")) {
System.out.print(Integer.parseInt(data)+"\t");
}
if (this.tagName.equals("brand")) {
System.out.println(data);
}
}
}
public static void main(String[] args) throws Exception {
SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
File inputFile = new File("/home/project/vehicles.xml") ;
SAXParseXML saxParseXML = new SAXParseXML();
parser.parse(inputFile, saxParseXML);
}
}
编译运行:
javac SAXParseXML.java
java SAXParseXML
输出结果:
可见,在编码时,SAX 解析方式需要继承 DefaultHandler 类,然后重写该类的方法。