真實世界的 XML:使用 .NET 框架中集成的讀取器和寫入器很容易操作 XML 數據

 

Dino Esposito

本文假定您熟悉 XML 和 .NET 框架

下載本文的代碼:Real-WorldXML.exe(120KB)

 

*

本頁內容
從 MSXML 到 .NET 中的 XML 從 MSXML 到 .NET 中的 XML
XML 分析模型 XML 分析模型
XmlReader 類 XmlReader 類
分析屬性內容 分析屬性內容
對 XML 文本進行操作 對 XML 文本進行操作
字符串和片段 字符串和片段
驗證讀取器 驗證讀取器
節點讀取器 節點讀取器
XmlTextWriter 類 XmlTextWriter 類
讀取和寫入流 讀取和寫入流
設計 XmlReadWriter 類 設計 XmlReadWriter 類
小結 小結

大約三年前,我在參加一個軟件會議後相信,如果不深入理解 XML,未來就不可能參加編程工作。從初期到現在,XML 的確已經走過了很長的路程,甚至涉及到公用編程框架的最深處。在本文中,我將回顧用於處理 XML 文檔的 Microsoft .NET 框架 API 的角色和內部特徵,然後繼續討論幾個尚未解決的問題。

從 MSXML 到 .NET 中的 XML

在 .NET 框架出現之前,編寫 XML 驅動的 Windows 應用程序的習慣方式是利用 MSXML 的服務 - 基於 COM 的庫。MSXML 庫與 .NET 框架中的類不同,MSXML 庫更像是死板的 API,而不是與基本操作系統完全集成的代碼。MSXML 肯定可以與應用程序的其餘部分通信,但它沒有真正與周圍的環境集成。

可以在 Win32 甚至在用於公共語言運行庫 (CLR) 的代碼中導入 MSXML 庫,但它保留了一個充當服務器組件的外部黑匣。另一方面,基於 .NET 框架的應用程序可以將 XML 核心類與 .NET 框架的其他命名空間直接放在一起使用,以便最終的代碼良好集成在一起,並且容易閱讀。

作爲獨立的組件,MSXML 分析器提供了高級的功能,例如異步分析。在 .NET 框架的 XML 類中,顯然缺少這個功能。但是,通過將 XML 類與 .NET 框架中其他類集成在一起,可以很容易獲得相同的功能,甚至獲得更大的進程控制能力。

XML 函數庫應當提供至少一組基本的服務,包括分析、查詢和轉換。在 .NET 框架中,可以找到支持 XPath 查詢和 XSLT 轉換的類,以及讀取和寫入 XML 文檔的類。此外,.NET 框架合併了執行與 XML 有關的任務的類,例如,這些任務包括對象序列化(XmlSerializer 和 SoapFormatter 類)、應用程序配置(AppSettingsReader 類)和數據持久性(DataSet 類)。在本文中,我將重點介紹完成基本 I/O 操作的類。

XML 分析模型

由於 XML 是標記語言,因此需要一個能夠分析和理解詞彙語法的工具來有效地使用存儲在文檔中的信息。該工具就是 XML 分析器 £- 它是一種讀取標記文本並返回特定於平臺的對象的黑匣組件。

不管基本平臺是什麼,程序員可用的所有 XML 分析器都屬於兩個主要類別中的某一個:基於樹的處理器,和基於事件的處理器。通常,使用這兩個類別的兩個最流行和最具體的實現來識別它們:Microsoft XML Document Object Model (XMLDOM) 和 Simple API for XML (SAX)。XMLDOM 分析器是一個通用的基於樹的 API,它可以將 XML 文檔呈現爲內存中結構。SAX 分析器提供了基於事件的 API,它可以處理在 XML 數據流中的每個重要元素。通常,可以從 SAX 流中加載 Document Object Model (DOM) 實現,所以,兩種類型的處理器不是相互排斥的。

從概念上講,SAX 分析器與 XMLDOM 分析器截然相反,兩個模型之間的差異確實相當大。XMLDOM 已在它的功能集中得到很完善的定義,該模型已經沒有太多可以期待的合理發展餘地。當然,這個大型功能集正在走下坡路 £- 爲了處理大型文檔,它需要大量佔用內存和帶寬。

SAX 分析器的工作原理是,讓客戶端應用程序傳遞活動的、特定於平臺的對象實例,以便處理分析器事件。分析器控制整個過程,並將數據傳遞給應用程序,而該應用程序可以自由地接受或只是忽略它。這個模型非常瘦,特點是佔用的內存非常有限。

.NET 框架爲 XMLDOM 分析模型提供了完整支持,但沒有爲 SAX 這樣做。這有一個好的理由。.NET 框架支持兩種不同模型的分析器:XMLDOM 分析器和 XML 讀取器。雖然明顯缺少對 SAX 分析器的支持,但這並不意味着您必須拒絕它們所提供的功能。通過使用 XML 讀取器,很容易實現 SAX 分析器的所有功能,並且這些功能更有效。不像 SAX 分析器,.NET 框架讀取器工作在客戶端應用程序的總體控制下。通過這種方式,應用程序本身就可以只獲取它確實需要的數據,並跳過 XML 流的其餘信息。使用 SAX,分析器可以將所有可用信息傳遞給客戶端應用程序,然後,後者必須使用或放棄該信息。

