在.NET Framework中轻松处理XML数据ZT

http://www.pconline.com.cn/pcedu/empolder/wz/xml/10305/173130.html

在.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}", 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方法了。



分析属性值







大部分情况下,属性值都是一个简单的文本字符串。然而,这并不意味着实际应用中的属性值都是字符型的。有时候,属性值是由许多种类型的数据组合而成的,例如Date或Boolean,这时,你就要用XmlConvert或System.Convevt类的方法把这些类型转换成原来的类型。XmlConvert和System.Convevt类都能实现数据类型的转换,但是XmlConvert类依据XSD中指定的数据类型进行转换,而不管它现在是什么类型。



假设你有以下的XML数据片断:







让我们先确认,birthdaay属性值是February 8, 2001,如果你用System.Convert类把该字符串转换成.NET Framework中的DateTime类型,这样,我们就可以把它当成date类型使用了。相比下,如果你用XmlConvert类来转换字符串,你将看到一个分析错误,因为XmlConvert类不能正确解释这个字符串中的日期。因为在XML中,日期型数据的格式必须是YYYY-MM-DD形式的。XmlConvert类担任CLR类型与XSD类型之间的相互转换工作。当转换工作发生时,转换结果是局部的。



在某些解决方案中,属性值是由纯文本和实体共同组成的。在所有的阅读器类中,只有XmlValidatingReader类能处理实体。XmlTextReader虽然不能处理实体,但它们同时出现在属性值中的时候,它只能把文本值取出来。出现这种情况,你必须用ReadAttributeValue方法替代简单的读方法来分析属性值的内容。



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;



}



当属性值全部被分析后,ReadAtributeValue方法返回False, 从而结束循环。属性值的最终结果就是全局变量buffer的值了。



处理XML文本(Text)



当我们在处理XML标签文本时,如果不能正确的处理,它的错误原因能很快地确定。例如一个字符转换错误,它必然是传输了非XML文本到一个XML数据流中。不是所有在给定的平台中有效的字符都是有效的XML字符。只有在XML规范(Extensible Markup Language (XML) 1.0 (Second Edition))中规定的有效的字符才能安全的用作元素和属性名。



XmlConvert类提供了把非XML标准的命名转换成标准的XML命名的功能。当标签名中包含有无效的XML字符时,EncodeName 和 DecodeName方法能把它们调整成符合Schema的XML命名。包括SQL Server™ 和Microsoft Office,这些应用程序允许及支持Unicode文档,然而,这些文档中的字符有些也不是有效的XML命名。典型的情况是在你处理数据库中包含空格的列名时。虽然SQL Server允许长列名,但这对XML流来说可能就不是有效的命名。空格会被十六进制代码Invoice_0x0020_Details替代。下面的代码演示了怎么样在程序中获得该字符串:



XmlConvert.EncodeName(“Invoice Details”);



与此相反的方法是DecodeName。该方法把XML文本转换成其原始的格式。要注意的是它只能转换完整的十六进制代码,只有_0x0020_才被当成一个空格,而_0x20_就不是了:



XmlConvert.DecodeName(“Invoice_0x0020_Details”);



在XML文档中的空格即重要也不重要。说它重要,是当它出现在元素的内容中或者它在注释语句中时,它能表示实际意义。例如下面的情况:







<MyNode xml:space="preserve">



<!-- any space here must be preserved -->



•••



</MyNode>



在xml中,空格不只是代表空格(空白),也代表回车、换行和缩进。



通过XmlTextReader类的WhiteSpaceHandling属性你可以处理空格。这个属性接受及返回一个WhiteSpaceHandling枚举值(该枚举类有三种可选值)。默认值是All,它表示有意义和无意义的空格都会作为节点返回---- 分别为SignificantWhitespace和Whitespace节点。 另一个枚举值是None,它表示对任何空格都不作为节点返回。最后,就是Signficant枚举值,它表示忽略没有意义的空格,而只返回节点类型为SignficantWhitespace的节点。注意WhiteSpaceHandling属性是少数阅读器属性中的一个。它能被改变在任何时候和给Read操作带来影响。而Normalization及 XmlResolver属性是“Sensitive”的。



String和Fragment



