即便對高級 XML 問題具有豐富經驗的開發人員也不一定就完全了解 XML 最基本的一些問題。為了為您打下堅實的基礎,本文討論了最基本的 XML 服務:解析。本文介紹了各種解析方法,著重說明了各自的優缺點。
了解基礎
從 XML 的出現至今大約有 9 年的時間了。對於可擴展標記語言來說這是一段不短的歷程。現在很難找到完全不用 XML 的應用程序了。
但是和客戶在一起的時候,仍然不可避免地發現基礎性的東西尚未被透徹地全部理解。對復雜的 XML 主題理解透徹的開發人員,最近卻發現對基礎性的東西(比如解析)的把握還存在很多不足,這真有點出人意料。
而對 XML 的處理是從何處開始的呢?沒錯,是解析。解析可能是開發人員能夠使用的最基本的服務。解析器讀取 XML 文檔,解釋語法並向應用程序傳遞有意義的對象。解析器可能還提供其他服務,比如驗證(保證文檔符合 XML Schema 或 DTD)或者名稱空間解析。
本文介紹了各種解析方法,著重分析了各自的優缺點,幫助您在下一個項目中選擇適當的工具。文中包含大量文章的鏈接,選擇工具的時候可以詳細研究給定的 API。
解析的重要性
解析為什麼重要?因為所有 XML 處理都從解析開始。無論使用高層編程語言(如 XSLT)還是低層 Java 編程,第一步都是要讀入 XML 文件,解碼結構和檢索信息等等,這就是解析。
解析文檔時面臨的第一個選擇是采用現成的解析庫(基本上每種編程語言都有,包括 COBOL [Common Business OrIEnted Language])還是自己創建一個。答案非常簡單:選擇現成的庫。
坦白地說,XML 不是一種多麼復雜的語法,因此認為可以自己通過正則表達式或其他特殊方法來解析的想法是可以理解的。但實際上卻很難成功:XML 語法要求支持多種編碼和很多難以捉摸的特性,比如 CDATA 節和實體。自定義的實現幾乎很難照顧到所有這些方面,因而造成了不兼容性。
相反,隨開發環境提供的解析器大都經過了與兼容性有關的測試。采用 XML 這樣的標准語法的主要原因是兼容其他應用程序和工具箱,這是真正值得使用經過良好測試的庫的情況之一。
多數解析器提供了至少兩種 API,通常是一個對象模型 API 和一個事件 API(也稱為流 API)。比如,Java 平台同時提供了 DOM(文檔對象模型)和 SAX(Simple API for XML)。
這兩套 API 提供了相同的服務:文檔解碼、可選的驗證、名稱空間解析等等。差別不在於服務而在於 API 使用的數據模型。
關鍵的選擇:第一種方法
對象模型 API 定義了層次化對象模型來表示 XML 文檔。換句話說,對應 XML 語法中的每個概念定義相應的類:元素、屬性、實體、文檔。解析器讀入 XML 文檔的時候,建立 XML 語法和類之間的一對一映射。比如,每遇到一個標記,就實例化一個元素類。
毫不奇怪,對哪種數據模型最好存在一些爭議。W3C 規范化了 DOM,它的主要優點是可移植性:它是作為一種 CORBA 接口定義的,被映射到很多語言。因此如果了解了 Javascript 中的 DOM,也就知道了 Java、C++、Perl、Python 和其他語言中的 DOM。
另一種數據模型是 JDOM,一種針對 Java 優化的 DOM(專用於 Java),和 Java 語言結合得更緊密,但是按照定義缺乏可移植性。
盡管人們可以繼續商討對 XML 語法來說哪種數據模型最好,但我認為沒有多少意義,因為各種基於對象的 API 其優點和不足基本上是一樣的。從好的方面來說,如果熟悉 XML 語法的話,對象模型 API 更容易理解。因為它直接從 XML 語法映射到類,很容易學習、使用和調試。
簡單的代價是效率,至少對很多項目而言是這樣。讀入文檔的時候,解析器根據語法結構創建對象。對很多應用程序來說,XML 語法並不是很合適:
XML 語法非常羅嗦,即使文檔很小,解析器也要創建很多對象。
對 XML 詞匯表進行的優化通常針對的是存儲和數據傳輸效率,而不是處理,因而應用程序可能需要對數據進行預處理,比方說,在開始真正的處理之前,先計算部分和或者合並其他來源的數據。很多情況下,在處理之前必須將數據從 XML 對象模型復制到應用程序專用的對象模型或者數據庫。
因為這種對象模型是通用的,包含很多應用程序並不需要的對象之間的引用(比如,從子元素到父元素的反向引用)。這些引用進一步增加了內存消耗。
在桌面上處理小型文檔這可能不是大問題,但是在其他環境中,比如服務器上,對象模型固有的低效率是不可接受的。
第二種方法
第二種選擇是事件API,比如 SAX。這個概念是上述對象模型方式的一種反映。只不過這種方法不根據 XML 語法定義通用的數據模型,其解析器依賴應用程序程序員建立定制的數據模型。
因此解析器可以做得更小,因為只需要傳遞最少量的信息。更重要的是,和一個型號打天下的對象模型(不管對象模型多麼好)相比總的效率更高,程序員可以根據應用程序的需要定制對象模型。
它的優點很明顯:
統計應用程序或總結信息的任何應用程序都可以從中獲益,因為它們的數據模型只需計算總計而無需復制整個文檔。
類似的,即使動態處理文檔的應用程序(比如把文檔加載到數據庫中)不需處理或者只需少量處理,也可從中受益,因為根本不需要存儲數據。
由於減少了內存需求,事件 API 可以處理任意大小的文檔,包括大小超過可用內存的文檔。基於同樣的原因,這類 API 也非常適合多個進程並發執行和共享內存的服務器。
效率的代價是簡單性的損失。事件 API 一向以難用著稱,因為應用程序員要負責更多的操作。雖然短期看來如此,但根據我的經驗,從中期和長期來看,效率上的改進足以抵消略微增加的復雜度。
流式API 有兩種形式:推式和拉式。從歷史上看,推式方法更加流行,因為這正是 SAX 采用的模型。推式方法正在實現標准化,很快將作為 StAX 集成到 Java 平台中。
兩者有什麼區別呢?區別在於由誰控制讀循環。和讀取文件的任何軟件一樣,解析器也是圍繞著讀循環(讀入文件的循環)創建的。
在推模式(SAX)下,解析器控制循環。實際上應用程序調用解析器的時候,在文件結束之前控制權不會返回給應用程序。前面已經提到,解析器回調應用程序以建立數據模型,解析器處於控制地位。
在拉模式下,應用程序控制循環。循環中應用程序負責反復調用解析器,直到文件結束。
推模式最適合邊讀入邊處理 XML 文檔,比如讀入 RSS 提要並顯示為 Html 網頁。對於使用 XML 存儲數據的多數應用程序來說,“讀文檔”用對解析器的一次調用實現最方便。
拉模式更適合於處理不同 XML 詞匯表的文檔。這類應用程序通常需要嗅探輸入(讀入前幾行)以根據詞匯表決定調用子例程。
對於控制解析器的應用程序而言,一次循環是必要的,因為應用程序很容易在嗅探前面幾行之後停止讀入。
第三種方法
如果不提到另一種選擇,即 XML 編組庫形式的解析,如 Castor,本文就不完整。該方法介於對象模型和事件方法之間。
其思想是從 XML Schema 生成一個對象模型而不是通用模型(如 DOM),解析器生成更加針對所用詞匯表的數據模型。比方說,如果詞匯表處理的是發貨單,那麼可以預料其中會包含發送方、接收方、日期、產品類別、產品標識、單價和總價。DOM 將這些元素映射到一個一般性的元素類。編組庫 為發送方、接收方、日期、產品類別、產品標識、單價、總價和文檔中出現的其他元素創建專門的類。
從處理的是根據詞匯表定制(與根據應用程序的需要定制可能相同,也可能不同)的而不是通用數據模型這方面來講,編組庫具備事件 API 的一些優點。
如何寫入XML 呢?
解析器讀取和解碼 XML 文檔,將其從磁盤上轉到內存中。那麼另一個方向上的移動該如何處理呢?如果應用程序需要將數據存儲到 XML 文件中怎麼辦?
雖然我建議您避免使用特殊的例程解碼 XML 文檔,但是對於寫入 XML 沒有這樣的疑慮。讀的時候必須保證實現了所有的規則,包括一些隱晦之處。但是寫入的時候,則可以實現一個小型的、可工作的詞匯表子集。
但是多數對象模型 API 仍然承擔了雙重職責,除了讀以外還要能將對象樹寫入磁盤。如果使用事件 API,就可以從數據結構生成寫事件