天天看點

在.NET Framework中輕松處理XML資料(一)

本文假設你已熟悉XML和.NET Framework

前言  

在.NET Framework中,XmlTextReader和XmlTextWriter類提供了對xml資料的讀和寫操作。在本文中,作者講述了XML閱讀器(Reader)的體系結構及它們怎樣與XMLDOM 和SAX 解釋器結合。作者也示範了怎麼樣運用閱讀器分析和驗證XML文檔,怎麼樣建立格式良好的XML文檔,以及怎麼樣用函數讀/寫基于Base64和BinHex編碼的大型的XML文檔。最後,作者講了怎麼樣實作一個基于流的讀/寫分析器,它把讀寫器都封裝在一個單獨的類裡。

概三年前,我參加了一個軟體研讨會,主題是“沒有XML,就沒有程式設計的未來”。XML确實也在一步一步的發展,它已經嵌入到. NET Framework中了。在本文中,我将講解. NET Framework中用于處理XML文檔的API的角色和它的内部特性,然後我将示範一些常用的功能。

從MSXML 到.net 的XML

       在. NET Framework出現之前,你習慣使用MSXML服務----一個基于COM的類庫---寫windows的XML的驅動程式。不像. NET Framework中的類,MSXML類庫的部分代碼比API更深,它完全的嵌在作業系統的底層。MSXML的确能夠與你的應用程式通信,但是它不能真正的與外部環境結合。

MSXML類庫能在win32中被導入,也能在CLR中運用,但它隻能作為一個外部伺服器元件使用。但是基于.NET Framework的應用程式能直接的用XML類與.NET Framework 的其它命名空間整合使用,并且寫出來的代碼易于閱讀。

作為一個獨立的元件,MSXML分析器提供了一些進階的特性如異步分析。這個特性在.NET Framework中的XML類及.NET Framework的其它類都沒有提供,但是,NET Framework中的XML類與其它的類整合可以很輕易的獲得相同的功能,在這個基礎上你可以增加更多的功能。

.NET Framework中的XML類提供了基本的分析、查詢、轉換XML資料的功能。在.NET Framework中,你可以找到支援Xpath查詢和XSLT轉換的類,及讀/寫XML文檔的類。另外,.NET Framework也包含了其它處理XML的類,例如對象的序列化(XmlSerializer和the SoapFormatter類),應用程式配置(AppSettingsReader類),資料存儲(DataSet類)。在本文中,我隻讨論實作基本XML I/O操作的類。

XML 分析模式

既然XML是一種标記語言,就應該有一種工具按一定的文法來分析和了解存儲在文檔中資訊。這個工具就是XML分析器---一個元件用于讀标記文本并傳回指定平台的對象。

所有的XML分析器,不管它屬于哪個操作平台,不外乎都分以下的兩類:基于樹或者基于事件的處理器。這兩類通常都是用XMLDOM(the Microsoft XML Document Object Model)和SAX(Simple API for XML)來實作。XMLDOM分析器是一個普通的基于樹的API---它把XML文檔當成一個記憶體結構樹呈現。SAX分析器是基于事件的API----它處理每個在XML資料流中的元素(它把XML資料放進流中再進行處理)。通常,DOM能被一個SAX流載入并執行,是以,這兩類的處理不是互相排斥的。

總的來說,SAX分析器與XMLDOM分析器正好相反,它們的分析模式存在着極大的差别。XMLDOM被很好的定義在它的functionalition集合裡面,你不能擴充它。當它在處理一個大型的文檔時,它要占用很大記憶體空間來處理functionalition這個巨大的集合。

SAX分析器利用用戶端應用程式通過現存的指定平台的對象的執行個體去處理分析事件。SAX分析器控制整個處理過程,把資料“推出”到處理程式,該處理程式依次接受或拒絕處理資料。這種模式的優點是隻需很少的記憶體空間。

.NET Framework完全支援XMLDOM模式,但它不支援SAX模式。為什麼呢?因為.NET Framework支援兩種不同的分析模式:XMLDOM分析器和XML閱讀器。它顯然不支援SAX分析器,但這并不意味它沒有提供類似SAX分析器的功能。通過XML閱讀器SAX的所有的功能都能很容易的實作及更有效的運用。不像SAX分析器,.NET Framework的閱讀器整個都運作在用戶端應用程式下面。這樣,應用程式本身就可以隻把真正需要的資料“推出”,然後從XML資料流中跳出來。而SAX分析模式要處理所有的對應用程式有用和無用的資訊。

