在項目里,我們往往會把一些配置信息放到xml文件里,或者各部門間會通過xml文件來交換業務數據,所以有時候我們會遇到“解析xml文件”的需求。一般來講,有基於DOM樹和SAX的兩種解析xml文件的方式,在這部分里,將分別給大家演示通過這兩種方式解析xml文件的一般步驟。
1 XML的文件格式
XML是可擴展標記語言(Extensible Markup Language)的縮寫,在其中,開始標簽和結束標簽必須配套地出現,我們來看下book.xml這個例子。
1 <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 <books> 3 <book id="01"> 4 <name>Java</name> 5 <price>15</price> 6 <memo>good book</memo> 7 </book> 8 <book id="02"> 9 <name>FrameWork</name> 10 <price>20</price> 11 <memo>new book</memo> 12 </book> 13 </books>
整個xml文件是一個文檔(document),其中第1行表示文件頭,在第2和第13行里,我們能看到配套出現的books標簽,從標簽頭到標簽尾的部分那我們稱之為元素(element)。
所以我們可以這樣說,在books元素里,我們分別於第3到第7行和第8到第12行定義了2個book元素,在每個book元素,比如從第4到第6行,又包含着3個元素,比如第一本書的name元素是<name>Java</name>,它的name元素值是Java。
在第3行里,我們還能看到元素里的屬性(attribute),比如這個book元素具有id這個屬性,具體id的屬性值是01。
2 基於DOM樹的解析方式
DOM是Document Object Model(文檔對象模型)的縮寫,在基於DOM樹的解析方式里,解析代碼會先把xml文檔讀到內存里,並整理成DOM樹的形式,隨后再讀取。根據之前部分里給出的book.xml文檔,我們可以繪制出如下形式的DOM樹。