讀取器基於 .NET 框架流,其工作方式與數據庫遊標基本相同。有趣的是,實現這個類似遊標的分析模型的類,還爲 XMLDOM 分析器的 .NET 框架實現提供了基礎。兩個抽象類(XmlReader 和 XmlWriter)正是所有 .NET 框架 XML 類(包括 XML DOM 類、ADO.NET 相關類和配置類)的基礎。因此,當需要處理 XML 數據時,在 .NET 框架中有兩個可能的方法。可以使用直接建立在 XmlReader 和 XmlWriter 基礎上的類,也可以使用通過衆所周知的 XMLDOM 對象模型來公開信息的類。對 .NET 框架中文檔讀取器的更全面介紹包含在我撰寫的 2002 年 8 月的 Cutting Edge 專欄中。

XmlReader 類

XML 讀取器提供的編程接口可以讓調用方用來連接到 XML 文檔,並獲取它們需要的數據。如果更密切地觀察讀取器,您將意識到,它們在後臺的工作方式就好像是從數據庫獲取數據的應用程序。數據庫服務器返回遊標對象的引用,該對象包含所有查詢結果,並使這些結果可以根據需要可用。XML 讀取器的客戶端將接收對讀取器類的實例的引用,這個類會將基本數據流抽象化,並將它呈現爲 XML 樹。讀取器類的方法允許您在整個內容中向前翻滾,從一個節點移動到另一個節點,而不是從一個字節移動到另一個字節,或從一個記錄移動到另一個記錄。

從讀取器的觀察角度看,XML 文檔不是標記文本文件,而是節點的序列化集合。這樣的遊標模型是特定於 .NET 框架的;您在其他地方找不到可用的類似編程 API。

在讀取器和 XMLDOM 分析器之間,有幾個關鍵差異。XML 讀取器是隻進的,它們沒有周圍節點的概念(同代、父代、祖先、子代),並且它們只能讀取。在 .NET 框架中,讀取和寫入 XML 文檔是兩個完全分離的功能,它們需要不同的、無關的類:XmlReader 和 XmlWriter。要想能夠編輯 XML 文檔的內容,可以使用 XMLDOM 分析器(圍繞 XmlDocument 類生成),也可以設計自定義的類,用這個類將像讀取器和寫入器這樣兩個明顯不同的實體合併在一個公用邏輯框架下面。讓我們首先分析讀取器類的編程特性。

XmlReader 是一個抽象類,您可以用它來生成更具體的功能。用戶應用程序通常基於這三個派生類中的某一個:XmlTextReader、XmlValidatingReader 或 XmlNodeReader。所有這些類均共享一組公共屬性(參閱圖 1)和方法(參閱圖 2)。注意,有時,屬性實際包含的值取決於您正在代碼中使用的讀取器類。因此,對圖 1 所提供的每個屬性的描述指的是它的預期目標,即使這可能沒有反映該屬性在派生的讀取器類中的角色。例如,CanResolveEntity 只對 XmlValidatingReader 返回 True;對於其他任何類型的讀取器類,它被設置爲 False。同樣,圖 2 列出的某些方法的行爲和返回值受到讀取器所處理的節點類型的影響。例如,如果節點不是元素節點,則所有包含屬性的方法都是 void。

XmlTextReader 類被設計用於以只進、只讀方式快速訪問 XML 數據流。讀取器將驗證所提交的 XML 是否具有規範的格式,如果不是則產生異常。讀取器還會執行快速檢查,以確保所引用的文檔類型定義 (DTD)(如果有定義)是存在的。XmlTextReader 類不會根據架構或 DTD 對文檔內容進行驗證。如果要快速處理可通過文件名、URL 或打開的流來訪問的、規範格式的 XML 數據,則 XmlTextReader 分析器是理想的選擇。如果需要進行數據驗證,則應當使用 XmlValidatingReader 類。

可以用很多方法並從各種數據源(包括磁盤文件、URL、流和文本讀取器)創建 XmlTextReader 類的實例:

XmlTextReader reader = new XmlTextReader(file);

注意,所有可用的公共構造函數都需要您指明數據源:它是不是流、文件或其他。XmlTextReader 類的默認構造函數被標記爲受保護的,並且(因此)是不可直接使用的。與所有 .NET 框架讀取器類一樣,一旦讀取器對象建立並運行,則必須使用 Read 方法來訪問數據。只能使用 Read 方法,將讀取器從其初始狀態移動到第一個元素。要從任何節點移動到下一個節點,可以繼續使用 Read 以及其他很多更專用的方法(包括 Skip、MoveToContent 和 ReadInnerXml)。要處理 XML 源的全部內容,通常可以設置一個由 Read 方法的返回值控制的循環:當有數據可讀則爲 False,否則爲 True。