閱讀器是基于.NET Framework流模式工作的,它的工作方式類似于資料庫的遊标。有趣的是,實作類似遊标分析模式的類提供對.NET Framework中的XMLDOM分析器的底層支援。XmlReader、XmlWriter兩個抽象類是所有.NET Framework中XML類的基礎類,包括XMLDOM類、ADO.NET驅動類及配置類。是以在.NET Framework中你有兩種可選的方法去處理XML資料。用XmlReader和XmlWriter類直接處理XML資料,或者用XMLDOM模式處理。更多的關于在.NET Framework中讀文檔的介紹可以參見MSDN 2002 年八月刊的Cutting Edge欄目文章。

XmlReader

       XML閱讀器支援一個程式設計接口,接口用于連接配接XML文檔,“推出”你要的資料。如果你更深入去了解閱讀器,你會發現閱讀器工作原理類似于我們的桌面應用程式從資料庫中取出資料的原理。資料庫服務傳回一個遊标對象,它包含所有查詢結果集,并傳回指向目标資料集的開始位址的引用。XML閱讀器的用戶端收到一個指向閱讀器執行個體的引用。該執行個體提取底層的資料流并把取出的資料呈現為一棵XML樹。閱讀器類提供隻讀、向前的遊标,你可以用閱讀器類提供的方法滾動遊标周遊結果集中的每一條資料。

       從閱讀器中看XML文檔不是一個标簽文本檔案,而是一個序列化的節點集合。它是.NET Framework中的一種特殊的遊标模式;在.NET Framework中,你找不到其它的任何一個類似的API函數。

       閱讀器和XMLDOM分析器有幾點不同的地方。XML閱讀器是隻進的,它沒有父、子、祖宗、兄弟節點的概念,而且是隻讀的。在.NET Framework中,讀寫XML文檔是分為兩種完全不同的功能,分别由XmlReader和XmlWriter類來完成。要編輯XML文檔,你可以用XMLDOM分析器,或者你自己設計一個類來實作這兩種功能。讓我們開始分析閱讀器的程式功能。

       XmlReader是一個抽象類,你可以繼承并擴充它的功能。使用者程式一般都基于下面的三種類:XmlTextReader、XmlValidatingReader或者 XmlNodeReader類。所有的這些類都有如圖一的屬性和圖二的方法。要注意的是,某些屬性的值實際上依賴于實際的某個閱讀器類,不同的類與基類可能不同。是以,在圖一中每個屬性的說明都是以基類為準的。例如,CanResolveEntity屬性在XmlValidatingReader類中隻傳回true;而在其它的閱讀器類中它卻可以設為false。同樣的,在圖二中的某些方法的實際傳回值對不同的類可能不同。例如,如果節點類型不是元素節點(element node),所有包含Atrributes的方法的傳回值類型都是void。

       XmlTextReader類用隻進,隻讀的方式快速通路XML資料流。閱讀器先驗證XML文檔是否是格式良好的,如果不是則抛出一個異常。XmlTextReader 檢查 DTD 的格式是否良好,但不使用 DTD 對文檔進行驗證。XmlTextReader通過XML文檔的檔案名,或它的URL,或者從檔案流中載入XML文檔,然後快速的處理XML文檔資料。如果你需要對文檔的資料進行驗證,你可以用XmlValidatingReader類。

       可以用多種方法建立XmlTextReader類的執行個體,從硬碟中加載檔案,或從URL位址中加載,流(streams)中加載,還有就是從文本中讀入XML文檔資料:

XmlTextReader reader = new XmlTextReader(file);

      注意,所有XmlTextReader類的公共(public)構造函數都要求你指定資料源,資料源可以是stream、檔案或者其它。XmlTextReader預設的構造函數是受保護的(protected),是以不能直接使用。像.NET Framework中所有的閱讀器類一樣(如SqlDataReader類),一旦閱讀器對象連接配接并打開,你就可以用Read方法去通路資料了。開始的時候隻能用Read方法把指針移到第一個元素;然後我們可以用Read方法或其它方法(如Skip, MoveToContent和ReadInnerXml)移動指針到下一個節點元素。要處理整個XML文檔的内容,可以根據Read方法的傳回值用一個循環周遊文檔内容,因為Read方法傳回一個布爾值,當讀到文檔的尾節點時,Read方法傳回false,否則它傳回true。