其中,books屬於根(root)結點,也叫根元素,由於它包含着兩個book元素,所以第二層是兩個book結點,每個book元素包含着3個元素,所以第三層是6個元素。在下面的ParserXmlByDom.java的代碼里,我們來看下通過DOM樹方式解析book.xml文檔的詳細步驟。
1 //省略import相關類庫的代碼
2 public class ParserXmlByDom {
3 public static void main(String[] args) {
4 //創建DOM工廠
5 DocumentBuilderFactory domFactory=DocumentBuilderFactory.newInstance();
6 InputStream input = null;
7 try {
8 //通過DOM工廠獲得DOM解析器
9 DocumentBuilder domBuilder=domFactory.newDocumentBuilder();
10 //把XML文檔轉化為輸入流
11 input=new FileInputStream("src/book.xml");
12 //解析XML文檔的輸入流,得到一個Document
13 Document doc=domBuilder.parse(input);
從第5行到第13行,我們完成了用DOM樹解析XML文件的准備工作,具體包括,在第5行里創建了DOM工廠,在第9行通過DOM工廠創建了解析xml文件DocumentBuilder類型對象,在第11行把待解析的xml文件放入到一個InputStream類型的對象里,在第13行通過parse方法把xml文檔解析成一個基於DOM樹結構的Document類型對象。
14 //得到XML文檔的根節點,只有根節點是Element類型 15 Element root=doc.getDocumentElement(); 16 // 得到子節點 17 NodeList books = root.getChildNodes();
整個XML文件包含在第13行定義的doc對象里,在第15行里,我們通過getDocumentElement方法得到了根節點(也就是books節點),在第17行,通過getChildNoes方法得到該books節點下的所有子節點,隨后開始解析整個xml文檔。
需要說明的是,在解析前,我們會通過觀察xml文檔來了解其中的元素名和屬性名,所以在后繼的代碼里,我們會針對元素名和屬性名進行編程。
18 if(books!=null){
19 for(int i=0;i<books.getLength();i++){
20 Node book=books.item(i);
21 //獲取id屬性
22 if(book.getNodeType()==Node.ELEMENT_NODE){
23 String id=book.getAttributes().getNamedItem("id").getNodeValue();
24 System.out.println("id is:" + id);
25 //遍歷book下的子節點
26 for(Node node=book.getFirstChild(); node!=null;node=node.getNextSibling()){
27 if(node.getNodeType()==Node.ELEMENT_NODE){
28 //依次讀取book里的name,price和memo三個子元素
29 if(node.getNodeName().equals("name")){
30 String name=node.getFirstChild().getNodeValue();
31 System.out.println("name is:" + name);
32 }
33 if(node.getNodeName().equals("price")){
34 String price=node.getFirstChild().getNodeValue();
35 System.out.println("price is:" + price);
36 }
37 if(node.getNodeName().equals("memo")){
38 String memo=node.getFirstChild().getNodeValue();
39 System.out.println("memo is:" + memo);
40 }
41 }
42 }
43 }
44 }
45 }
第19行的for循環里,我們是遍歷book元素通過觀察xml文件,我們發現book元素出現了2次,所有這個循環會運行兩次,而且,book元素有1個id屬性,所有我們需要通過第23行的代碼,得到id屬性的值。
在文檔里,book元素有3個子節點,分別是name,price和memo,所以在代碼的26行里,再次使用for循環遍歷其中的子節點。在遍歷時,我們通過29到32行的代碼獲取到了book元素里name的值,通過類似的代碼后繼的33到40行代碼里得到了price和memo這兩個元素的值。
46 } catch (ParserConfigurationException e) {
47 e.printStackTrace();
48 } catch (FileNotFoundException e) {
49 e.printStackTrace();
50 } catch (IOException e) {
51 e.printStackTrace();
52 } catch (SAXException e) {
53 e.printStackTrace();
54 } catch (Exception e) {
55 e.printStackTrace();
56 }
57 //在finally里關閉io流
58 finally{
59 try {
60 input.close();
61 } catch (IOException e) {
62 e.printStackTrace();
63 }
64 }
65 }
66 }
同樣地,在解析完成后,在finally從句里,我們關閉了之前用到的IO流(input對象)。
3 基於事件的解析方式
SAX是Simple API for XML的縮寫,不同於DOM的文檔驅動,它是事件驅動的,也就是說,它是一種基於回調(callback)函數的解析方式,比如開始解析xml文檔時,會調用我們自己定義的startDocument函數,從下表里,我們能看到基於SAX方式里的各種回調函數以及它們被調用的時間點。
| 函數名 |
調用時間點 |
| startDocument |
開始解析xml文檔時(解析xml文檔第一個字符時)會被調用 |
| endDocument |
當解析完xml文檔時(解析到xml文檔最后一個字符時)會被調用 |
| startElement |
當解析到開始標簽時會被調用,比如在解析“<name>FrameWork</name>”這個element時,當讀到開始標簽“<name>”時,會被調用 |
| endElement |
當解析到結束標簽時會被調用,比如在解析“<name>FrameWork</name>”這個element時,當讀到結束標簽“</name>”時,會被調用 |
| characters |
1行開始后,遇到開始或結束標簽之前存在字符,則會調用 2兩個標簽之間,存在字符,則會調用,比如在解析“<name>FrameWork</name>”時,發現存在FrameWork,則會被調用 3標簽和行結束符之前存在字符,則會調用 |
從上表里我們能看到characters方法會在多個場合被回調,但我們最期望的調用場景是第2種,這就要求我們最好在解析xml文檔前整理下它的格式,盡量避免第1和第3種情況。在ParserXmlBySAX.java這個案例中,我們通過了編寫上述的回調函數,實現了SAX方式解析xml文檔的功能。
1 //省略import的代碼
2 //基於SAX的解析代碼需要繼承DefaultHandler類
3 public class ParserXmlBySAX extends DefaultHandler{
4 // 記錄當前解析到的節點名
5 private String tagName;
6 //主方法
7 public static void main(String[] argv) {
8 String uri = "src/book.xml";
9 try {
10 SAXParserFactory parserFactory = SAXParserFactory.newInstance();
11 ParserXmlBySAX myParser = new ParserXmlBySAX();
12 SAXParser parser = parserFactory.newSAXParser();
13 parser.parse(uri, myParser);
14 } catch (IOException ex) {
15 ex.printStackTrace();
16 } catch (SAXException ex) {
17 ex.printStackTrace();
18 } catch (ParserConfigurationException ex) {
19 ex.printStackTrace();
20 } catch (FactoryConfigurationError ex) {
21 ex.printStackTrace();
22 }
23 }
在main方法的第8行里,我們指定了待解析xml文檔的路徑和文件名,在第10行里,我們創建了SAXParserFactory這個類型的SAX解析工廠對象。在第12行,我們通過SAX解析工廠對象,創建了SAXParser這個類型的解析類。在第13行,通過了parse方法啟動了解析。
在上文里我們就已經知道,在SAX的方式里,是通過調用各種回調函數來完成解析的,所以在代碼里,我們還得自定義各個回調函數,代碼如下。
// 處理到文檔結尾時,直接輸出,不做任何動作
25 public void endDocument() throws SAXException {
26 System.out.println("endDocument");
27 }
28 // 處理到結束標簽時,把記錄當前標簽名的tagName設置成null
29 public void endElement(String uri, String localName, String qName) throws SAXException {
30 tagName = null;
31 }
32 // 開始處理文檔時,直接輸出,不做任何動作
33 public void startDocument() throws SAXException {
34 System.out.println("startDocument");
35 }
36 // 處理開始標簽
37 public void startElement(String uri, String localName, String name,Attributes attributes) throws SAXException {
38 if ("book".equals(name)) { //解析book標簽的屬性
39 for (int i = 0; i < attributes.getLength(); i++) {
40 System.out.println("attribute name is:" + attributes.getLocalName(i) + " attribute value:" + attributes.getValue(i));
41 }
42 }
43 //把當前標簽的名字記錄到tagName這個變量里
44 tagName = name;
45 }
46 //通過這個方法解析book的三個子元素的值
47 public void characters(char[] ch, int start, int length)
48 throws SAXException {
49 if(this.tagName!=null){
50 String val=new String(ch,start,length);
51 //如果是name,price或memo,則輸出它們的值
52 if("name".equals(tagName))
53 { System.out.println("name is:" + val); }
54 if("price".equals(tagName))
55 { System.out.println("price is:" + val); }
56 if("memo".equals(tagName))
57 { System.out.println("memo is:" + val); }
58 }
59 }
60 }
我們用tagName來保存當前的標簽名,是為了解析book元素的name,price和memo這三個子元素。
<name>FrameWork</name>
比如當解析到name這個開始標簽時,在第44行里,startElement會把tagname值設置成name,當解析到FramWork時,由於它包含在兩個標簽之間,所以會被觸發第47行的characters方法,在其中的第52行的if判斷里,由於得知當前的標簽名是name,所以會輸出FrameWork這個name元素的值,當解析到</name>這個結束標簽時,會觸發第29行的endElement方法,在其中的30行里,會把tagName值清空。
這段代碼的輸出結果如下,其中第1行和第10行分別是在開始解析和完成解析時輸出的。
第2行針對id屬性的輸出是在startElement方法的第40行里被打印的,第3到第5行針對3個book子元素的輸出是在characters方法里被打印的。
第2到第5行是針對第一個book元素的輸出,而第6到第9行是針對第2個book。
1 startDocument 2 attribute name is:id attribute value:01 3 name is:Java 4 price is:15 5 memo is:good book 6 attribute name is:id attribute value:02 7 name is:FrameWork 8 price is:20 9 memo is:new book 10 endDocument
4 DOM和SAX兩種解析方式的應用場景
在基於DOM的方式里,由於我們會把整個xml文檔以DOM樹的方式裝載到內存里,所以可以邊解析邊修改,而且還能再次解析已經被解析過的內容。
而在SAX的方式里,由於我們是以基於回調函數的方式來解析,所以並不需要把整個文檔載入到內存,這樣能節省內存資源。
所以說,選擇 DOM 還是 SAX,這取決於如下三個個因素。
第一,如果我們在解析時還打算更新xml里的數據,那么建議使用DOM方式。
第二,如果待解析的文件過大,把它全部裝載到內存時可能會影響到內存性能,那么建議使用SAX的方式。
第三,如果我們對解析的速度有一定的要求,那么建議使用SAX方式,因為它比DOM方式要快些。
