XML文档的结构
XML文档应当以一个文档头开始,如:
<?xml version="1.0"?>或
<?xml version="1.0" encoding="UTF-8"?>
严格说来,文档头是可有可无的,但是强烈推荐使用文档头。
文档头之后通常是文档类型定义(Document Type Definition,DTD),如:
<!DOCTYPE web-app PUBLIC"-//Sun Microsystems,Inc.//DTD Web Application 2.2//EN""http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
文档类型定义是确保文档正确的一个重要机制,但是这不是必需的。
最后,XML文档的正文包含根元素根元素包含其他一些元素。如:
<?xml version="1.0"?>
<!DOCTYPE configuration ...>
<configuration>
<title>
<font>
<name>Helvetica</name>
<size>36</size>
</font>
</title>
...
</configuration>
注意:在设计XML文档结构时,最好使元素只包含子元素或只包含文本,也就是应该避免以下情况 :
<font>
Helvetica
<size>36</size>
</font>
在XML规范中,这叫混合式内容。如果避免了混合内容,可以简化解析过程。
XML元素可以包含属性,如<size unit="pt">36</size>
属性的灵活性比元素差,关于使用元素或属性的一个通常经验法则是,属性只应该在修改值的解释时使用,而不是在指定值时使用。
注意:在HTML中属性的使用规则很简:凡是不显示在网页上的都是属性,如<a href="http://java.sum.com">Java Technology</a>;然而,这个规则对于大多数XML并不那么管用。因为XML文件的数据并非像通常意义那样是让人浏览的。元素和文本是XML文档的主要要素,以下是你会遇到的其他一些标记的说明:
- 字符引用的形式是&#十进制值或&#x十六进制值。
- 实例引用的形式是&name。以下实例引用
<
>
&
"
'
它们表示:小于,大于 ,&,引号,省略号等字符。可以在DTD中定义其他的实体引用。
- CDATA部分用<![ 和 ]]>来限定界限。它们是字符数据的一种特殊形式。你可以使用它们来包含那些含有<,>,&之类字符的字符串,而不必将它们解释为标记,如:<![ CDATA[ <&> are my favorite delimiters ] ]>,CDATA部分不能包含字符吕]]>。它常被用做将传统数据纳入XML文档的一种特殊方法。
- 处理指令是指那些专门在处理XML文档的应用程序中使用的指令,它们将用<? 和 ?>来限定其界限,例如:<?xml-stylesheet href="mystyle.css" type="text/css"?>
- 注释用 <!- 和 -->限定其界限,例如:<!-- This is a comment. -->注释不能含有字符串--。注释只是为了给文档的读者提供信息,其中绝不含有隐藏的命令,命令是由处理指令来实现。
解析XML文档
Java库提供了两个XML解析器:
像文档对象模型(Document Object Model,DOM)解析器这样的树型解析器,它们将读入的XML文档转换成树结构。
像用于XML的简单API(Simple API for XML,SAX)解析器这样的流机制解析器,它们在读入XML文档时生成相应的事件。
DOM解析器对于实现我们的大多数目的都很容易。但如果处理很长的文档,使用它生成树结构将会消耗大量内存,或者如果你只是对于某些元素感兴趣,而不关心它们的上下文,那么你应该考虑使用流机制解析器。
DOM解析器的接口已经被W3C标准化了。Java中org.w3c.dom包中包含了接口类型的定义,如Document和Element等。可通过以下代码来获得Document:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder buider = factory.newDocumentBuilder();
File f = new File("...xml");
Document doc = buider.parse(f);
通过GetDocumentElement方法将返回文档根元素:
Element root = doc.getDocumentElement();
getChildNodes方法将返回一个类型为NodeList的集合,包含了所有的子元素。其中item方法将得到指定索引项,getLength方法则提供项的总数,以下代码枚举所有子元素:
NodeList children = root.getChildNodes();
for(int i = 0; i < children.getLength(); i++){
Node child = children.item(i);
...
}
注意:分析子元素要很仔细。如下面文档 :
<font>
<name>Helvetica</name>
<size>36</size>
</font>
你期望font有两个子元素,但解析器却报告有5个:
<font>和<name>之间的空白字符
name元素
</name>和<size>之间的空白字符
size元素
</size>和</font>之间的空白字符
如果只希望得到子元素,可以通过以下代码忽略空白字符:
NodeList children = root.getChildNodes();
for(int i = 0; i < children.getLength(); i++){
Node child = children.item(i);
if(child instanceof Element){
Element childElement = (Element)child;
...
}
}
如果你的文档在有DTD(下面会讲到),那么解析器会知道哪些元素没有文本节点子元素,而且它会帮你禁止空白字符。
也可以通过getFirstChild得到第一个子元素,用getNextSiblingt得到下一个兄弟节点,可用以下代码遍历子节点:
for(Node chiNode = root.getFirstChild(); chiNode != null; chiNode = chiNode.getNextSibling()){
...
}
当你分析name和size元素时,想检索到它们包含的文本字符串,而这些文本字符串本身包含在Text类型的子节点中。既然知道这些Text节点是唯一子元素,可以用getFirstChild方法而不用再遍历一个NodeList,然后可以用getData方法检索存储在Text节点中的字符串。
NodeList children = root.getChildNodes();
for(int i = 0; i < children.getLength(); i++){
Node child = children.item(i);
if(child instanceof Element){
Element childElement = (Element)child;
Text textNode = (Text)childElement.getFirstChild();
String text = textNode.getData().trim();
...
}
}
注意:getData的返回值调用trim方法是个好主意,如下XML:
<size>
36
</size>
那么,解析器将会把所有的换行符和空格都包含到文本节点中去。调用trim方法可以把实际数据前后的空白字符删掉。如果要枚举节点属性,可调用getAttributes方法,返回一个NamedNodeMap对象,其中包含描述属性的节点对象:
NamedNodeMap attributes = element.getAttributes();
for(int i = 0; i < attributes.getLength(); i++){
Node attribute = attributes.item(i);
String name = attribute.getNodeName();
String value = attribute.getNodeValue();
...
}
或者,如果知道属性名,则可以直接得到相应属性值:
String unit = element.getAttribute("unit");
验证XML文档
如果要规范文档结构,可以提供一个文档类型定义(DTD),DTD包含了用于解释文档是如何构成的规则 ,这些规则规范了每个元素的合法子元素和属性。如:
<!ELEMENT font(name,size)>
这个规则表明,一个font元素总是有两个子元素,分别是name和size。
具体DTD语法请参看Java核心技术或W3C。
对下一个XML文档 :
<?xml version="1.0"?>
<!DOCTYPE staff[
<!ELEMENT staff (employee)*>
<!ELEMENT employee (name,salary)>
<!ATTLIST employee nationality CDATA "china">
<!ELEMENT name (#PCDATA)>
<!ELEMENT salary (#PCDATA)>
]>
<staff>
<employee nationality="china">
<name>bin</name>
<salary>500</salary>
</employee>
<manager nationality="china">
<name>bin</name>
<salary>500</salary>
</manager>
</staff>
使用以下代码进行解析:
import java.io.*;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
public class XMLDTDStudy {
public static void main(String[] args) {
try{
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(true); //打开验证特性
factory.setIgnoringElementContentWhitespace(true); //设置为匆略文本节点的空白字符
DocumentBuilder buider = factory.newDocumentBuilder();
File f = new File("staffWithDTD.xml");
Document doc = buider.parse(f);
Element root = doc.getDocumentElement();
NodeList children = root.getChildNodes();
for(int i = 0; i < children.getLength(); i++){
Node child = children.item(i);
System.out.println(i + ": " + child.getNodeName());
}
}catch(ParserConfigurationException e){
e.printStackTrace();
}catch(IOException e){
e.printStackTrace();
}catch(SAXException e){
e.printStackTrace();
}
}
}
得到结果如下:
程序报告了manager元素并没有在DTD中声明并忽略空白字符。
使用XPath定位信息
<?xml version="1.0"?>
<staff>
<employee nationality="china">
<name>bin</name>
<salary>500</salary>
</employee>
<employee>
<name>zhou</name>
<salary>800</salary>
</employee>
</staff>
XPath可以描述XML文档中的一组节点,如“/staff/employee”则描述了根元素staff的子元素中所有的employee元素。可以用[]操作符选择特定元素:“/staff/employee[1]”表示选择第一行(索引号从1开始)
使用@操作可以得到属性值,如:"/staff/employee[1]/@nationality"得到第一个员工的国籍china
XPath有很多有用的函数,如:"count(/staff/employee)"返回根元素staff的子元素中employee元素的数量。
Java SE5.0增加了一个API计算XPath表达式,先从XPathFactory对象创建一个XPath对象。
XPathFactory xpfactory = XPathFactory.newInstance();
XPath path = xpfactory.newXPath();
然后,调用evaluate方法计算XPath对象表达 :
String name = path.evaluate("/staff/employee[1]/salary", doc);
如果XPath表达式产生一组节点,则如下调用:
NodeList nodes = (NodeList)path.evaluate("/staff/employee", doc,XPathConstants.NODESET);
如果结果只有一个节点,则如下调用:
Node node = (Node) path.evaluate("/staff/employee[1]", doc,XPathConstants.NODE);
如果结果是一个数字,则使用
int salary = ((Number) path.evaluate("/staff/employee[1]/salary", doc,XPathConstants.NUMBER)).intValue();
不必从文档的根节点开始搜索,可以从任意一个节点或节点列表开始,如果你有前一个计算得到的一个节点node,就可以调用:
result = path.evaluate(expression,node);
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
try {
DocumentBuilder buider = factory.newDocumentBuilder();
File f = new File("staff.xml");
Document doc = buider.parse(f);
XPathFactory xpfactory = XPathFactory.newInstance();
XPath path = xpfactory.newXPath();
NodeList nodes = (NodeList)path.evaluate("/staff/employee", doc,XPathConstants.NODESET);
for(int i = 0; i < nodes.getLength(); i++){
System.out.println(path.evaluate("name", nodes.item(i))); //输出每个员工的名字
}
System.out.println(path.evaluate("/staff/employee[1]/salary", doc)); //输出第一个员工国籍
System.out.println(path.evaluate("count(/staff/employee)", doc)); //得到员工总数
int salary = ((Number) path.evaluate("/staff/employee[1]/salary", doc,XPathConstants.NUMBER)).intValue(); //得到第一个员工的工资
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
catch(XPathExpressionException e){
e.printStackTrace();
}
使用命名空间
Java语言使用包来避免名字冲突。XML也有类似的命名空间机制 ,用于元素名和属性名。
名字空间是由统一资源标识符(URI)来标识,如:http://www.w3.org/2001/XMLSchema
下面是一个典型例子:
<xsd:shecma xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:element name="gridbag" type="GridBagType">
...
</xsd>
下面的属性:xmlns:alias="namespaceURI"用于定义命名空间和别名,上面例子中别名为xsd。这样,xsd:schema实际上指的是“命名空间http://www.w3.org/2001/XMLSchema中的schema”
注意:只有子元素继承了它们父元素的命名空间,而不带显式别名前缀的属性不是命名空间的一部分,如:
<configuration xmlns="http://www.horstmann.com/corejava"
xmlns:si="http://www.bipm.fr/enus/3_SI/si.html">
<size value="210" si:unit="mm"/>
...
</configuration>
在这个示例中,元素configuration和size是URI http://www.horstmann.com/corejava的命名空间的一部分。属性si:unit是URI http://www.bipm.fr/enus/3_SI/si.html命名空间的一部分,然而,属性值不是任何命名空间的一部分。默认地,Sun公司的DOM解析器是关闭了命名空间处理特性的。要打开命名空间处理特性,可以调用DocumentBuilderFactory类的setNamespaceAware方法:
factory.setNamespaceAware(true);
这样工作生产的所有生成器都支持命名空间了。每个节点有三个属性:
- 带有别名前缀的限定名,由getNodeName和getTagName等方法返回。
- 命名空间URI,由getNamescapceURI方法返回
- 不带别名前缀和命名空间的本地名,由getLocalName方法返回。
<xsd:shecma xmlns:xsd="http://www.w3.org/2001/XMLSchema">
会得到:
- 限定名为xsd:shecma
- 命名空间URI为http://www.w3.org/2001/XMLSchema
- 本地名为shecma
如果XPath要解析有命名空间的XML,还需要一些工作,
首先将XML内容修改为:
<?xml version="1.0"?>
<xsd:staff xmlns:xsd="http://www.test">
<xsd:employee nationality="china">
<name>bin</name>
<salary>500</salary>
</xsd:employee>
</xsd:staff>
再实现一个NamespaceContext接口,它做的工作是将文档中提取命名空间:
import java.util.Iterator;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import org.w3c.dom.Document;
public class UniversalNamespaceResolver implements NamespaceContext {
private Document sourceDocument;
public UniversalNamespaceResolver(Document document) {
sourceDocument = document;
}
public String getNamespaceURI(String prefix) {
if (prefix.equals(XMLConstants.DEFAULT_NS_PREFIX)) {
return sourceDocument.lookupNamespaceURI(null);
} else {
return sourceDocument.lookupNamespaceURI(prefix);
}
}
public String getPrefix(String namespaceURI) {
return sourceDocument.lookupPrefix(namespaceURI);
}
public Iterator getPrefixes(String namespaceURI) {
// not implemented yet
return null;
}
}
测试代码如下:
import java.io.*;
import javax.xml.parsers.*;
import javax.xml.xpath.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
public class DomStudy {
public static void main(String[] args) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
try {
DocumentBuilder buider = factory.newDocumentBuilder();
File f = new File("staff.xml");
Document doc = buider.parse(f);
XPathFactory xpfactory = XPathFactory.newInstance();
XPath path = xpfactory.newXPath();
path.setNamespaceContext(new UniversalNamespaceResolver(doc)); //设置XPath的命名空间
NodeList nodes = (NodeList)path.evaluate("/xsd:staff/xsd:employee", doc,XPathConstants.NODESET);
for(int i = 0; i < nodes.getLength(); i++){
Node node = nodes.item(i);
System.out.println(node.getNamespaceURI());
System.out.println(node.getLocalName());
System.out.println(node.getNodeName());
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
catch(XPathExpressionException e){
e.printStackTrace();
}
}
}
得到结果:
http://www.test
employee
xsd:employee
流机制解析器
当XML文档很大时,并且处理算法非常简单,可能在运行时解析节点,而不必看到所有的树形结构时,使用DOM可能显得效率低下,这时,应使用流机制解析器。
SAX解析器
SAX解析器在解析XML输入的构件时就报告事件,但不会以任何方式存储文档,而由事件处理器处理数据。实际上,DOM解析器是在SAX解析器的基础上建立起来的,它在接收到解析器事件时建立DOM树。
在使用SAX解析器,需要一个处理器来定义不同的解析器事件的事件动作,ContentHandler接口定义了若干个回调方法,下面是最重要的几个:
- startElement和endElement在每当遇到起始事终止标签时调用
- characters每当遇到字符数据时调用
- startDocument和endDocument分别在文档开始和结束各调用一次。
<font>
<size units="ps">36</size>
</font>
解析器确保产生以下调用:
1.startElement ,元素名:font
2.startElement, 元素名:size , 属性:units="pt"
3.characters, 内容:36
4.endElement, 元素名:size
5.endDocument, 元素名:font
处理器必须覆盖这些方法,让它们执行在解析文件时想要执行的动作。
注意:与DOM解析器一样,命名空间处理特性默认关闭。
如果使用下面代码处理上面带有命名空间的staff.xml
import java.io.*;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;
public class SAXStudy {
public static void main(String[] args) {
DefaultHandler handler = new DefaultHandler(){ //定义一个DefaultHandler,并覆盖startElement方法,输出相关信息
public void startElement(String uri, String localName,
String qName,Attributes attributes)throws SAXException{
System.out.println("URI:" + uri + " LocalName:" + localName + " qName:" + qName );
}
};
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
try{
SAXParser saxParser = factory.newSAXParser();
InputStream in = new FileInputStream("staff.xml");
saxParser.parse(in, handler);
in.close();
}
catch(ParserConfigurationException e){
e.printStackTrace();
}
catch(SAXException e){
e.printStackTrace();
}
catch(FileNotFoundException e){
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}
}
}
使用StAX解析器
StAX解析器是一种“拉解析器(pull parser)”,与安装事件处理器不同,只需要使用下面这样的基本循环来迭代所有的事件:
InputStream in = new FileInputStream("staff.xml");
XMLInputFactory factory = XMLInputFactory.newFactory();
XMLStreamReader parser = factory.createXMLStreamReader(in);
while(parser.hasNext()){
int event = parser.next();
call parser methods to obtain event details
}
如解析以下片断:
<font>
<size units="ps">36</size>
</font>
解析器将产生下面的事件:
1.START_ELEMENT, 元素名:font
2.CHARACTERS, 内容:空白字符
3.START_ELEMENT, 元素名:size
4.CHARACTERS, 内容:36
5.END_ELEMENT, 元素名:size
6.CHARACTERS, 内容:空白字符
7.END_ELEMENT, 元素名:font
下面是一个实现:
import java.io.*;
import javax.xml.namespace.QName;
import javax.xml.stream.*;
public class StAXTest {
public static void main(String[] args) {
try{
InputStream in = new FileInputStream("staff.xml");
XMLInputFactory factory = XMLInputFactory.newFactory();
XMLStreamReader parser = factory.createXMLStreamReader(in);
while(parser.hasNext()){
int event = parser.next();
if(event == XMLStreamConstants.START_ELEMENT){
QName qname = parser.getName();
System.out.println(qname.toString());
}
}
}
catch(FileNotFoundException e){
e.printStackTrace();
}
catch(XMLStreamException e){
e.printStackTrace();
}
}
}
生成XML文档
通过调用DocumentBuilder类的newDocument方法得到一个空文档:
Document doc = builder.newDocument();
使用Document类的createElement方法可以构建文档里的元素:
Element rootElement = doc.createElement(rootName);
Element childElement = doc.createElement(childName);
使用createTextNode方法构建文本节点:
Text textNode = doc.createTextNode(textContents);
使用以下方法给文档加上根元素,给父结节加上子节点:
doc.appendChild(rootElement);
rootElement.appendChild(childElement);
childElement.appendChild(textNode);
调用Element类的setAttribute方法设置元素属性:
rootElement.setAttrbute(name,value);
将doc输出到文件中,可以使用Transformer类,通过transform输出doc树
import java.io.File;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.w3c.dom.*;
public class WriteXML {
public static void main(String[] args) {
try{
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = builder.newDocument();
Element staff = doc.createElementNS("http://www.test", "xsd:staff"); //创建根结点
Element employee = doc.createElement("xsd:employee"); //创建employee结点
Element nameElem = doc.createElement("name"); //创建name结点
Text nameText = doc.createTextNode("bin"); //创建文本结点
Element salaryElem = doc.createElement("salary");
Text salaryText = doc.createTextNode("500");
//将结果组织到doc树中
doc.appendChild(staff);
staff.appendChild(employee);
employee.appendChild(nameElem);
nameElem.appendChild(nameText);
employee.appendChild(salaryElem);
salaryElem.appendChild(salaryText);
//将doc树输出到文件中
Transformer t = TransformerFactory.newInstance().newTransformer();
//设置输出格式
t.setOutputProperty(OutputKeys.METHOD,"xml");
t.setOutputProperty(OutputKeys.INDENT, "yes");
File f = new File("writerStaff.xml");
t.transform(new DOMSource(doc), new StreamResult(f));
}
catch(ParserConfigurationException e){
e.printStackTrace();
}
catch(TransformerConfigurationException e){
e.printStackTrace();
}
catch(TransformerException e){
e.printStackTrace();
}
}
}
使用StAX写XML文档
StAX API使我们可以直接将XML树写出,先构建一个XMLStreamWriter:
XMLOutputFactory factory = XMLOutputFactory.newInstance();
XMLStreamWriter writer = factory.createXMLStreamWriter(out);
要产生XML文件头,调用:
writer.writeStartDocument();
然后要产生元素则调用 :
writer.writeStartElement(name);
添加属性需要调用:
writer.writeAttribute(name,value);
写出字符则调用:
writer.writeCharacters(text);
要写出没有子节点的元素可调用:
writer.writeEmptyElement);
在添加完所有子节点后,调用:
writer.writeEndElement();
这会导致当前元素被关闭
最后,在文档的结尾,调用
writer.writeEndDocument();
调用将关闭所有的元素。
注意:与使用DOM/XSLT方式一样,不必担心属性值和字符数据的转义字符。并且,StAX当前的版本还没有任何对产生缩进输出的支持。
下面是一个实例:
import java.io.*;
import javax.xml.stream.*;
public class StAXWriterXML {
public static void main(String[] args) {
try{
FileOutputStream out = new FileOutputStream("StAXWriterStaff.xml");
XMLOutputFactory factory = XMLOutputFactory.newInstance();
XMLStreamWriter writer = factory.createXMLStreamWriter(out);
writer.writeStartDocument();
writer.writeStartElement("staff");
writer.writeStartElement("employee");
writer.writeStartElement("name");
writer.writeCharacters("bin");
writer.writeEndElement();
writer.writeEndElement();
writer.writeEndDocument();
writer.close();
out.close();
}
catch (XMLStreamException e) {
e.printStackTrace();
}
catch(FileNotFoundException e){
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}
}
}
XSL转换
XSL转换 机制可以指定将XML文档转换为其他格式的规则,例如,纯文本,XHTML或其他任何XML格式。XSLT通常用于将一个机器可读的XML格式转译为另一种机器可读的XML格式,或者将XML转译为适于人类阅读的表示格式。
具体方法请参看Java核心技术。