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核心技術。