圖 3 展示一個簡單函數,它輸出給定 XML 文檔的節點佈局。該函數打開文檔並設置一個循環來遍歷所有內容。每次調用 Read 方法時,讀取器的內部指針都會向前移動一個節點。儘管您很可能使用 Read 方法來處理元素節點,但請考慮在通常情況下,當您從一個節點移動到下一個時,您不必在類型相同的節點之間移動。例如,方法不會通過屬性節點。讀取器的 MoveToContent 方法讓您跳過所有標題節點,並將指針直接放在第一個內容節點上。因此,方法會跳過類型爲 ProcessingInstruction、DocumentType、Comment、Whitespace 和 SignificantWhitespace 的節點。

每個節點都被賦予一個從 NodeType 枚舉中選取的類型。在圖 3 所示的代碼中,只有兩個類型的節點是相關的:Element 和 EndElement。代碼的輸出會複製原始文檔的結構,但會放棄屬性和文本,只保留節點佈局。讓我們假設我使用下面的 XML 片段:

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

最後的輸出如下所示:

<mags> 
   <mag>
   </mag>
   <mag>
   </mag>
</mags>

子節點的縮進可以通過使用讀取器的 Depth 屬性來獲得,該屬性返回整數值以表示當前節點的嵌套級別。所有文本都累積在 StringWriter 對象中 £- 該對象是用於 StringBuilder 類的包裝,它非常方便易用,而且是基於流的。

我在前面提到過,屬性節點不會被使用 Read 方法向前移動的讀取器自動訪問。若要訪問當前元素節點的屬性集合,必須使用類似的循環,但這個循環要受 MoveToNextAttribute 方法控制。下面的代碼將訪問當前節點(用 Read 選擇的節點)的所有屬性,並將它們的名稱和值連成以逗號分隔的字符串:

if (reader.HasAttributes)
while(reader.MoveToNextAttribute())
   buf += reader.Name + "=/"" + reader.Value + "/","; 
reader.MoveToElement();

一旦處理完節點屬性,請考慮調用讀取器對象的 MoveToElement 方法。MoveToElement 將把內部指針“移動”回包含屬性的元素節點。準確地說,該方法沒有真地“移動”指針,因爲在屬性的導航期間,指針從不離開元素節點。MoveToElement 方法只是刷新某些內部成員,使它們公開元素節點的值,而不是被讀取的上一屬性。例如,Name 屬性返回在調用 MoveToElement 之前被讀取的最後一個屬性的名稱,和後來讀取的父節點的名稱。一旦處理完屬性,如果對節點沒有進一步的操作,並且想要繼續處理下一個元素,那麼,您並不真的需要調用 MoveToElement。

分析屬性內容

大多數情況下,屬性的內容是一個簡單的文本字符串。但是,這並不意味着字符串是屬性值的實際類型。有時,屬性值由類型更具體(例如 Date 或 Boolean)的字符串表示法組成,您可以使用靜態類(XmlConvert 或 System.Convert)的方法將這種特殊的字符串轉換爲本機類型。這兩個類執行的任務幾乎完全相同,但 XmlConvert 類按照 XML 架構定義 (XSD) 數據類型規範工作,並且忽略當前區域設置。

假設您有一個像下面這樣的 XML 片段:

<person birthday="2-8-2001" />

讓我們再假設,按照當前區域設置,生日屬性是 February 8, 2001。如果使用 System.Convert 類將字符串轉換成具體的 .NET 框架類型(DateTime 類型),則所有事情將按期望進行,並且字符串將轉換成期望的日期對象。相反,如果使用 XmlConvert 來轉換字符串,您將得到一個分析錯誤,因爲 XmlConvert 類沒有在字符串中識別出正確的日期。原因是 XML 日期必須採用 YYYY-MM-DD 格式才能被理解。XmlConvert 類在 CLR 類型和 XSD 類型之間充當了轉換器。當轉換髮生時,結果將與區域設置無關。

某些情況下,屬性的值由純文本以及實體組成。在所有讀取器類之中,實際上只有 XmlValidatingReader 能夠解析實體。XmlTextReader 類儘管無法解析實體引用,但當文本和實體嵌入在屬性的值中時,這個類可以將二者分離開來。要做到這一點,必須使用 ReadAttributeValue 方法分析屬性的內容,而不只是通過 Value 屬性讀取它。

ReadAttributeValue 方法將分析屬性值,並將每個成分標記隔離開來,不管它是純文本或實體。在一個循環中重複調用 ReadAttributeValue,直到到達屬性字符串值的末尾爲止。由於 XmlTextReader 不解析實體,所以沒有其他辦法來處理嵌入的實體,只能編寫您自己的解析程序,或者也許只識別它然後將它跳過。下面的代碼片段說明了如何調用自定義解析程序:

