在Java中使用DOM和XPath進(jìn)行有效的XML處理
-
文檔對象模型(Document Object Model,DOM)是公認(rèn)的 W3C 標(biāo)準(zhǔn),它被用于與平臺及語言無關(guān)的 XML
文檔內(nèi)容、結(jié)構(gòu)和樣式的動(dòng)態(tài)訪問和更新。它為表示文檔定義了一套標(biāo)準(zhǔn)的接口集,也為訪問和操縱文檔定義了一套標(biāo)準(zhǔn)的方法。DOM
得到廣泛的支持和普及,并且它以各種不同的語言實(shí)現(xiàn),包括 Java、Perl、C、C++、VB、Tcl 和 Python.
-
正如我將在本文所演示的,當(dāng)基于流的模型(例如 SAX)不能滿足 XML 處理要求時(shí),DOM
是一個(gè)極佳的選擇。不幸的是,規(guī)范的幾個(gè)方面,例如其語言無關(guān)性接口和“一切都是節(jié)點(diǎn)(everything-is-a-node)”抽象概念的使用,使
其難以使用且易于生成脆弱代碼。這在最近的幾個(gè)大型 DOM
項(xiàng)目的研究中尤其明顯,這些項(xiàng)目是由許多開發(fā)人員過去一年所創(chuàng)建的。下面討論了常見的問題及其補(bǔ)救措施。 -
文檔對象模型
-
DOM 規(guī)范被設(shè)計(jì)成可與任何編程語言一起使用。因此,它嘗試使用在所有語言中都可用的一組通用的、核心的功能部件。DOM
規(guī)范同樣嘗試保持其接口定義方面的無關(guān)性。這就允許 Java 程序員在使用 Visual Basic 或 Perl 時(shí)應(yīng)用他們的 DOM
知識,反之亦然。 -
該規(guī)范同樣將文檔的每個(gè)部分看成由類型和值組成的節(jié)點(diǎn)。這為處理文檔的所有方面提供了完美的概念性框架。例如,下面的 XML 片段
-
the Italicized portion.
-
就是通過以下的 DOM 結(jié)構(gòu)表示的:
-
圖 1:XML 文檔的 DOM 表示
-
樹的每個(gè)Document 、 Element 、 Text 和 Attr 部分都是 DOM Node 。
-
完美的抽象確實(shí)付出了代價(jià)。考慮 XML 片段: Value 。您或許會認(rèn)為文本的值可以通過普通的 Java String
對象來表示,并且通過簡單的 getValue 調(diào)用可訪問。實(shí)際上,文本被當(dāng)成 tagname 節(jié)點(diǎn)下的一個(gè)或多個(gè)子 Node
。因此,為了獲取文本值,您需要遍歷 tagname 的子節(jié)點(diǎn),將每個(gè)值整理成一個(gè)字符串。這樣做有充分的理由: tagname
可能包含其它嵌入的 XML 元素,在這種情況下獲取其文本值沒有多大意義。然而,在現(xiàn)實(shí)世界中,我們看到由于缺乏便利的函數(shù)導(dǎo)致頻繁的編碼錯(cuò)誤占了
80% 的情況,這樣做的確有意義。 -
設(shè)計(jì)問題
-
DOM 語言無關(guān)性的缺點(diǎn)是通常在每個(gè)編程語言中使用的一整套工作方法和模式不能被使用。例如,不能使用熟悉的 Java new
構(gòu)造創(chuàng)建新的 Element ,開發(fā)者必須使用工廠構(gòu)造器方法。 Node 的集合被表示成 NodeList ,而不是通常的 List 或
Iterator 對象。這些微小的不便意味著不同尋常的編碼實(shí)踐和增多的代碼行,并且它們迫使程序員學(xué)習(xí) DOM 的行事方法而不是用直覺的方法。 -
DOM 使用“一切都是節(jié)點(diǎn)”的抽象。這就意味著幾乎 XML 文檔的每個(gè)部分,例如: Document 、 Element 和
Attr ,全都繼承( extend ) Node 接口。這不僅是概念上完美,而且還允許每個(gè) DOM
的不同實(shí)現(xiàn)通過標(biāo)準(zhǔn)接口使其自身的類可見,并且沒有通過中間包裝類所導(dǎo)致的性能損失。 -
由于存在的節(jié)點(diǎn)類型數(shù)量及其訪問方法缺乏一致性,“一切都是節(jié)點(diǎn)”的抽象喪失了一些意義。例如, insertData 方法被用來設(shè)置
CharacterData 節(jié)點(diǎn)的值,而通過使用 setValue 方法來設(shè)置 Attr
(屬性)節(jié)點(diǎn)的值。由于對于不同的節(jié)點(diǎn)存在不同的接口,模型的一致性和完美性降低了,而學(xué)習(xí)曲線增加了。 -
JDOM
-
JDOM 是使 DOM API 適應(yīng) Java 的研究計(jì)劃,從而提供了更自然和易用的接口。由于認(rèn)識到語言無關(guān) DOM 構(gòu)造的棘手本質(zhì),JDOM 目標(biāo)在于使用內(nèi)嵌的 Java 表示和對象,并且為常用任務(wù)提供便利的函數(shù)。
-
例如,JDOM 直接處理“一切都是節(jié)點(diǎn)”和 DOM 特定構(gòu)造的使用(如 NodeList )。JDOM 將不同的節(jié)點(diǎn)類型(如
Document 、 Element 和 Attribute )定義為不同的 Java 類,這意味著開發(fā)者可以使用 new
構(gòu)造它們,避免頻繁類型轉(zhuǎn)換的需要。JDOM 將字符串表示成 Java String ,并且通過普通的 List 和 Iterator
類來表示節(jié)點(diǎn)的集合。(JDOM 用其本身類替代 DOM 類。) -
JDOM 為提供更完善的接口做了相當(dāng)有益的工作。它已經(jīng)被接受成為 JSR(正式的 Java Specification
Request),而且它將來很可能會被包含到核心的 Java 平臺中。但是,因其還不是核心 Java API
的一部分,一些人對于使用它還心存猶豫。這兒還有關(guān)于與 Iterator 和 Java 對象頻繁創(chuàng)建相關(guān)的性能問題的報(bào)告。(請參閱 參考資料)。 -
如果您對 JDOM 的接受性和可用性已經(jīng)滿足,并且如果您也沒有將 Java 代碼和程序員轉(zhuǎn)移到其它語言的直接需求,JDOM 是個(gè)值得探索的好選擇。JDOM 還不能滿足本文探討的項(xiàng)目所在的公司需要,因而他們使用了非常普遍的 DOM。本文也是這樣做的。
-
常見編碼問題
-
幾個(gè)大型 XML 項(xiàng)目分析揭示了使用 DOM 中的一些常見問題。下面對其中的幾個(gè)進(jìn)行介紹。
-
代碼臃腫
-
在我們研究中查看的所有項(xiàng)目,本身都出現(xiàn)一個(gè)突出的問題:花費(fèi)許多行代碼行來做簡單的事情。在某個(gè)示例中,使用 16
行代碼檢查一個(gè)屬性的值。而同樣的任務(wù),帶有改進(jìn)的健壯性和出錯(cuò)處理,可以使用 3 行代碼實(shí)現(xiàn)。DOM API
的低級本質(zhì)、方法和編程模式的不正確應(yīng)用以及缺乏完整 API 的知識,都會致使代碼行數(shù)增加。下面的摘要介紹了關(guān)于這些問題的特定實(shí)例。 -
遍歷 DOM
-
在我們探討的代碼中,最常見的任務(wù)是遍歷或搜索 DOM。 清單 1 演示了需要在文檔的 config 節(jié)里查找一個(gè)稱為“header”節(jié)點(diǎn)的濃縮版本代碼:
-
清單 1 中,從根開始通過檢索頂端元素遍歷文檔,獲取其第一個(gè)子節(jié)點(diǎn)( configNode ),并且最終單獨(dú)檢查 configNode 的子節(jié)點(diǎn)。不幸的是,這種方法不僅冗長,而且還伴隨著脆弱性和潛在的錯(cuò)誤。
-
例如,第二行代碼通過使用 getFirstChild 方法獲取中間的 config
節(jié)點(diǎn)。已經(jīng)存在許多潛在的問題。根節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)實(shí)際上可能并不是用戶正在搜索的節(jié)點(diǎn)。由于盲目地跟隨第一個(gè)子節(jié)點(diǎn),我忽視了標(biāo)記的實(shí)際名稱并且可能
搜索不正確的文檔部分。當(dāng)源 XML 文檔的根節(jié)點(diǎn)后包含空格或回車時(shí),這種情況中發(fā)生一個(gè)頻繁的錯(cuò)誤;根節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)實(shí)際是
Node.TEXT_NODE 節(jié)點(diǎn),而不是所希望的元素節(jié)點(diǎn)。您可以自己試驗(yàn)一下,從 參考資料下載樣本代碼并且編輯 sample.xml 文件
― 在 sample 和 config 標(biāo)記之間放置一個(gè)回車。代碼立即異常而終止。要正確瀏覽所希望的節(jié)點(diǎn),需要檢查每個(gè) root
的子節(jié)點(diǎn),直到找到非 Text 的節(jié)點(diǎn),并且那個(gè)節(jié)點(diǎn)有我正在查找的名稱為止。 -
清單 1 還忽視了文檔結(jié)構(gòu)可能與我們期望有所不同的可能性。例如,如果 root 沒有任何子節(jié)點(diǎn), configNode 將會被設(shè)置為
null
,并且示例的第三行將產(chǎn)生一個(gè)錯(cuò)誤。因此,要正確瀏覽文檔,不僅要單獨(dú)檢查每個(gè)子節(jié)點(diǎn)以及核對相應(yīng)的名稱,而且每步都得檢查以確保每個(gè)方法調(diào)用返回的是一
個(gè)有效值。編寫能夠處理任意輸入的健壯、無錯(cuò)的代碼,不僅需要非常關(guān)注細(xì)節(jié),而且需要編寫很多行代碼。 -
最終,如果最初的開發(fā)者了解它的話,清單 1 中示例的所有功能應(yīng)該可以通過利用對 getElementsByTagName 函數(shù)的簡單調(diào)用實(shí)現(xiàn)。這是下面要討論的。
-
檢索元素中的文本值
-
在所分析的項(xiàng)目中,DOM 遍歷以后,第二項(xiàng)最常進(jìn)行的任務(wù)是檢索在元素中包含的文本值。考慮 XML 片段 The Value 。如果已經(jīng)導(dǎo)航到 sometag 節(jié)點(diǎn),如何獲取其文本值( The Value )呢?一個(gè)直觀的實(shí)現(xiàn)可能是:
-
sometagElement.getData();
-
正如您所猜測到的,上面的代碼并不會執(zhí)行我們希望的動(dòng)作。由于實(shí)際的文本被存儲為一個(gè)或多個(gè)子節(jié)點(diǎn),因此不能對 sometag 元素調(diào)用 getData 或類似的函數(shù)。更好的方法可能是:
-
sometag.getFirstChild().getData();
-
第二種嘗試的問題在于值實(shí)際上可能并不包含在第一個(gè)子節(jié)點(diǎn)中;在 sometag
內(nèi)可能會發(fā)現(xiàn)處理指令或其它嵌入的節(jié)點(diǎn),或是文本值包含在幾個(gè)子節(jié)點(diǎn)而不是單單一個(gè)子節(jié)點(diǎn)中。考慮到空格經(jīng)常作為文本節(jié)點(diǎn)表示,因此對
sometag.getFirstChild() 的調(diào)用可能僅讓您得到標(biāo)記和值之間的回車。實(shí)際上,您需要遍歷所有子節(jié)點(diǎn),以核對
Node.TEXT_NODE 類型的節(jié)點(diǎn),并且整理它們的值直到有完整的值為止。 -
注意,JDOM 已經(jīng)利用便利的函數(shù) getText 為我們解決了這個(gè)問題。DOM 級別 3 也將有一個(gè)使用規(guī)劃的 getTextContent 方法的解答。教訓(xùn):盡可能使用較高級的 API 是不會錯(cuò)的。
-
getElementsByTagName
-
DOM 級別 2 接口包含一個(gè)查找給定名稱的子節(jié)點(diǎn)的方法。例如,調(diào)用:
-
NodeList names = someElement.getElementsByTagName("name");
-
將返回一個(gè)包含在 someElement 節(jié)點(diǎn)中稱為 names 的節(jié)點(diǎn) NodeList 。這無疑比我所討論的遍歷方法更方便。這也是一組常見錯(cuò)誤的原因。
-
問題在于 getElementsByTagName
遞歸地遍歷文檔,從而返回所有匹配的節(jié)點(diǎn)。假定您有一個(gè)包含客戶信息、公司信息和產(chǎn)品信息的文檔。所有這三個(gè)項(xiàng)中都可能含有 name 標(biāo)記。如果調(diào)用
getElementsByTagName
搜索客戶名稱,您的程序極有可能行為失常,除了檢索出客戶名稱,還會檢索出產(chǎn)品和公司名稱。在文檔的子樹上調(diào)用該函數(shù)可能會降低風(fēng)險(xiǎn),但由于 XML
的靈活本質(zhì),使確保您所操作的子樹包含您期望的結(jié)構(gòu),且沒有您正在搜索的名稱的虛假子節(jié)點(diǎn)就變得十分困難。 -
DOM 的有效使用
-
考慮到由 DOM 設(shè)計(jì)強(qiáng)加的限制,如何才能有效和高效的使用該規(guī)范呢?下面是使用 DOM 的幾條基本原則和方針,以及使工作更方便的函數(shù)庫。
-
基本原則
-
如果您遵循幾條基本原則,您使用 DOM 的經(jīng)驗(yàn)將會顯著提高:
-
◆ 不要使用 DOM 遍歷文檔。
-
◆ 盡可能使用 XPath 來查找節(jié)點(diǎn)或遍歷文檔。
-
◆ 使用較高級的函數(shù)庫來更方便地使用 DOM。
-
這些原則直接從對常見問題的研究中得到。正如上面所討論的,DOM 遍歷是出錯(cuò)的主要原因。但它也是最常需要的功能之一。如何通過不使用 DOM 而遍歷文檔呢?
-
Path
-
XPath 是尋址、搜索和匹配文檔的各個(gè)部分的語言。它是 W3C 推薦標(biāo)準(zhǔn)(Recommendation),并且在大多數(shù)語言和
XML 包中實(shí)現(xiàn)。您的 DOM 包可能直接支持 XPath 或通過加載件(add-on)支持。本文的樣本代碼對于 XPath 支持使用
Xalan 包。 -
XPath 使用路徑標(biāo)記法來指定和匹配文檔的各個(gè)部分,該標(biāo)記法與文件系統(tǒng)和 URL 中使用的類似。例如,XPath: /x/y/z 搜索文檔的根節(jié)點(diǎn) x ,其下存在節(jié)點(diǎn) y ,其下存在節(jié)點(diǎn) z 。該語句返回與指定路徑結(jié)構(gòu)匹配的所有節(jié)點(diǎn)。
-
更為復(fù)雜的匹配可能同時(shí)在包含文檔的結(jié)構(gòu)方面以及在節(jié)點(diǎn)及其屬性的值中。語句 /x/y/* 返回父節(jié)點(diǎn)為 x 的 y 節(jié)點(diǎn)下的任何節(jié)點(diǎn)。
/x/y[@name=''a''] 匹配所有父節(jié)點(diǎn)為 x 的 y 節(jié)點(diǎn),其屬性稱為 name ,屬性值為 a 。請注意,XPath
處理篩選空格文本節(jié)點(diǎn)以獲得實(shí)際的元素節(jié)點(diǎn) ― 它只返回元素節(jié)點(diǎn)。 -
詳細(xì)探討 XPath 及其用法超出了本文的范圍。請參閱 參考資料獲得一些優(yōu)秀教程的鏈接。花點(diǎn)時(shí)間學(xué)習(xí) XPath,您將會更方便的處理 XML 文檔。
-
函數(shù)庫
-
當(dāng)研究 DOM
項(xiàng)目時(shí)令我們驚奇的一個(gè)發(fā)現(xiàn),是存在的拷貝和粘貼代碼的數(shù)量。為什么有經(jīng)驗(yàn)的開發(fā)者沒有使用良好的編程習(xí)慣,卻使用拷貝和粘貼方法而不是創(chuàng)建助手
(helper)庫呢?我們相信這是由于 DOM
的復(fù)雜性加深了學(xué)習(xí)的難度,并使開發(fā)者要理解能完成他們所需要的第一段代碼。開發(fā)產(chǎn)生構(gòu)成助手庫規(guī)范的函數(shù)所需的專門技術(shù)需要花費(fèi)大量的時(shí)間。 -
要節(jié)省一些走彎路的時(shí)間,這里是一些將使您自己的庫可以運(yùn)轉(zhuǎn)起來的基本助手函數(shù)。
-
findValue
-
使用 XML
文檔時(shí),最常執(zhí)行的操作是查找給定節(jié)點(diǎn)的值。正如上所討論的,在遍歷文檔以查找期望的值和檢索節(jié)點(diǎn)的值中都出現(xiàn)難度。可以通過使用 XPath
來簡化遍歷,而值的檢索可以一次編碼然后重用。在兩個(gè)較低級函數(shù)的幫助下,我們實(shí)現(xiàn)了 getValue 函數(shù),這兩個(gè)低級函數(shù)是:由 Xalan
包提供的 XPathAPI.selectSingleNode (用來查找和返回與給定的 XPath 表達(dá)式匹配的第一個(gè)節(jié)點(diǎn));以及
getTextContents ,它非遞歸地返回包含在節(jié)點(diǎn)中的連續(xù)文本值。請注意,JDOM 的 getText 函數(shù),或?qū)⒊霈F(xiàn)在 DOM 級別
3 中規(guī)劃的 getTextContent 方法,都可用來代替 getTextContents 。 清單
2包含了一個(gè)簡化的清單;您可以通過下載樣本代碼來訪問所有函數(shù)(請參閱 參考資料)。 -
通過同時(shí)傳入要開始搜索的節(jié)點(diǎn)和指定要搜索節(jié)點(diǎn)的 XPath 語句來調(diào)用 findValue 。函數(shù)查找第一個(gè)與給定 XPath 匹配的節(jié)點(diǎn),并且抽取其文本值。
-
setValue
-
另一項(xiàng)常用的操作是將節(jié)點(diǎn)的值設(shè)置為希望的值,如 清單 3 所示。該函數(shù)獲取一個(gè)起始節(jié)點(diǎn)和一條 XPath 語句 ― 就象
findValue ―
以及一個(gè)用來設(shè)置匹配的節(jié)點(diǎn)值的字符串。它查找希望的節(jié)點(diǎn),除去其所有子節(jié)點(diǎn)(因此除去包含在其中的任何文本和其它元素),并將其文本內(nèi)容設(shè)置為傳入的
(passed-in)字符串。 -
appendNode
-
雖然某些程序查找和修改包含在 XML 文檔中的值,而另一些則通過添加和除去節(jié)點(diǎn)來修改文檔本身的結(jié)構(gòu)。這個(gè)助手函數(shù)簡化了文檔節(jié)點(diǎn)的添加,如 清單 4所示。
-
該函數(shù)的參數(shù)有:要將新節(jié)點(diǎn)添加到其下的節(jié)點(diǎn),要添加的新節(jié)點(diǎn)名稱,以及指定要將節(jié)點(diǎn)添加到其下位置的 XPath 語句(也就是,新節(jié)點(diǎn)的父節(jié)點(diǎn)應(yīng)當(dāng)是哪個(gè))。新節(jié)點(diǎn)被添加到文檔的指定位置。
-
最終分析
-
DOM 的語言無關(guān)性設(shè)計(jì)為其帶來了非常廣泛的可應(yīng)用性并使其在大量的系統(tǒng)和平臺上得以實(shí)現(xiàn)。這樣做的代價(jià)是:使 DOM 比為每個(gè)語言專門設(shè)計(jì)的 API 更困難且更缺乏直觀性。
-
DOM 奠定了一個(gè)非常有效的基礎(chǔ),遵循一些簡單的原則就可其上構(gòu)建易于使用的系統(tǒng)。凝結(jié)了一大群用戶智慧和經(jīng)驗(yàn)的 DOM
未來版本正在設(shè)計(jì)之中,而且極有可能為這里討論的問題提供解決方案。如 JDOM 這樣的項(xiàng)目正在修改該 API 以獲得更自然 Java
感覺,而且如本文中所述的技術(shù)可以幫助您使 XML 的操縱更方便、更簡潔并且不易出錯(cuò)。利用這些項(xiàng)目且遵循這些用法模式以允許 DOM 成為基于
XML 項(xiàng)目的出色平臺。
posted on 2009-04-28 17:27 liyang 閱讀(785) 評論(0) 編輯 收藏 所屬分類: xml