前面一篇文章說到了在java中使用DOM解析和操作XML,這篇文章講解如何在java中使用SAX解析和操作XML,在手機平台上SAX的有點非常鮮明,其中基於XML事件的機制使得可以節省不少的記憶體。
-----文章本文-----
來源:http://www0.ccidnet.com/tech/guide/2001/10/08/58_3392.html
SAX概念
SAX是Simple API for XML的縮寫,它並不是由W3C官方所提出的標準,可以說是“民間”的事實標準。實際上,它是一種社區性質的討論產物。雖然如此,在XML中對SAX的應用絲毫不比DOM少,幾乎所有的XML解析器都會支援它。
與DOM比較而言,SAX是一種輕量型的方法。我們知道,在處理DOM的時候,我們需要讀入整個的XML文檔,然後在記憶體中建立DOM樹,產生DOM樹上的每個Node對象。當文檔比較小的時候,這不會造成什麼問題,但是一旦文檔大起來,處理DOM就會變得相當費時費力。特別是其對於記憶體的需求,也將是成倍的增長,以至於在某些應用中使用DOM是一件很不划算的事(比如在applet中)。這時候,一個較好的替代解決方案就是SAX。
SAX在概念上與DOM完全不同。首先,不同於DOM的文檔驅動,它是事件驅動的,也就是說,它並不需要讀入整個文檔,而文檔的讀入過程也就是SAX的解析過程。所謂事件驅動,是指一種基於回調(callback)機制的程式運行方法。(如果你對Java新的代理事件模型比較清楚的話,就會很容易理解這種機制了)
在XMLReader接受XML文檔,在讀入XML文檔的過程中就進行解析,也就是說讀入文檔的過程和解析的過程是同時進行的,這和DOM區別很大。解析開始之前,需要向XMLReader註冊一個ContentHandler,也就是相當於一個事件監聽器,在ContentHandler中定義了很多方法,比如startDocument(),它定製了當在解析過程中,遇到文檔開始時應該處理的事情。當XMLReader讀到合適的內容,就會拋出相應的事件,並把這個事件的處理權代理給ContentHandler,調用其相應的方法進行響應。
這樣泛泛的說來或許有些不容易理解,別急,後面的例子會讓你明白SAX的解析過程。看看這個簡單XML檔案:
<;POEM> <AUTHOR>Ogden Nash</AUTHOR> <TITLE>Fleas</TITLE> <LINE>Adam</LINE> </POEM> |
當XMLReader讀到<;POEM>標籤時,就會調用ContentHandler.startElement()方法,並把標籤名POEM作為參數傳遞過去。在你實現的startElement()方法中需要做相應的動作,以處理當<;POEM>出現時應該做的事情。各個事件隨著解析的過程(也就是文檔讀入的過程)一個個順序的被拋出,相應的方法也會被順序的調用,最後,當解析完成,方法都被調用後,對文檔的處理也就完成了。下面的這個表,列出了在解析上面的那個XML檔案的時候,順序被調用的方法:
遇到的項目 |
方法回調 |
| {文檔開始} |
startDocument() |
| <;POEM> |
startElement(null,"POEM",null,{Attributes}) |
| "/n" |
characters("<;POEM>/n...", 6, 1) |
| <AUTHOR> |
startElement(null,"AUTHOR",null,{Attributes}) |
| "Ogden Nash" |
characters("<;POEM>/n...", 15, 10) |
| </AUTHOR> |
endElement(null,"AUTHOR",null) |
| "/n" |
characters("<;POEM>/n...", 34, 1) |
| <TITLE> |
startElement(null,"TITLE",null,{Attributes}) |
| "Fleas" |
characters("<;POEM>/n...", 42, 5) |
| </TITLE> |
endElement(null,"TITLE",null) |
| "/n" |
characters("<;POEM>/n...", 55, 1) |
| <LINE> |
startElement(null,"LINE",null,{Attributes}) |
| "Adam" |
characters("<;POEM>/n...", 62, 4) |
| </LINE> |
endElement(null,"LINE",null) |
| "/n" |
characters("<;POEM>/n...", 67, 1) |
| </POEM> |
endElement(null,"POEM",null) |
| {文檔結束} |
endDocument() |
ContentHandler實際上是一個介面,當處理特定的XML檔案的時候,就需要為其建立一個實現了ContentHandler的類來處理特定的事件,可以說,這個實際上就是SAX處理XML檔案的核心。下面我們來看看定義在其中的一些方法:
| void characters(char[] ch, int start, int length): |
這個方法用來處理在XML檔案中讀到字串,它的參數是一個字元數組,以及讀到的這個字串在這個數組中的起始位置和長度,我們可以很容易的用String類的一個構造方法來獲得這個字串的String類:String charEncontered=new String(ch,start,length)。
當遇到文檔的開頭的時候,調用這個方法,可以在其中做一些預先處理的工作。
和上面的方法相對應,當文檔結束的時候,調用這個方法,可以在其中做一些善後的工作。
| void startElement(java.lang.String namespaceURI, java.lang.String localName, java.lang.String qName, Attributes atts) |
當讀到一個開始標籤的時候,會觸發這個方法。在SAX1.0版本中並不支援名域,而在新的2.0版本中提供了對名域的支援,這兒參數中的namespaceURI就是名域,localName是標籤名,qName是標籤的修飾首碼,當沒有使用名域的時候,這兩個參數都未null。而atts是這個標籤所包含的屬性列表。通過atts,可以得到所有的屬性名稱和相應的值。要注意的是SAX中一個重要的特點就是它的串流,在遇到一個標籤的時候,它並不會紀錄下以前所碰到的標籤,也就是說,在startElement()方法中,所有你所知道的資訊,就是標籤的名字和屬性,至於標籤的嵌套結構,上層標籤的名字,是否有子元屬等等其它與結構相關的資訊,都是不得而知的,都需要你的程式來完成。這使得SAX在編程處理上沒有DOM來得那麼方便。
| void endElement(java.lang.String namespaceURI, java.lang.String localName, java.lang.String qName) |
這個方法和上面的方法相對應,在遇到結束標籤的時候,調用這個方法。
因為ContentHandler是一個介面,在使用的時候可能會有些不方便,因而,SAX中還為其制定了一個Helper類:DefaultHandler,它實現了這個介面,但是其所有的方法體都為空白,在實現的時候,你只需要繼承這個類,然後重載相應的方法即可。
OK,到這兒SAX的基本知識已經差不多講完了,下面我們來看看兩個具體的例子,以更好的理解SAX地用法。
SAX編程執行個體
我們還是沿用講DOM的時候使用的那個文檔例子,但首先,我們先看一個簡單一些的應用,我們希望能夠統計一下XML檔案中各個標籤出現的次數。這個例子很簡單,但是足以闡述SAX編程的基本思路了。
一開始當然還是import語句了:
import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.*; import org.xml.sax.*; import org.xml.sax.helpers.*; import java.util.*; import java.io.*; |
然後,我們建立一個繼承於DefaultHandler的類,具體的程式邏輯在這兒可以暫且放在一邊,要注意的是程式的結構:
public class SAXCounter extends DefaultHandler { private Hashtable tags; //這個Hashtable用來記錄tag出現的次數 // 處理文檔前的工作 public void startDocument() throws SAXException { tags = new Hashtable();//初始化Hashtable } //對每一個開始元屬進行處理 public void startElement(String namespaceURI, String localName, String rawName, Attributes atts) throws SAXException { String key = localName; Object value = tags.get(key); if (value == null) { // 如果是新碰到的標籤,這在Hastable中添加一條記錄 tags.put(key, new Integer(1)); } else { // 如果以前碰到過,得到其計數值,並加1 int count = ((Integer)value).intValue(); count++; tags.put(key, new Integer(count)); } } //解析完成後的統計工作 public void endDocument() throws SAXException { Enumeration e = tags.keys(); while (e.hasMoreElements()) { String tag = (String)e.nextElement(); int count = ((Integer)tags.get(tag)).intValue(); System.out.println("Tag <" + tag + "> occurs " + count + " times"); } } //程式入口,用來完成解析工作 static public void main(String[] args) { String filename = null; boolean validation = false; filename="links.xml"; SAXParserFactory spf = SAXParserFactory.newInstance(); XMLReader xmlReader = null; SAXParser saxParser=null; try { // 建立一個解析器SAXParser對象 saxParser = spf.newSAXParser(); // 得到SAXParser中封裝的SAX XMLReader xmlReader = saxParser.getXMLReader(); } catch (Exception ex) { System.err.println(ex); System.exit(1); } try { //使用指定的ContentHandler,解析給XML檔案,這兒要注意的是,為了 //程式的簡單起見,這兒將主程式和ContentHandler放在了一起。實際上 //main方法中所作的所有事情,都與ContentHandler無關。 xmlReader.parse(new File(filename),new SAXCounter()); } catch (SAXException se) { System.err.println(se.getMessage()); System.exit(1); } catch (IOException ioe) { System.err.println(ioe); System.exit(1); } } } |
我們來看看這段程式作了些什麼,在main()方法中,主要做的就是建立解析器,然後解析文檔。實際上,在這兒建立SAXParser對象的時候,為了使程式碼於具體的解析器無關,使用了同DOM中一樣的設計技巧:通過一個SAXParserFactory類來建立具體的SAXParser對象,這樣,當需要使用不同的解析器的時候,要改變的,只是一個環境變數的值,而程式的代碼可以保持不變。這就是FactoryMethod模式的思想。在這兒不再具體講了,如果還有不明白的,可以參看上面DOM中的解釋,原理是一樣的。
不過在這兒還有一點點要注意的地方,就是SAXParser類和XMLReader類之間的關係。你可能有些迷糊了吧,實際上SAXParser是JAXP中對XMLReader的一個封裝類,而XMLReader是定義在SAX2.0種的一個用來解析文檔的介面。你可以同樣的調用SAXParser或者XMLReader中的parser()方法來解析文檔,效果是完全一樣的。不過在SAXParser中的parser()方法接受更多的參數,可以對不同的XML文檔資料來源進行解析,因而使用起來要比XMLReader要方便一些。
這個例子僅僅涉及了SAX的一點皮毛,而下面的這個,可就要進階一些了。下面我們要實現的功能,在DOM的例子中已經有實現了,就是從XML文檔中讀出內容並格式化輸出,雖然程式邏輯看起來還是很簡單,但是SAX可不比DOM哦,看著吧。
前面說過,當遇到一個開始標籤的時候,在startElement()方法中,我們並不能夠得到這個標籤在XML文檔中所處的位置。這在處理XML文檔的時候是個大麻煩,因為在XML中標籤的語義,有一部分是由其所處的位置所決定的。而且在一些需要驗證文檔結構的程式中,這更是一個問題。當然,沒有解決不了的問題了,我們可以使用一個棧來實現對文檔結構的紀錄。
棧的特點是先進先出,我們現在的想法是,在startElemnt()方法中用push將這個標籤的名字添加到棧中,在endElement()方法中在把它pop出來。我們知道對一個結構良好的XML而言,其嵌套結構是完備的,每一個開始標籤總會對應一個結束標籤,而且不會出現標籤嵌套之間的錯位。因而,每一次startElement()方法的調用,必然會對應一個endElement()方法的調用,這樣push和pop也是成對出現的,我們只需要分析棧的結構,就可以很容易的知道當前標籤所處在文檔結構中的位置了。
public class SAXReader extends DefaultHandler { java.util.Stack tags=new java.util.Stack(); //--------------XML Content------------- String text=null; String url=null; String author=null; String description=null; String day=null; String year=null; String month=null; //---------------------------------------------- public void endDocument() throws SAXException { System.out.println("------Parse End--------"); } public void startDocument() throws SAXException { System.out.println("------Parse Begin--------"); } public void startElement(String p0, String p1, String p2, Attributes p3) throws SAXException { tags.push(p1); } public void endElement(String p0, String p1, String p2) throws SAXException { tags.pop(); //一個link節點的資訊收集齊了,將其格式化輸出 if (p1.equals("link")) printout(); } public void characters(char[] p0, int p1, int p2) throws SAXException { //從棧中得到當前節點的資訊 String tag=(String) tags.peek(); if (tag.equals("text")) text=new String(p0,p1,p2); else if (tag.equals("url")) url=new String(p0,p1,p2); else if (tag.equals("author")) author=new String(p0,p1,p2); else if (tag.equals("day")) day=new String(p0,p1,p2); else if (tag.equals("month")) month=new String(p0,p1,p2); else if (tag.equals("year")) year=new String(p0,p1,p2); else if (tag.equals("description")) year=new String(p0,p1,p2); } private void printout(){ System.out.print("Content: "); System.out.println(text); System.out.print("URL: "); System.out.println(url); System.out.print("Author: "); System.out.println(author); System.out.print("Date: "); System.out.println(day+"-"+month+"-"+year); System.out.print("Description: "); System.out.println(description); System.out.println(); } static public void main(String[] args) { String filename = null; boolean validation = false; filename="links.xml"; SAXParserFactory spf = SAXParserFactory.newInstance(); SAXParser saxParser=null; try { saxParser = spf.newSAXParser(); } catch (Exception ex) { System.err.println(ex); System.exit(1); } try { saxParser.parse(new File(filename),new SAXReader()); } catch (SAXException se) { System.err.println(se.getMessage()); System.exit(1); } catch (IOException ioe) { System.err.println(ioe); System.exit(1); } } } |
在這兒雖然沒有使用到棧的分析,但實際上棧的分析是一件很容易的事情,應為java.util.Stack繼承了java.util.Vector類,而且Stack中的元素是按棧的結構由底至上排列的,因個,我們可以使用Vector類的size()方法來得到Stack的元素個數,還可以使用Vector的get(int)方法來得到具體的每一個元屬。實際上,如果把Stack的元素從底向上逐一排列出來,我們就得到了從XML根節點到當前節點的一條唯一的路徑,有了這條路徑的資訊,文檔的結構就在清楚不過了。
小節
好了,到這兒為止,我們已經掌握了對於XML編程的兩大利器:DOM和SAX,也知道了該如何在一個Java程式中使用它們。DOM編程相對簡單,但是速度比較慢,佔用記憶體多,而SAX編程複雜一些,但是速度快,佔用記憶體少。所以,我們應該根據不同的環境選擇使用不同的方法。大部分的XML應用基本都可以用它們來解決。需要特別說明的是,DOM和SAX其實都是語言無關的,並非Java所專屬,也就是說,只要有相應的語言實現,DOM和SAX可以應用在任何物件導向的語言中。
上面我們介紹了XML文檔的讀入,內容提取,以及文檔添加和修改的一些方法。還有一類的問題就是XML文檔的轉換,雖然用DOM和SAX也可以解決,但是實現起來很複雜,而應用XSLT就會簡單許多。這個問題,筆者將會在以後的文章中再和大家詳細討論。