while(reader.ReadAttributeValue())
{
   if (reader.NodeType == XmlNodeType.EntityReference)
// Resolve the "reader.Name" reference and add
// the result to a buffer
buf += YourResolverCode(reader.Name);
   else
    // Just append the value to the buffer
buf += reader.Value;
}

混合內容屬性的最後值由在全局緩衝區中累積文本來確定。當屬性值已完全分析時,ReadAttributeValue 方法將返回 False。

對 XML 文本進行操作

處理 XML 標記文本時,如果不正確處理,某些功能可以快速成爲問題。一個這樣的障礙點是字符轉換,有時執行該操作是必要的,以便在 XML 數據流中傳輸非 XML 文本。並非在給定平臺上可以找到的所有字符都必須是有效的 XML 字符。只有 XML 規範 (www.w 3.org/TR/2000/RECxml 20001006.html) 中的字符才能安全用作元素和屬性的名稱。

XmlConvert 類爲在服務器之間通過 XML 以隧道方式傳輸非 XML 名稱提供了關鍵的功能。當名稱包含無效 XML 字符時,方法 EncodeName 和 DecodeName 可以調整它們,使其符合 XML 名稱架構。幾個應用程序(包括 SQL Server?和 Microsoft Office)允許和支持在它們的文檔中使用 Unicode 字符。但是,某些這樣的字符不能充當有效的 XML 名稱。說明 XmlConvert 重要性的典型環境出現在當您操作包含空格的數據庫列名稱時。儘管 SQL Server 允許使用像“Invoice Details”這樣的列名稱,但對 XML 流來說這不是有效名稱。空格必須替換爲它的十六進制編碼,結果是“Invoice_0x0020_Details”。十六進制序列的唯一有效的格式是 _0xHHHH_,其中 HHHH 代表一個四位數的十六進制值。相似但不同的格式會保持不變,雖然可以將它們看作在邏輯上是等價的。下面演示如何通過編程獲得該字符串:

XmlConvert.EncodeName("Invoice Details");

相反的操作由 DecodeName 完成。該方法通過對任何轉義序列執行逆轉義操作,將 XML 名稱轉換回它的原始格式。注意,只檢測完整的轉義格式。例如,只有 _0x0020_ 將呈現爲空格,而 _0x 20_ 則不會被處理:

XmlConvert.DecodeName("Invoice_0x0020_Details");

在 XML 中,XML 文檔的正文內的空白可以是重要的,也可以是不重要的。如果空白出現在元素節點的文本中,或出現在空白聲明的作用範圍內,則說該空白是重要的:

<MyNode xml:space="preserve">
<!-- any space here must be preserved -->
???
</MyNode>

在 XML 中,術語空白 (whitespace) 不僅包括空格 (ASCII 0x20),而且包括回車符 (ASCII 0x0D),換行符 (ASCII 0x0A) 和製表符 (ASCII 0x09)。

XmlTextReader 類通過 WhiteSpaceHandling 屬性讓您控制如何處理空白。該屬性接受並返回從 WhiteSpaceHandling 枚舉取得的值,該枚舉列出了三個可行的選項。默認選項是 All,這意味着重要和不重要的空格都會作爲不同的節點返回 £- 分別是 SignificantWhitespace 和 Whitespace。選項 None 表示任何空白都不會作爲節點返回。最後,選項“Significant”表示放棄所有不重要的空白,並且只返回類型爲 SignificantWhitespace 的節點。注意,WhiteSpaceHandling 屬性是很少幾個可以隨時更改的讀取器屬性中的一個,更改將在下一次讀取操作時生效。其他“敏感的”屬性是 Normalization 和 XmlResolver。

字符串和片段

仔細研究了 MSXML 的程序員肯定注意到 COM 庫和 .NET 框架 XML API 之間的關鍵差異。.NET 框架類不提供分析存儲在字符串中的 XML 數據的本機方式。不像 MSXML 分析器對象,XmlTextReader 類不提供任何種類的 loadXML 方法來從格式規範的字符串生成讀取器。缺少 loadXML 風格的方法存在爭議。該方法是不需要的,因爲使用特殊的文本讀取器(StringReader 類),可以獲得相同的功能。

一個 XmlTextReader 構造函數接受 TextReader 派生的對象,並用文本讀取器的內容實例化 XML 讀取器。文本讀取器類是一個已經爲字符輸入進行過優化的流。StringReader 類繼承自 TextReader,並使用內存中字符串作爲輸入流。下面的代碼片段說明如何使用格式規範的 XML 字符串初始化 XML 讀取器:

string xmlText = "...";
StringReader strReader = new StringReader(xmlText);
XmlTextReader reader = new XmlTextReader(strReader);

同樣,通過使用 StringWriter 類代替 TextWriter 類,可以用內存字符串創建 XML 文檔。