程序员把在MSXML的程序剪切下来,会发现在COM和.NET Framework XML API 之间的差别很大。.NET Framework类本身没有提供方法去分析存储在字符串中XML数据。不像MSXML分析器对象,XmlTestReader类没有提供任何一种LoadXML方法从一个格式良好的字符中创建阅读器。没有提供类似LoadXML的方法因为你可以用特殊的text reader—StringReader类来获得同样的功能。



XmlTextReader其中一个构造函数接受一个TextReader派生对象和一个XML reader作参数(该阅读器以text reader的内容为基础创建)。一个text reader类是一个流,这个流是输入的字符经优化生成的。StringReader类继承TextReader类,并用一个内存中字符串作为其输入流。下面的代码片断演示了怎样初始化一个XML reader,用一个格式良好的XML 字符串作为其输入:



string xmlText = “…”;



StringReader strReader = new StringReader(xmlText);



XmlTextReader reader = new XmlTextReader(strReader);







另外,用StringWriter类代替TextWrite类,你可以从内存字符中创建一个XML文档。



一个指定类型的XML字符串是一个XML片断(fragment). XML片断由XML文本构成,但没有根节点的XML文档不是格式良好的XML文档,所以不能被应用。一个XML片断是原始的文档的一部分,所以它可能缺少根节点。例如,下面的XML文本是一个有效的XML 片断,但不是一个有效的XML文档,因为它没有根节点:



Dino



Esposito



.NET Framework XML API允许程序员把XML片断与一个分析器内容结合使用,分析器内容由类似encoding字符集,DTD文档,命名空间,语言和空格处理程序构成:



public XmlTextReader(



string xmlFragment,



XmlNodeType fragType,



XmlParserContext context



);



xmlFragment参数包括了XML字符串分析。FragType参数表示fragment的类型,它给出了fragment根节点的类型。只有element,attibute和document类型的节点才能作为fragment的根节点,分析器的内容才能被XmlParserContext类解释。

带验证的阅读器







XmlValidatingReader类实现了XmlReader类,它提供了支持多种类型的XML验证:DTD,XML-Data Reduced(XDR)架构,以及XSD,DTD和XSD都是W3C官方推荐的。而XDR是Microsoft早期用于处理XML构架的一种格式。



你可以用XmlVlidatingReader类去验证XML文档和XML片断。XmlValidatingReader类工作在XML阅读器上面—是一个典型的XMLTextReader类实例。XMLTextReade用于读取文档的节点,但是XmlVlidatingReader依据需要的验证类型去验证每一个XML块。



XmlVlidatingReader类只实现了非常小的XML阅读器必备的一个功能子集。该类总是工作在一个已存在的XML阅读器上面,它监视方法和属性。如果你深入该类的构造函数,你会发现它很明显的依靠一个已存在的文本阅读器。带验证的XML阅读器不能直接的从一个文件或一个URL序列化。该类的构造函数列表如下:



public XmlValidatingReader(XmlReader);



public XmlValidatingReader(Stream, XmlNodeType, XmlParserContext);



public XmlValidatingReader(string, XmlNodeType, XmlParserContext);



带验证的XML阅读器能分析任何的XML片断,XML片断通过一个string或者一个stream提供,也可以分析任何阅读器提供的XML文档。



XmlVlidatingReader类中有重大改变的方法非常少(相对其它reader类来说),另外对 Read,它有Skip和ReadTypedValue方法。Skip方法跳过当前节点所有的子节点(你不能跳过不良格式的XML文本,它是相当有用的算法),Skip方法也验证被跳过的内容。ReadTypedValue方法返回指定 XML 架构 (XSD) 类型对应的CLR类型。如果该方法找到了XSD类型对应的CLR类型,则返回CLR的类型名。如果找不到,则把该节点的值作为一个字符串值返回。



带验证的XML阅读器正如其名,它是一个基于节点的阅读器,它验证当前节点的结构是否符合当前的schema。验证是增量式的;它没有方法返回表示文档是否有效的布尔值。通常你都是用Read方法去读输入的XML文档。实际上,你也可以用带验证的阅读器去读XML文档。在每一步中,当前被访问的节点的结构是否与指定的schema符合,如果不符合,抛出一个异常。图四是一个控制台应用程序,它有一个要输入文件名的命令行,最后输出验证结果。



Figure 4 Console App



using System;



using System.Xml;



using System.Xml.Schema;







class MyXmlValidApp



