XML解析
如果不同厂商开发的XML解析器提供的API都依赖于自向的产品,那么应用程序要使用解析器,就只能使用特定于厂商的API。假如以后应用程序需要更换解析器,那么就只能重新编写代码了。庆幸的是,目前几乎所有的解析器都对两套标准的API提供了支持,这两套API就是DOM和SAX。
DOM(Document Object Model)的缩写,即文档对象模型,是W3C组织推荐的处理XML的标准接口。SAX是Simple API for XML的缩写,它不是某个官方机构的标准,但它是XML社区事实上的标准。虽然只是“民间”的标准,但是它在XML中的应用不比DOM少,几乎所有XML解析器都支持它。
DOM和SAX只是定义了一些接口,以及某些接口的默认实现(什么事情也不做),一个应用程序要想利用DOM或SAX访问XML文档,还需要一个实现了DOM或SAX的解析器,即实现DOM和SAX中定义的接口,提供DOM和SAX定义的功能。
Apache的Xerces是一个使用非常广泛的解析器,它提供了DOM和SAX的调用接口。SAX的标准解析接口为org.xml.sax.XMLReader,而Xerces中提供的解析接口的实现类为org.apache.xerces.parsers.SAXParser,在应用程序中可以采用如下方式访问XML文档:
org.xml.sax.XMLReader sp = new org.apache.xerces.parsers.SAXParser();
现有一个问题,虽然我们使用的是标准的DOM和SAX接口,但不同解析器的实现类是不同的,如果要使用另外一种解析器,仍然需要修改应用程序,只不过修改的代码量较少。有没有办法让我们在更换解析器时,不用对已发布程序做任何改变呢?JAXP API可以帮我们实现这一点。为了屏蔽具体厂商的实现,让开发人员以一种标准的方式对XML进行编程,SUN制定了JAXP(Java API for XML Processing)规范。JAXP没有提供解析XML的新方法,也没有为XML的处理提供新的功能,它只是在解析器之上封闭了一个抽象层,允许开发人员以独立于厂商的API调用访问XML数据。
JAXP开发包由javax.xml包及其子包、org.w3c.dom包及其子包、org.xml.sax包及其子包组成。在javax.xml.parsers包中,定义了几个工厂类,用于加载DOM和SAX的实现类,JAXP由接口、抽象类和一些辅助类组成,符合JAXP规范的解析器实现其中的接口和抽象类,开发人员只要使用JAXP的API编程,底层的解析器就可以任意更换。
应用程序àJAXP的接口与抽象类àXerces的JAXP实现àXerces的DOM或SAX解析器
有了JAXP,开发人员就可以随意更换解析器的实现,而不需要修改应用程序。
使用DOM解析XML
DOM是独立于程序语言的,W3C组织以IDL(接口中定义语言)的形式定义了DOM中接口。某种语言要实现DOM,需要将DOM接口转换为本语言中的对应结构。W3C提借了Java和ECMAScript这两种语言的实现。
Node对象是DOM结构中最基本的对象,代表了文档树中的一个节点,通常直接使用Node很少,一般使用Document、Element、Attr、Text等Node对象的子对象来操作文档。虽然在Node接口中定义了对节点进行操作的通用方法,但是有一些Node对象的子对象,如Text对象并不存在子节点。
1、 Node接口主要是对它的子节点进行增、删、获取。
2、 Node接口中定义了各种节点的类型常量,可以用它们来判断是哪种节点。
3、 void normalize():将该节点所有的后代文本节点,包括属性节点,调整为规范化的形式,这仅是以结构(如:元素、注释、处理指令、CDATA段、实体引用)来分隔文本节点,也就是说,在节点所在的这棵树下,既不存在相邻的文本节点,也不存在空的文本节点。
Node节点的getNodeName、getNodeValue 和 getAttributes 的值将根据以下节点类型的不同而不同。
Interface(节点类型) |
getNodeName(节点名字) |
getNodeValue(节点值) |
getAttributes(节点属性) |
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 |
DOM树中的节点类型
XML中最常见的节点类型是:文档、元素、文本和属性节点,在DOM API中对应的接口是Document、Element、Text和Attr。
文档节点Document
文档节点是文档树的根节点,也是文档中其他所有节点的父节点。要注意的是,文档节点并不是XML文档的根元素,因为在XML文档中,处理指令、注释等内容可以出现在根元素以外,所以在构造DOM树时,根元素并不适合作为根节点,于是就有了文档节点,而根元素则作为文档节点的子节点。
4、 通过 Element getDocumentElement()方法得到XML文档的根元素。
5、 通过createXX相应的方法创建不同类型的节点。
6、 Element getElementById(String elementId):通过给出的ID类型的属性值elementId来查找对应用的元素。一个ID类型的属性值唯一标识了XML文档中的一个元素。除非特别定义,名字为“ID”或者“id”的属性,其类型并不是ID类型。
7、 NodeList getElementsByTagName(String tagname):以文档顺序返回标签名字为tagname的所有元素。如果参数为“*”,则返回所有的元素。
8、 NodeList getElementsByTagNameNS(String namespaceURI,String localName):按照指定的名称空间URI和元素的本地名返回所有匹配的元素。如果参数namespaceURI为“*”,则表示所有的名称空间,同样,如果localName为“*”,则匹配所有元素。
元素节点Element
文本节点Text
属性节点Attr
属性实际上是属于某个元素的,所以属性节点不是元素的子节点。因而在DOM中,属性节点没有被作为文档树的一部分,所以在属性节点上调用getParentNode、getPreviousSibling、getNextSibling时返回null。
NodeList接口
Node:
NodeList getChildNodes();
Document、Element:
NodeList getElementsByTagName(String tagname);
NodeList getElementsByTagNameNS(String namespaceURI, String localName);
NamedNodeMap接口
Node:
NamedNodeMap getAttributes();
DocumentType:
NamedNodeMap getEntities();
NamedNodeMap getNotations();
DOM解析器工厂和DOM解析器
在javax.xml.parsers包中,定义了DOM解析器工厂类DocumentBuilderFactory,用来产生DOM解析器。DocumentBuilderFactory工厂类是一个抽象类,在这个类中提供了一个静态方法newInstance()方法,用来创建一个工厂类的实例。
DocumentBuilderFactory是个抽象类,那它的newInstance()产生的是哪个实现呢?采用JAXP编程可以任意更换解析器的关键就在于这个工厂类。DocumentBuilderFactory抽象类的实现由遵照JAXP规范的解析器提供商来给出的。解析器提供商为自身的解析器编写一个从DocumentBuilderFactory类继承的工厂类,然后由这个工厂类实例负责产生解析器对象。
那么DocumentBuilderFactory的newInstance()方法是如何找到解析器提供商给出的工厂类呢?可通过下面3种途径依次查找解析器工厂类:
1、 首先查看是否设置了javax.xml.parsers.DocumentBuilderFactory系统属性。这又可通过两种方式来设置这个系统属性:
System.setProperty("javax.xml.parsers.DocumentBuilderFactory",
"org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
建议不要使用此种方式,因为如果更换解析器,程序就要修改。另一种是在用java.exe执行程序时,通过-D选项来设置系统属性:
java -Djavax.xml.parsers.DocumentBuilderFactory=oracle.xml.jaxp.JXDocument DOMTest
2、 查找JRE目录中lib子目录下的jaxp.properties文件,如果存在,则读取该配置文件:
javax.xml.parsers.DocumentBuilderFactory=org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
3、 在classpath环境变量所指定JAR文件中,查找META-INF/services目录下的javax.xml.parsers.DocumentBuilderFactory文件,使用这个文件中所指定的工厂类名来构造工厂的实例,这种方式被多数解析器提供商所采用,在他们的发布的包包含解析器的JAR包中,往往会提供这个的文件,我们来看看Apache提供的解析器实现包xercesImpl.jar:
其中javax.xml.parsers.DocumentBuilderFactory文件的内容如下:
org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
如果上面3种途径都没有找到解析器工厂类,就使用平台默认的解析器工厂。从JAXP1.2开始,SUN公司对Apache的Xerces解析器重新包装了一下,并将org.apache.xerces包名改为了com.sun.org.apache.xerces.internal,然后在JAXP的开发包中一起提供,作为默认的解析器。
在得到工厂实例后,就可以通过DocumentBuilderFactory的newDocumentBuilder()方法来创建DOM解析器实例了:
DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();//得到具体厂商的XML解析器
Document d = db.parse("text.xml");//通过解析器解析得到文档对象
DocumentBuilderFactory另外几个重要的方法:
l public void setValidating(boolean validating):指定解析器是否难被解析的文档,默认为false。注,该方法只能DTD验证有效。如果使用Schema验证,则可设为false后,使用setSchema(Schema)方法将一个模式与解析关联。
l public void setSchema(Schema schema):如果要使用模式对XML文档进行验证,需要使用该方法。Schema对象由javax.xml.validation.SchemaFactory工厂类创建。
l public void setIgnoringElementContentWhitespace(boolean whitespace):是否要忽略元素内容中的空白。默认是false。按照XML1.0的推荐标准,元素内容中的空白必须由解析器保留,但当根据DTD进行验证时,解析器可以知道文档的特定部分不会支持空格(如具有元素型内容的元素),所以这一区域的任何空格都“可忽略的”。注,这一标志只有通过setValidating打开验证功能后才有效。
使用DOM解析XML文档的实例
--students.xml
<?xml version="1.0" encoding="GB2312"?>
<?xml-stylesheet type="text/xsl" href="students.xsl"?>
<students>
<student sn="01">
<name>张三</name>
</age>
</student>
<student sn="02">
<name>李四</name>
</age>
</student>
</students>
--end
遍历文档
publicclass DOMPrinter {
/**
* 输出节点的类型、名字和值。
*/
publicstaticvoid printNodeInfo(String nodeType, Node node) {
System.out.println(nodeType + "\t" + node.getNodeName() + " : "
+ node.getNodeValue());
}
/**
* 采用递归调用,输出给定节点下的所有后代节点。
* 注:为了简单起见,只对处理指令节点、元素节点、
* 属性节点和文本节点进行了处理。
*/
publicstaticvoid traverseNode(Node node) {
short nodeType = node.getNodeType();
switch (nodeType) {
case Node.PROCESSING_INSTRUCTION_NODE:
printNodeInfo("处理指令", node);
break;
case Node.ELEMENT_NODE:
printNodeInfo("元素", node);
NamedNodeMap attrs = node.getAttributes();
int attrNum = attrs.getLength();
for (int i = 0; i < attrNum; i++) {
Node attr = attrs.item(i);
printNodeInfo("属性", attr);
}
break;
case Node.TEXT_NODE:
printNodeInfo("文本", node);
break;
default:
break;
}
Node child = node.getFirstChild();
while (child != null) {
// 递归调用
traverseNode(child);
child = child.getNextSibling();
}
}
publicstaticvoid main(String[] args) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new File("students.xml"));
// Document接口提供了三个方法,分别用于获取XML声明的三个部分:
// 版本、文档编码、独立文档。
// 调用这三个方法需要的JDK版本最小是1.5
System.out.println("<?xml='" + doc.getXmlVersion() + "' encoding='"
+ doc.getXmlEncoding() + "' standalone='"
+ doc.getXmlStandalone() + "'?>");
traverseNode(doc);
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
添加、删除、修改和保存
publicclass DOMConvert {
publicstaticvoid main(String[] args) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse("students.xml");
// ------------------添加节点------------------
// 创建表示一个学生信息的各元素节点。
Element eltStu = doc.createElement("student");
Element eltName = doc.createElement("name");
Element eltAge = doc.createElement("age");
// 创建<student>元素的sn属性节点。
Attr attr = doc.createAttribute("sn");
attr.setValue("03");
// 创建代表学生信息的文本节点。
Text txtName = doc.createTextNode("王五");
Text txtAge = doc.createTextNode("19");
// 将文本节点添加为对应的元素节点的子节点。
eltName.appendChild(txtName);
eltAge.appendChild(txtAge);
// 将name和age节点添加为student节点的子节点。
eltStu.appendChild(eltName);
eltStu.appendChild(eltAge);
// 为<student>元素添加sn属性节点。
eltStu.setAttributeNode(attr);
// 得到XML文档的根元素。
Element eltRoot = doc.getDocumentElement();
// 将student节点添加为根元素的子节点。
eltRoot.appendChild(eltStu);
NodeList nl = doc.getElementsByTagName("student");
// ------------------删除节点------------------
Node nodeDel = nl.item(0);
nodeDel.getParentNode().removeChild(nodeDel);
// ------------------修改节点------------------
// 注意:NodeList对象是活动的,所以前面删除节点的操作会影响到NodeList对象,
// NodeList的节点是
// 的节点。
Element eltChg = (Element) nl.item(0);
Node nodeAgeChg = eltChg.getElementsByTagName("age").item(0);
nodeAgeChg.getFirstChild().setNodeValue("22");
// 输出修改后的学生信息。
for (int i = 0; i < nl.getLength(); i++) {
Element elt = (Element) nl.item(i);
Node nodeName = elt.getElementsByTagName("name").item(0);
Node nodeAge = elt.getElementsByTagName("age").item(0);
String name = nodeName.getFirstChild().getNodeValue();
String age = nodeAge.getFirstChild().getNodeValue();
System.out.println("-----------------学生信息-----------------");
System.out.println("编号:" + elt.getAttribute("sn"));
System.out.print("姓名:");
System.out.println(name);
System.out.print("年龄:");
System.out.println(age);
System.out.println();
}
// ------------------保存修改结果------------------
// 利用文档节点创建一个DOM输入源。
DOMSource source = new DOMSource(doc);
// 以converted.xml文件构造一个StreamResult对象,用于保存转换后结果。
StreamResult result = new StreamResult(new File("converted.xml"));
// 得到转换器工厂类的实例。
TransformerFactory tff = TransformerFactory.newInstance();
// 创建一个新的转换器,用于执行恒等转换,
// 即直接将DOM输入源的内容复制到结果文档中。
Transformer tf = tff.newTransformer();
tf.setOutputProperty(OutputKeys.INDENT, "yes");//缩进
tf.setOutputProperty(OutputKeys.ENCODING, "gb2312");//编码
// 执行转换。
tf.transform(source, result);
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (TransformerConfigurationException e) {
e.printStackTrace();
} catch (TransformerException e) {
e.printStackTrace();
}
}
}
使用SAX解析XML
XMLReader(解析器接口)
SAX解析器接口和事件处理器接口在org.xml.sax包中定义,XMLReader是SAX2.0解析器必须实现的接口(SAX1.0解析器实现Parser接口,该接口已不再使用)。
l setEntityResolver(EntityResolver resolver):注册一个实体解析器。
l setDTDHandler(DTDHandler handler):注册一个DTD事件处理器。
l setContentHandler(ContentHandler handler):注册一个内容事件处理器。
l parse(InputSource input):解析XML文档。
ContentHandler(内容事件处理器接口)
SAXAPI定义了许多事件,这些事件分别由事件处理器中相应的方法去响应。在SAX1.0前使用的是DocumentHandler接口,SAX2.0中已被ContentHandler取代。
startPrefixMapping(String prefix, String uri):在一个前缀URI名称空间映射范围的开始时被调用。
<students xmlns:stu="http://www.sunin.org/students">
…
</stuents>
SAX解析器解析到<stuedents>元素时,就会调用startPrefixMapping方法,将stu传递给prefiex参数,将http://www.sunin.org/students传递给uri参数,然后产生<students>元素的startElement事件。在产生<students>元素的endElement事件后,解析器将调用endPrefixMapping方法。
SAX解析器工厂
与Dom类似,JAXP也为SAX解析器提供了工厂类——SAXParserFactory类。与DOM解析器工厂类不同的是,SAXParserFactory类的newInstance()方法查找的工厂类属性为:java.xml.parsers.SAXParserFactory,如果我们使用Apache的Xerces解析器,可以配置如下:
java.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp.SAXParserFactoryImpl
JAXP中定义的SAX解析器类是SAXParser,获取SAXParser类的实例与获取DocumentBuilder类的实例相似,在得到工厂类的实例后,通过SAXParserFactory 实现类实例的newSAXParser()方法得到 SAX解析器实例:public abstract SAXParser newSAXParser()
你可以调用SAXParser或者XMLReader中的parser()方法业解析文档,效果是完全一样的。不过在SAXParser中的parse()方法能接受更多的参数。可以对不同的XML文档数据源进行解析,因此使用起来比XMLReader要方便一些。
另外,与DOM不同的是,SAX本身也提供了创建XMLReader对象的工厂类,在org.xml.sax.helpers包中提供了XMLReaderFactory类,该类createXMLReader用于创建XMLReader对象。
实际上,SAXParser是JAXP对XMLReader实现类的一个包装类,在SAXPars中定义了getXMLReader()方法,用于返回它内部的XMLReader实例:abstract XMLReader getXMLReader()
使用SAX解析XML
解析并完整输出XML文档
/**
* 使用SAX解析XML文档,实际上就是编写事件处理器。
* 为了简化事件处理器接口的实现,我们让SAXPrinter类继承DefaultHandler帮助类
*/
publicclass SAXPrinter extends DefaultHandler {
// SAX解析器开始解析文档时,将会调用这个方法
publicvoid startDocument() throws SAXException {
// 输出XML声明。
System.out.println("<?xml version='1.0' encoding='GB2312'?>");
}
// SAX解析器读取了处理指令,将会调用这个方法
publicvoid processingInstruction(String target, String data)
throws SAXException {
// 输出文档中的处理指令。
System.out.println("<?" + target + " " + data + "?>");
}
// SAX解析器读取了元素的开始标签后,将会调用这个方法
publicvoid startElement(String uri, String localName, String qName,
Attributes attrs) throws SAXException {
// 输出元素的开始标签及其属性。
System.out.print("<" + qName);
int len = attrs.getLength();
for (int i = 0; i < len; i++) {
System.out.print(" ");
System.out.print(attrs.getQName(i));
System.out.print("=\"");
System.out.print(attrs.getValue(i));
System.out.print("\"");
}
System.out.print(">");
}
// SAX解析器读取了字符数据后,将会调用这个方法
publicvoid characters(char[] ch, int start, int length)
throws SAXException {
// 输出元素的字符数据内容。
System.out.print(new String(ch, start, length));
}
// SAX解析器读取了元素的结束标签后,将会调用这个方法
publicvoid endElement(String uri, String localName, String qName)
throws SAXException {
// 输出元素的结束标签。
System.out.print("</" + qName + ">");
}
publicstaticvoid main(String[] args) {
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser sp = null;
try {
sp = spf.newSAXParser();
File file = new File("students.xml");
sp.parse(file, new SAXPrinter());
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用ErrorHandler处理解析中的错误
DefaultHandler既现实了ContentHandler接口,又实现了ErrorHandler接口。
publicclass ErrorProcessor extends DefaultHandler {
publicvoid warning(SAXParseException ex) throws SAXException {
System.err.println("[Warning] " + getLocationString(ex) + ": "
+ ex.getMessage());
}
publicvoid error(SAXParseException ex) throws SAXException {
System.err.println("[Error] " + getLocationString(ex) + ": "
+ ex.getMessage());
}
publicvoid fatalError(SAXParseException ex) throws SAXException {
System.err.println("[Fatal Error] " + getLocationString(ex) + ": "
+ ex.getMessage());
}
/**
* 获取导致错误或者警告的文本结束位置的行号和列号。
* 如果是实体引发错误,还获取它的公共标识符和系统标识符。
*/
private String getLocationString(SAXParseException ex) {
StringBuffer str = new StringBuffer();
String publicId = ex.getPublicId();
if (publicId != null) {
str.append(publicId);
str.append(" ");
}
String systemId = ex.getSystemId();
if (systemId != null) {
str.append(systemId);
str.append(':');
}
str.append(ex.getLineNumber());
str.append(':');
str.append(ex.getColumnNumber());
return str.toString();
}
/**
* 输出元素的结束标签,以便于查看不同类型的错误对文档解析的影响。
*/
publicvoid endElement(String uri, String localName, String qName)
throws SAXException {
System.out.println("</" + qName + ">");
}
publicstaticvoid main(String[] args) {
try {
/*
* 利用XMLReaderFactory工厂类,创建XMLReader对象。
* 注,这里没有使用JAXP中定义的SAX解析器工厂类和解析器类,
* 而是使用了SAX本身定义的XMLReaderFactory工厂类与XMLRader
* 解析器。当然最好是使用JAXP中的工厂类。
*/
XMLReader xmlReader = XMLReaderFactory.createXMLReader();
// 打开解析器的验证功能。
xmlReader
.setFeature("http://xml.org/sax/features/validation", true);
ErrorProcessor ep = new ErrorProcessor();
xmlReader.setErrorHandler(ep);
xmlReader.setContentHandler(ep);
// InputSource类位于org.xml.sax包中,表示XML实体的输入源。
// 它可以用java.io.InputStream对象来构造,更多信息请参看JDK的API文档
InputSource is = new InputSource(
new FileInputStream("students.xml"));
xmlReader.parse(is);
} catch (SAXException e) {
System.out.println(e.toString());
} catch (IOException e) {
System.out.println(e.toString());
}
}
}
如果被解析的XML不符合对应的模式文档(DTD或Schema)或不存在模式文档时,都会报错