XML 片段是特殊類型的 XML 字符串。片段是由沒有應用正確格式的 XML 文檔的根級別規則的 XML 文本組成的。結果,XML 片段與普通文檔不同,因爲它可以缺少唯一的根節點。例如,下面的 XML 字符串是有效的 XML 片段,但不是有效的 XML 文檔,因爲 XML 文檔必須有一個根節點:

<firstname>Dino</firstname>
<lastname>Esposito</lastname>

.NET 框架 XML API 允許程序員將 XML 片段與由諸如編碼字符集、DTD 文檔、命名空間、語言和空白處理等信息構成的分析器上下文關聯起來:

public XmlTextReader(
   string xmlFragment, 
   XmlNodeType fragType,
   XmlParserContext context
);

xmlFragment 參數包含要分析的 XML 字符串。fragType 參數代表片段的類型,並由片段的根節點類型給出。只允許元素、屬性和文檔節點作爲片段的根,並且分析器上下文由 XmlParserContext 類來表達。

驗證讀取器

XmlValidatingReader 類是 XmlReader 類的實現,它爲幾種類型的 XML 驗證提供支持:DTD、XML 數據簡化 (XDR) 架構和 XSD。DTD 和 XSD 是 W3C 發佈的正式建議,而 XDR 是 XML 架構的早期工作草案的 Microsoft 實現。

可以使用 XmlValidatingReader 類來驗證整個 XML 文檔以及 XML 片段。XmlValidatingReader 類工作在 XML 讀取器的頂部 £- 通常是 XmlTextReader 類的實例。文本讀取器用來遍歷文檔節點,而驗證讀取器應當按照所請求的驗證類型對每一片 XML 進行驗證。

XmlValidatingReader 類只實現了 XML 讀取器必須提供的功能中非常小的一個子集。該類總是工作在現有 XML 讀取器的頂部,並反映很多方法和屬性。如果查看類的構造函數,就會發現,驗證讀取器對現有文本讀取器的依賴性非常明顯。無法從文件或 URL 直接初始化 XML 驗證讀取器。可用構造函數的清單由下面的重載組成:

public XmlValidatingReader(XmlReader);
public XmlValidatingReader(Stream, XmlNodeType, XmlParserContext);
public XmlValidatingReader(string, XmlNodeType, XmlParserContext);

驗證讀取器可以分析可通過字符串或打開的流以及爲其提供讀取器的任何 XML 文檔訪問的任何 XML 片段。

類爲其提供有意義實現的方法的列表非常短。除了 Read,還有 Skip 和 ReadTypedValue 方法。在基本讀取器中,Skip 方法將跳過當前活動節點的子代。(需要注意的是,無法跳過格式錯誤的 XML 文本。)Skip 方法還會驗證被跳過的內容。ReadTypedValue 方法將基本節點的值作爲 CLR 類型返回。如果方法可以使用 CLR 類型映射 XSD 類型,它將同樣返回它。如果直接映射是不可能的,則節點值將作爲字符串返回。

驗證讀取器只是它的名稱所指的東西 £- 根據當前架構來驗證當前節點結構的、基於節點的讀取器。驗證過程增量發生;它沒有可返回布爾值以表示給定文檔是否有效的方法。同樣使用 Read 方法在輸入文檔中移動。實際上,使用驗證讀取器的方式與使用任何其他基於 XML 的 .NET 框架讀取器是一樣的。在每一步,都會根據指定的架構對當前所訪問的節點的結構進行驗證,如果發現錯誤則產生異常。圖 4 包含的控制檯應用程序在命令行取得文件名稱,然後輸出驗證過程的結果。

ValidationType 屬性用於設置需要的驗證類型 £- DTD、XSD、XDR 或無。如果沒有指定驗證類型(通過使用 ValidationType.Auto 選項),則讀取器自動應用它認爲最適合於該文檔的驗證。調用方應用程序將通過 ValidationEventHandler 事件得到任何錯誤通知。如果沒有指定自定義事件處理程序,則對應用程序產生 XML 異常。定義 ValidationEventHandler 方法是捕獲由源文檔中的不一致所導致的任何 XML 異常的一個途徑。注意,讀取器檢查文檔格式是否規範的機制與檢查架構遵守情況的機制是明顯不同的。如果驗證讀取器碰巧遇到格式錯誤的 XML 文檔,則不會觸發事件,但會產生 XmlException 異常。

驗證發生在用戶使用 Read 方法向前移動指針時。一旦節點已被分析和讀取,它就會被傳遞給內部的驗證器對象,進行進一步處理。驗證器基於節點類型和已請求的驗證類型執行操作。它確保節點擁有所有屬性以及期望它包含的子代。

驗證器對象內部調用兩種風格的對象:DTD 分析器和架構生成器。DTD 分析器根據 DTD 處理當前節點及其子樹的內容。架構生成器基於 XDR 或 XSD 架構源代碼爲當前節點建立一個架構對象模型 (SOM)。架構生成器類實際上是更特殊的 XDR 和 XSD 架構生成器的基類。但重要的是,處理 XDR 和 XSD 架構的方式基本相同,並且在性能上沒有差異。