{



public MyXmlValidApp(String fileName)



{



try {



Validate(fileName);



}



catch (Exception e) {



Console.WriteLine(“Error:t{0}”, e.Message);



Console.WriteLine(“Exception raised: {0}”,



e.GetType().ToString());



}



}







private void Validate(String fileName)



{



XmlTextReader xtr = new XmlTextReader(fileName);



XmlValidatingReader vreader = new XmlValidatingReader(xtr);



vreader.ValidationType = ValidationType.Auto;



vreader.ValidationEventHandler += new



ValidationEventHandler(this.ValidationEventHandle);







vreader.Read();



vreader.MoveToContent();







while (vreader.Read()) {}







xtr.Close();



vreader.Close();



}







public void ValidationEventHandle(Object sender,



ValidationEventArgs args)



{



Console.Write("Validation error: " + args.Message + “rn”);



}







public static void Main(String[] args)



{



MyXmlValidApp o = new MyXmlValidApp(args[0]);



return;



}



}



ValidationType属性设置验证的类型,它可以是:DTD, XSD, XDR或者none。如果没有指定验证的类型(用ValidationType.Auto选项),阅读器将自动的根据文档用最适合的验证类型。在验证过程中出现任何错误,都会触发ValidationEventHandler事件。如果未提供事件ValidationEventHandler事件处理程序,则抛出一个XML异常。定义ValidationEventHandler事件处理程序是用于捕捉任何在XML源文件中存在错误而引发XML异常的一种方法。要注意的是阅读器的原理是检查一个文档是否是格式良好的,以及检查文档是否与架构吻合。如果带验证的阅读器发现一个有严重的格式错误的XML文档,只会触发XmlException异常,它不会触发其它的事件。



验证发生在用户用Read方法向前移动指针时,一旦节点被分析和读取,它获得传送过来的处理验证的内部的对象。验证操作是基于节点类型及被要求的验证类型。它确认节点所有的属性和节点包含的子节点是否符合验证条件。



验证对象在内部调用两个不同风格的对象:DTD分析器和架构生成器(schema builder)。DTD分析器处理当前节点的内容和不符合DTD的子树。架构生成器根据XDR或者XSD架构对当前的节点构建一个SOM(schema object model)。架构生成器类实际上是所有指定为XDR和XSD架构生成器的基类。为什么呢,虽然XDR和XSD架构的许多相同的方法被加工处理过,但是它们在执行时的性能没有区别。



如果节点有子节点,用另一个临时的阅读器收集子节点信息,因此节点的架构信息能被完全地验证。你可以看图五:





注意,尽管XmlValidatingReader类的构造函数可以接受一个XmlReader类作为其阅读器,但是该阅读器只能是XmlTextReader类的一个实例或者是它的一个派生类的实例。这意味着你不能用其它从XmlReader派生的类(例如一个自定义的XML阅读器)。在XmlValidatingReader类的内部,它假设阅读器是一个子XmlTextReader对象及把传入的阅读器显式的转换成XmlTextReader类。如果你用XmlNodeReader或者自定义的阅读器器,程序在编译时会出错,运行时抛出一个异常。





节点阅读器



XML阅读器提供一种增量式的方法(一个一个节点的读)来处理文档的内容。到目前为止,我们假设源文件是一个基于硬盘的流或者是一个字符串流,然而,我们不能保证在实际中会提供一个源文件的XMLDOM对象给我们。在这种情况下,我们需要一个带有特别的读方法的特别的类。对这种情况,.NET Framework提供了XmlNodeReader类。



就像XmlTextReader访问指定XML流中所有节点一样,XmlNodeReader类访问XMLDOM子树的所有节点。XMLDOM类(在.NET Framework中的XmlDocument类)支持基于Xpath的方法,例如SelectNodes方法和SelectSingleNode方法。这些方法的作用是把匹配的节点放在内存中。如果你需要处理子树中的所有节点,节点阅读器比用增量式方法处理节点的阅读器具有更高的效率:



// xmldomNode is the XML DOM node



XmlNodeReader nodeReader = new XmlNodeReader(xmldomNode);



while (nodeReader.Read())



{



// Do something here



}



当你要在配置文件(例如web.cofig文件)中引用自定义的数据时,先把这些数据填充到XMLDOM树中,然后用XmlNodeReader类与XMLDOM类结合处理这些数据。这也是高效的。