Figure 3 Outputting an XML Document Node Layout
string GetXmlFileNodeLayout(string file)
          
{
          
    // 建立一個XmlTextReader類使它指向目标XML文檔
          
    XmlTextReader reader = new XmlTextReader(file);
          
// 循環取出節點的文本并放入到StringWriter對象執行個體中
          
StringWriter writer = new StringWriter();
          
    string tabPrefix = "";
          
    while (reader.Read())
          
    {
          
        // 寫開始标志,如果節點類型為元素
          
        if (reader.NodeType == XmlNodeType.Element)
          
        {
          
            //根據元素所處節點的深度,加入reader.Depth個tab符,然後把元素名寫入到<>中。
          
                  tabPrefix = new string('/t', reader.Depth);
          
            writer.WriteLine("{0}<{1}>", tabPrefix, reader.Name);
          
        }
          
        else
          
        {
          
            //寫結束标志,如果節點類型為元素
          
            if (reader.NodeType == XmlNodeType.EndElement)
          
            {
          
                tabPrefix = new string('/t', reader.Depth);
          
                writer.WriteLine("{0}</{1}>", tabPrefix, reader.Name);
          
            }
          
        }
          
    }
          
    // 輸出到螢幕
          
    string buf = writer.ToString();
          
    writer.Close();
          
    // 關閉流
          
    reader.Close();
          
   return buf;
          
}      

      圖三示範了一個簡單的用于輸出一個給定的XML文檔的節點元素的函數。該函數先打開一個XML文檔,然後用循環處理XML文檔中所有的内容。每次調用Read方法,閱讀器的指針都會向下移一個節點。大部分情況下,用Read方法可以處理的元素節點,但有時候,當你從一個節點移動到下一個節點時,可能是在兩個不同類型的節點間移動。但是Read方法不能在屬性節點之間移動。閱讀器的MoveToContent方法可以讓指針從頭部節點位置跳到第一個内容節點位置。在ProcessingInstruction, DocumentType, Comment, Whitespace和SignificantWhitespace類型節點中也可以用Skip方法移動指針。

      每個節點的類型是XmlNodeType枚舉值中的一種,在如圖三所示的代碼中,我們隻用了其中的兩種類型:Element 和 EndElement。輸出源碼重新定制了原始的文檔結構,它丢棄或者說是忽略了XML元素的屬性和節點内容,隻輸出了元素節點名。假設我們運用了下面的XML片斷:

<mags>
         
   <mag name="MSDN Magazine">
         
   MSDN Magazine
         
   </mag>
         
   <mag name="MSDN Voices">
         
   MSDN Voices
         
   </mag>
         
</mags>
         

用上面的程式輸出的結果如下:

<mags>
         
   <mag>
         
   </mag>
         
   <mag>
         
   </mag>
         
</mags>
         
子節點的縮進量是根據閱讀器的深度屬性(Depth屬性)設定的,Depth屬性傳回一個整形的資料,它表示目前節點的嵌套層次。所有文本都放在StringWriter對象中(一個非常友善的基于流的封裝了StrigBuilder類的類)。
          
如前所述,閱讀器不會自動通過Read方法通路屬性節點。要通路目前元素的屬性節點集合,必須用一個簡單的用MoveToNextAttribute方法的傳回值控制的循環去周遊該集合。下面的代碼用于通路目前節點的所有屬性,并把屬性的名稱和它的值用逗号分開組合成一個字元串:
          
if (reader.HasAttributes)      
while(reader.MoveToNextAttribute())      
   buf += reader.Name + "=/"" + reader.Value + "/",";      
reader.MoveToElement();      

當你完成對屬性集的處理時,調用MoveToElement方法使指針傳回到屬性所屬的元素節點。準确的說,MoveToElement方法并不是真正的移動指針,因為在處理屬性集時指針從來就沒有從元素節點中移開。MoveToElement方法隻不過指向某個内部成員,并依次取得成員的值。例如,用Name屬性獲得某個屬性的屬性名,然後調用MoveToElement方法把指針移到其所屬的元素節點處。但是當你不需要繼續處理别的節點時,就不必再調用MoveToElement方法了。