如果節點有子代,則用另一個臨時讀取器來收集有關節點子代的信息,以便可以完整地調查節點的架構信息,如圖5 所示。


5 對子節點使用臨時讀取器

注意,儘管一個 XmlValidatingReader 構造函數的簽名通常引用 XmlReader 類作爲基本讀取器,但讀取器只能是 XmlTextReader 類或從它派生的類的實例。這意味着,您無法使用碰巧繼承自 XmlReader(例如,自定義的 XML 讀取器)的任何類。在內部,XmlValidatingReader 類假定基本讀取器是 XmlTextReader 對象,並且明確地將輸入讀取器轉換爲 XmlTextReader。如果使用 XmlNodeReader 或自定義讀取器類,則編譯時不會遇到任何錯誤,但將在運行時產生異常。

節點讀取器

XML 讀取器類可以按增量方式、逐節點地處理文檔內容。迄今爲止,我假設源文檔是基於磁盤的流或字符串。但是,也可以使用 XMLDOM 對象作爲源文檔。但在這種情況下,需要具有特殊 Read 方法的特殊類。爲此,.NET 框架提供了 XmlNodeReader 類。

正如 XmlTextReader 訪問所指定的 XML 流的所有節點一樣,XmlNodeReader 類訪問構成 XMLDOM 子樹的所有節點。XML DOM 類(在 .NET 框架中是 XmlDocument)提供了基於 XPath 的方法,例如 SelectNodes 和 SelectSingleNode。這些方法的實際結果是將匹配的節點集加載到內存中。如果需要處理子樹中的所有節點,則節點讀取器更有效率,因爲它只是以類似遊標的增量方式逐個處理節點:

// xmldomNode is the XML DOM node
XmlNodeReader nodeReader = new XmlNodeReader(xmldomNode);
while (nodeReader.Read()) {
    // Do something here
}

在處理填滿從配置文件(例如,web.config)摘取的自定義數據的 XMLDOM 樹時,將 XmlNodeReader 類與 XML DOM 類一起使用也是有好處的。

XmlTextWriter 類

以編程方式創建 XML 文檔再也不是特別困難的事情。這些年來,開發人員實現該操作的方式是:將幾個字符串在緩衝區中連在一起,在完成後,再將緩衝區內容全部寫到文件中。但是,只有當您能夠保證細微的錯誤永遠不會進入代碼流時,以這種方式創建 XML 文檔纔是有效的。.NET 框架通過使用 XML 寫入器,爲創建 XML 文檔提供了更有效率和更精彩的手段。

XML 寫入器類用於將 XML 數據以只進方式輸出到流或文件中。更重要的是,由設計保證 XML 寫入器產生的所有 XML 數據都遵守 W3C XML 1.0 和命名空間建議。您甚至不必關心單獨的尖括號,也不必擔心您讓其處於打開狀態的最後一個元素節點。XmlWriter 是所有 XML 寫入器的基本抽象類。.NET 框架只提供了一個實際的寫入器類 £- XmlTextWriter 類。

要查看 XML 寫入器和舊風格寫入器之間的差異,請考慮下面的代碼保留一個字符串數組:

StringBuilder sb = new StringBuilder("");
sb.Append("<array>");
foreach(string s in theArray) {
   sb.Append("<element value=/"");
   sb.Append(s);
   sb.Append("/"/>");
}
sb.Append("</array>");

代碼循環遍歷數組元素,準備標記文本,並將它累積在字符串緩衝區中。代碼負責確保輸出的格式是規範的,並且處理縮進、換行和命名空間支持。只要待創建的文檔是簡單的並具有某種結構,這種方式就不會出現明顯的錯誤。但是,如果必須支持處理指令、命名空間、縮進、格式化和實體,則所需代碼的複雜性就會呈指數級增長,造成錯誤和程序問題的可能性也會以同樣方式增長。

XML 寫入器具有爲每種可能的 XML 節點類型提供寫入方法的特點,並使創建 XML 輸出更符合邏輯和更少依賴於標記語言的細節。圖 6 說明如何使用 XmlTextWriter 類的服務序列化一個字符串數組。儘管同等緊湊,但該代碼使 XML 寫入器的使用變得清晰得多且更結構化。

遺憾的是,XML 寫入器沒有魔力 £- 它無法修復輸入錯誤。XML 寫入器不會檢查在元素和屬性名中是否有無效字符,並且不保證所使用的任何 Unicode 字符將適合於當前編碼架構。前面已經提到,要避免不正確的輸出,非 XML 字符在作爲 XML 往返轉換時必須被正確轉義。而寫入器不提供該服務。

此外,創建屬性節點時,寫入器不會驗證元素是否已存在同名屬性。最後,XmlWriter 類不是驗證寫入器,它不會根據任何架構或 DTD 保證輸出是有效的。.NET 框架目前還不提供驗證寫入器類。但是,我的書 Applied XML Programming for Microsoft .NET (Microsoft Press,2002) 中附帶的代碼包含了我編寫的實現。可以從 http://www.microsoft.com/MSPress/books/6235.asp 下載代碼。

圖 7 總結了 XML 寫入器的實際可行狀態。所有值均來自 WriteState 枚舉類型。創建寫入器時,它的狀態被設置爲 Start,這表示您還在配置對象,實際寫入階段尚未開始。下一個狀態是 Prolog,一旦您調用方法 WriteStartDocument 開始工作之後不久就會進入該狀態。之後,狀態轉換取決於您正在寫入的文檔的類型及其內容。只要您添加非元素節點(比如註釋、處理指令和文檔類型),狀態就會保持爲 Prolog。寫入第一個元素節點(文檔根節點)時,狀態就會更改爲 Element。調用 WriteStartAttribute 方法時,狀態切換爲 Attribute,但在使用更直接的 WriteAttributeString 方法寫入屬性時則不切換。在這種情況下,狀態繼續設置爲 Element。寫入關閉標記將使狀態轉爲 Content。完成後,您調用 WriteEndDocument,狀態將返回到 Start,直到開始另一個文檔或關閉寫入器爲止。

寫入器將輸出文本存儲在內部緩衝區中。通常情況下,緩衝區會全部騰空,XML 文本只在寫入器關閉時寫入。通過調用 Flush 方法,可以在任何時候騰空緩衝區,並將當前內容寫入基本流(通過 BaseStream 屬性公開)。某些工作內存被釋放,寫入器保持打開狀態,並且操作可以繼續。但請注意,其他過程無法訪問部分寫入的存儲文件,直到寫入器關閉。

屬性節點可以用兩種方式寫入。第一個方式涉及使用 WriteStartAttribute 方法來創建新的屬性節點,並相應更新狀態。然後用 WriteString 方法設置屬性的值。WriteEndElement 將關閉節點的寫入階段。另一種方式是使用 WriteAttributeString 方法,該方法在狀態爲 Element 時工作,並以一步操作創建屬性。同樣,WriteStartElement 將寫入節點的打開標記,然後您可以隨意設置節點的屬性和文本。通常,元素節點的關閉標記採用緊湊形式“/>”。如果想要完整地關閉標記,請使用 WriteFullEndElement 方法。

如果傳遞給寫入方法的文本包含敏感標記字符(例如,小於符號“<”),則該文本全部會被自動轉義。但 WriteRaw 方法讓您有機會將未分析的數據輸入流中。如果看一看這兩行代碼,就會看到,第一行代碼輸出 <,而第二行輸出未分析的 < 字符:

writer.WriteString("<");
writer.WriteRaw("<");

讀取和寫入流

有趣的是,讀取器和寫入器類提供了讀取和寫入數據流的方法(即使數據流被編碼爲 Base64 或 BinHex)。方法 WriteBase64 和 WriteBinHex 的特點是有一個與其他寫入方法略微不同的簽名。因爲它們是基於流的,所以方法起一個字節數組(而不是字符串)的作用。下面的代碼首先將字符串轉換成字節數組,然後將它作爲 Base64 編碼的流寫入。Encoding 類的靜態 GetBytes 方法執行轉換:

writer.WriteBase64(
   Encoding.Unicode.GetBytes(buf), 
   0, buf.Length*2);

圖 8 中的代碼將一個字符串數組保留爲 Base64 編碼的 XML 流,而圖 9 演示了當它出現在 Microsoft Internet Explorer 時的最終結果。


9 Internet Explorer 中的字符串數組

Reader 類具有相應的方法來對 Base64 和 BinHex 流進行解碼。下面的代碼片段說明如何使用 XmlTextReader 類的 ReadBase64 方法對先前創建的文件進行解碼:

XmlTextReader reader = new XmlTextReader(filename);
while(reader.Read()) {
  if (reader.LocalName == "element") {    
    byte[] bytes = new byte[1000];
int n = reader.ReadBase64(bytes, 0, 1000);
string buf = Encoding.Unicode.GetString(bytes);
    Console.WriteLine(buf.Substring(0,n));
  }
}
reader.Close();

字節到字符串的轉換是由 Encoding 類的 GetString 方法執行的。儘管我提供了 Base64 編碼架構的代碼,但如果要使用 BinHex 只需替換方法名稱。該技術可成功用於能夠用字節數組表達的任何種類的二進制數據,尤其是圖像。

設計 XmlReadWriter 類

前面已經提到,XML 讀取器和寫入器在相互隔離的情況下工作:讀取器只讀取,而寫入器只寫入。假設應用程序管理着冗長的、包含不穩定數據的 XML 文檔。讀取器就是讀取該內容的好方式。另一方面,寫入器是從頭創建文檔的非常有用的工具。但如果想要同時讀取並寫入文檔,則必須求助於成熟的 XMLDOM。如果文檔特別巨大,就會有問題。例如,可以用什麼辦法讀取和寫入 XML 文檔,而不用將其完全加載到內存中?讓我們考察一下如何建立一種混合類型的流式分析器,並讓它充當輕量級 XMLDOM 分析器。

至於只讀操作,可以使用標準的 XML 讀取器按順序訪問節點。差異是,在讀取時您將有機會通過在讀取器的頂部使用 XML 寫入器,來更改屬性值和節點內容。可以使用讀取器來讀取源文檔中的每個節點,並使用基本寫入器來創建它的隱藏副本。在副本中,可以添加某些新節點,忽略或編輯某些其他節點,並編輯屬性值。完成後,只需用新文檔替換舊文檔。

將若干批的節點從只讀流(成批)複製到寫入流的一個有效方式是使用 XmlTextWriter 類的兩個方法:WriteAttributes 和 WriteNode。WriteAttributes 方法可以讀取在讀取器中當前選中的節點上可用的所有屬性。下一步,該方法將把屬性作爲單個字符串複製到當前的輸出流。同樣,WriteNode 方法對任何其他類型的節點(除了屬性節點)執行相同的操作。圖 10 顯示的一段代碼使用這些方法來創建原始 XML 文件的副本,它經過修改以跳過某些節點。以通常的 node-first 方式訪問 XML 樹,但只寫出所有其他節點。可以將讀取器和寫入器合併在一個新類中,並生成全新的編程接口,以便允許容易地對屬性或節點進行讀取/寫入流訪問。

我的 XmlTextReadWriter 類沒有繼承自 XmlReader 或 XmlWriter,而是協調這兩個類的正在運行的實例的活動 £- 一個實例操作只讀流,另一個處理只寫流。XmlTextReadWriter 類的方法從讀取器讀取數據,並寫入到寫入器,並在這兩個過程中間應用任何所請求的更改。內部讀取器和寫入器分別通過稱爲 Reader 和 Writer 的只讀屬性公開。圖 11 列出了新類的方法。

這個類有 Read 方法,該方法是讀取器的 Read 方法的簡單包裝。此外,它還有一對 WriteStartDocument 和 WriteEndDocument 方法,這兩個方法可以初始化和終結內部讀取器和寫入器,並執行所有必需的 I/O 操作。涉及節點的更改由客戶端在讀取循環期間直接執行。出於性能原因,涉及編輯屬性的更改必須首先使用 AddAttributeChange 方法進行註冊。對節點屬性的所有更改都被臨時存儲在內部表中,並在調用 WriteAttributes 時被全部寫出。

圖 12 中的代碼說明了一個客戶端如何在讀取的同時利用 XmlTextReadWriter 類來更改屬性值。本月的代碼下載(鏈接位於本文頂部)提供了 XmlTextReadWriter 類的全部源代碼(C# 和 Visual Basic .NET 版本)。

XmlTextReadWriter 類更像是讀取器而不是寫入器。原因是,使用它可以讀取 XML 文檔的內容,但如果需要,還可以執行某些基本的更新。按照我的理解,基本更新是更改一個或多個現有屬性的值或節點的內容,或添加新的屬性或節點。對於更復雜的操作,則沒有可取代 XMLDOM 分析器的其他途徑。

小結

讀取器和寫入器是 .NET 框架中 XML 數據支持的基礎。它們代表了所有 XML 數據訪問功能的原始 API。讀取器充當了新式和創新類型的分析器,它介於真正功能強大的 XMLDOM 和由 SAX 所提供的快速和簡單方式二者之間。寫入器是與讀取器相對應的領域,組成它的各種工具被設計用於簡化 XML 文檔的創建。儘管讀取器和寫入器通常作爲 .NET 框架的一部分進行引用,但它們實際上是完全獨立的 API。本文討論瞭如何使用讀取器和寫入器完成關鍵任務,並介紹了驗證分析器的體系結構。將讀取器和寫入器統一在單獨的一個包含所有所需功能的類中仍然是可能的,這將產生一個輕量級、類似遊標的 XMLDOM 模型。

有關相關文章,請參閱:

XML in .NET:.NET Framework XML Classes and C# Offer Simple, Scalable Data Manipulation

Implementing XmlReader Classes for Non-XML Data Structures and Formats

Applied XML Programming for Microsoft .NET,作者:Dino Esposito(Microsoft Press 2002 年出版)

有關背景信息,請參閱:

XML Data:Overview of XML

Dino Esposito 是一位定居於意大利羅馬的培訓講師和顧問。他是 Building Web Solutions with ASP.NETADO.NET 的作者,這兩本書都由 Microsoft Press 出版。本文改編了在他所著的 Applied XML Programming for Microsoft .NET(Microsoft Press 2002 年出版)一書中詳述的某些概念。如果希望與 Dino 聯繫,可發送電子郵件至 [email protected]。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章