c#

位置:IT落伍者 >> c# >> 浏览文章

详谈.NET Framework处理XML操作技巧


发布日期:2018年10月22日
 
详谈.NET Framework处理XML操作技巧

Net Framework中XMLTextReader和XmlTextWriter类提供了对xml数据的读和写操作在本文中作者讲述了XML阅读器(Reader)的体系结构及它们怎样与XMLDOM和SAX解释器结合作者也演示了怎么样运用阅读器分析和验证XML文档怎么样创建格式良好的XML文档以及怎么样用函数读/写基于Base和BinHex编码的大型的XML文档最后作者讲了怎么样实现一个基于流的读/写分析器它把读写器都封装在一个单独的类里

大概三年前我参加了一个软件研讨会主题是没有XML就没有编程的未来XML确实也在一步一步的发展它已经嵌入到NET Framework中了在本文中我将讲解NET Framework中用于处理XML文档的API的角色和它的内部特性然后我将演示一些常用的功能

从的XML

NET Framework出现之前你习惯使用MSXML服务——一个基于COM的类库——写Windows的XML的驱动程序不像NET Framework中的类MSXML类库的部分代码比API更深它完全的嵌在操作系统的底层MSXML的确能够与你的应用程序通信但是它不能真正的与外部环境结合

MSXML类库能在win中被导入也能在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分析器的底层支持XmlReaderXmlWriter两个抽象类是所有NET Framework中XML类的基础类包括XMLDOM类ADONET驱动类及配置类所以在NET Framework中你有两种可选的方法去处理XML数据用XmlReader和XmlWriter类直接处理XML数据或者用XMLDOM模式处理更多的关于在NET Framework中读文档的介绍可以参见MSDN 年八月刊的Cutting Edge栏目文章

XmlReader类

XML阅读器支持一个编程接口接口用于连接XML文档推出你要的数据如果你更深入去了解阅读器你会发现阅读器工作原理类似于我们的桌面应用程序从数据库中取出数据的原理数据库服务返回一个游标对象它包含所有查询结果集并返回指向目标数据集的开始地址的引用XML阅读器的客户端收到一个指向阅读器实例的引用该实例提取底层的数据流并把取出的数据呈现为一棵XML树阅读器类提供只读向前的游标你可以用阅读器类提供的方法滚动游标遍历结果集中的每一条数据

从阅读器中看XML文档不是一个标签文本文件而是一个序列化的节点集合它是NET Framework中的一种特殊的游标模式NET Framework中你找不到其它的任何一个类似的API函数

阅读器和XMLDOM分析器有几点不同的地方XML阅读器是只进的它没有父祖宗兄弟节点的概念而且是只读的NET Framework中读写XML文档是分为两种完全不同的功能分别由XmlReader和XmlWriter类来完成要编辑XML文档你可以用XMLDOM分析器或者你自己设计一个类来实现这两种功能让我们开始分析阅读器的程序功能

XmlReader是一个抽象类你可以继承并扩展它的功能用户程序一般都基于下面的三种类XmlTextReaderXmlValidatingReader或者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方法或其它方法(如SkipMoveToContent和ReadInnerXml)移动指针到下一个节点元素要处理整个XML文档的内容可以根据Read方法的返回值用一个循环遍历文档内容因为Read方法返回一个布尔值当读到文档的尾节点时Read方法返回false否则它返回true

Figure 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 (readerRead())

{

// 写开始标志如果节点类型为元素

if (readerNodeType == XmlNodeTypeElement)

{

//根据元素所处节点的深度加入readerDepth个tab符然后把元素名写入到<>中

tabPrefix = new string(\t readerDepth);

writerWriteLine({}<{}> tabPrefix readerName);

}

else

{

//写结束标志如果节点类型为元素

if (readerNodeType == XmlNodeTypeEndElement)

{

tabPrefix = new string(\t readerDepth);

writerWriteLine({} tabPrefix readerName);

}

}

}

// 输出到屏幕

string buf = writerToString();

writerClose();

// 关闭流

readerClose();

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 (readerHasAttributes)

while(readerMoveToNextAttribute())

buf += readerName + =\ + readerValue + \;

readerMoveToElement();

当你完成对属性集的处理时调用MoveToElement方法使指针返回到属性所属的元素节点准确的说MoveToElement方法并不是真正的移动指针因为在处理属性集时指针从来就没有从元素节点中移开MoveToElement方法只不过指向某个内部成员并依次取得成员的值例如用Name属性获得某个属性的属性名然后调用MoveToElement方法把指针移到其所属的元素节点处但是当你不需要继续处理别的节点时就不必再调用MoveToElement方法了

分析属性值

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

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

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

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

ReadAttributeValue方法分析属性值然后把各个组成的要素分隔开(如把纯文本和实体分开)你可以用ReadAttributeValue方法的返回值作为循环条件遍历整个属性值中的要素既然XmlTextReader类不能处理实体那么你可以自己写一个用于处理实体的类下面的代码片断演示了怎么调用一个自定义的处理类

while(readerReadAttributeValue())

{

if (readerNodeType == XmlNodeTypeEntityReference)

// Resolve the readerName reference and add

// the result to a buffer

buf += YourResolverCode(readerName);

else

// Just append the value to the buffer

buf += readerValue;

}

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

处理XML文本(Text)

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

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

XmlConvertEncodeName(Invoice Details);

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

XmlConvertDecodeName(Invoice_x_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的程序剪切下来会发现在 Framework XML API 之间的差别很大NET Framework类本身没有提供方法去分析存储在字符串中XML数据不像MSXML分析器对象XmlTestReader类没有提供任何一种LoadXML方法从一个格式良好的字符中创建阅读器没有提供类似LoadXML的方法因为你可以用特殊的text readerStringReader类来获得同样的功能

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根节点的类型只有elementattibute和document类型的节点才能作为fragment的根节点分析器的内容才能被XmlParserContext类解释

带验证的阅读器

XMLValidatingReader类实现了XmlReader类它提供了支持多种类型的XML验证DTDXMLData Reduced(XDR)架构以及XSDDTD和XSD都是WC官方推荐的而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 Console App

using System;

using SystemXml;

using SystemXmlSchema;

class MyXmlValidApp

{

public MyXmlValidApp(String fileName)

{

try {

Validate(fileName);

}

catch (Exception e) {

ConsoleWriteLine(Error:\t{} eMessage);

ConsoleWriteLine(Exception raised: {}

eGetType()ToString());

}

}

private void Validate(String fileName)

{

XmlTextReader xtr = new XmlTextReader(fileName);

XmlValidatingReader vreader = new XmlValidatingReader(xtr);

vreaderValidationType = ValidationTypeAuto;

vreaderValidationEventHandler += new

ValidationEventHandler(thisValidationEventHandle);

vreaderRead();

vreaderMoveToContent();

while (vreaderRead()) {}

xtrClose();

vreaderClose();

}

public void ValidationEventHandle(Object sender

ValidationEventArgs args)

{

ConsoleWrite(Validation error: + argsMessage + \r\n);

}

public static void Main(String[] args)

{

MyXMLValidApp o = new MyXmlValidApp(args[]);

return;

}

}

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

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

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

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

498)this.width=498;" 0pt? 0cm MARGIN:>

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

节点阅读器

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

{

// Do something here

}

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

XMLTextWriter类

用在本节中的方法创建XML文档显然并不困难多年以来开发者都是通过在缓存在连接一些字符串连接好以后再把缓存中字符串输出到文件的方式来创建XML文档但是以这种方式创建XML文档的方法只有在你保证字符串中不存在任何细小的错误的时候才有效 Framework通过用XMLwriter提供了更好的创建XML文档的方法

XML Writer类以只前(forwardonly)的方式输出XML数据到流或者文件中更重要的是XML Writer在设计时就保证所有的XML数据都符合WC XML 推荐规范你甚至不用担心忘记写闭标签因为XML Writer会帮你写XmlWriter是所有 XML writer的抽象基类NET Framework只提供唯一的一个writer 类XmlTextWriter类

我们先来看看XML writers和旧的writers的不同点下面的代码保存了一个string型的数组

StringBuilder sb = new StringBuilder();

sbAppend();

foreach(string s in theArray) {

sbAppend(

sbAppend(s);

sbAppend(\\\\\\\\/>);

}

sbAppend();

代码通过循环取出数据中的元素写好标签文本并把它们累加到一个string中代码保证输出的内容是格式良好的并且注意了新行的缩进及支持命名空间当创建的文档结构比较简单时这种方法可能不会有错误然而当你要支持处理指令命名空间缩进格式化以及实体的时候代码的数量就成指数级增长出错的可能性也随之增长

XML writer写方法功能对应每个可能的XML节点类型它使创建xml文档的过程更符合逻辑更少的信赖于繁琐的标记语言图六演示了怎么样用XmlTextWriter类的方法来连接一个string数据代码很简洁用XML writer的代码更容易读结构更好

Figure Serializing a String Array

void CreateXmlFileUsingWriters(String[] theArray string filename)

{

// Open the XML writer (用默认的字符集)

XmlTextWriter xmlw = new XmlTextWriter(filename null);

xmlwFormatting = FormattingIndented;

xmlwWriteStartDocument();

xmlwWriteStartElement(array);

foreach(string s in theArray)

{

xmlwWriteStartElement(element);

xmlwWriteAttributeString(value s);

xmlwWriteEndElement();

}

xmlwWriteEndDocument();

// Close the writer

xmlwClose();

}

然而XML writer并不是魔术师它不能修复输入的错误XML writer不会检查元素名和属性名是否有效也不保证被用的任何的Unicode字符集适合当前架构的编码集如上所述为了避免输出错误必须要杜绝非XML字符但是writer没有提供这种方法

另外当创建一个属性节点时Writer不会检验属性节点的名称是否与已存在的元素节点的名称相同最后XmlWriter类不是一个带验证的Writer类也不保证输出是否符合schema或者DTDNET Framework中带验证的writer类目前来说还没有提供但是在我写的《Applied XML Programming for Microsoft NET (Microsoft Press? )》书中我自己写了一个带验证的Writer组件你可以到下面的网址去下载源码

图七列出了XML writer的一些状态值(state)这些值都源于WriteState枚举类当你创建一个Writer它的初始状态为Start表示你将要配置该对象实际上writer没有开始下一个状态是Prolog该状态是当你调用WriteStartDocument方法开始工作的时候设置的然后状态的转换就取决于你的写的文档及文档的内容了Prolog状态一直保留到当你增加一个非元素节点时例如注释元素处理指令及文档类型当第一个节点也就是根节点写完后状态就变为Element当你调用WriterStartAtribute方法时状态转换为Attribute而不是当你调用WriteAtributeString方法写属性时转换为该状态如果那样的话状态应该是Element当你写一个闭标签(>)时状态会转换成Content当你写完文档后调用WriteEndDocument方法状态就会返回为Start直到你开始写另一个文档或者把Writer关掉

Figure States for XML Writer

State

Description

Attribute

The writer enters this state when an attribute is being written

Closed

The Close method has been called and the writer is no longer

available for writing operations

Content

The writer enters this state when the content of a node is being written

Element

The writer enters this state when an element start tag is being written

Prolog

The writer is writing the prolog of a wellformed XML document

Start

The writer is in an initial state awaiting for a write call to be issued

Writer 把输出文本存在内部的一个缓沖区内一般情况下缓沖区会被刷新或者被清除当Writer被关闭前XML文本应该要写出在任何时你都可以通过调用Flush方法清空缓沖区把当前的内容写到流中(通过BaseStream属性暴露流)然后释放部分占用的内存Writer仍保持为打开状态(open state)可以继续操作注意虽然写了部分的文档内容但是在Writer没有关闭前其它的程序是不能处理该文档的

可以用两种方法来写属性节点第一种方法是用WriteStartAtribute方法去创建一个新的属性节点更新Writer的状态接着用WriteString方法设置属性值写完后用WriteEndElement方法结束该节点另外你也可以用WriteAttributeString方法去创建新的属性节点当writerr的状态为Element时WriterAttributeString开始工作它单独创建一个属性同样的WriteStartElement方法写节点的开始标签(<)然后你可以随意的设置节点的属性和文本内容元素节点的闭标签都带/ >如果想写闭标签可以用WriteFullEndElement方法来写

应该避免传送给写方法的文本中包含敏感的标记字符例如小于号(<)用WriteRaw方法写入流的字符串不会被解析我们可以用它来对xml文档写入特殊的字符串下面的两行代码第一行输出的是<第二行输出<:

writerWriteString(<);

writerWriteRaw(<);

读写流

有趣的是reader(阅读器)和writer类提供了基于Base 和BinHex编码的读写数据流的方法WriteBase 和 WriteBinHex方法的功能与其它的写方法的功能存在着细微的差别它们都是基于流的这两个方法的功能像一个byte数组而不是一个string下面的代码首先把一个string转换成一个byte数组然后把它们写成一个Base 编码流Encoding类的GetBytes静态方法完成转换的任务

writerWriteBase(

EncodingUnicodeGetBytes(buf)

bufLength*);

图八中代码演示了把一个string数据转换为Base 编码的XML流图九是输出的结果

Figure Persisting a String Array as Base

using System;

using SystemText;

using SystemIO;

using SystemXml;

class MyBaseArray

{

public static void Main(String[] args)

{

string outputFileName = testxml;

if (argsLength >)

outputFileName = args[]; // file name

// 把数组转换成XML

String[] theArray = {Rome New York Sydney Stockholm

Paris};

CreateOutput(theArray outputFileName);

return;

}

private static void CreateOutput(string[] theArray string filename)

{

// 打开XML writer

XmlTextWriter xmlw = new XmlTextWriter(filename null);

//使子元素根据 Indentation 和 IndentChar 设置缩进此选项只对元素内容进行缩进

xmlwFormatting = FormattingIndented;

//书写版本为的 XML 声明

xmlwWriteStartDocument();

//写出包含指定文本的注释

xmlwWriteComment(Array to Base XML);

//开始写出array节点

xmlwWriteStartElement(array);

//写出具有指定的前缀本地名称命名空间 URI 和值的属性

xmlwWriteAttributeString(xmlns x null dinoe:msdnmag);

// 循环的写入array的子节点

foreach(string s in theArray)

{

//写出指定的开始标记并将其与给定的命名空间和前缀关联起来

xmlwWriteStartElement(x element null);

//把S转换成byte[]数组 并把byte[]数组编码为 Base 并写出结果文本

要写入的字节数为s总长度的一个string占的字节数是字节

xmlwWriteBase(EncodingUnicodeGetBytes(s) sLength*);

//关闭子节点

xmlwWriteEndElement();

}

//关闭根节点只有两级

xmlwWriteEndDocument();

// 关闭writer

xmlwClose();

// 读出写入的内容

XmlTextReader reader = new XmlTextReader(filname);

while(readerRead())

{

//获取节点名为element的节点

if (readerLocalName == element)

{

byte[] bytes = new byte[];

int n = readerReadBase(bytes );

string buf = EncodingUnicodeGetString(bytes);

ConsoleWriteLine(bufSubstring(n));

}

}

readerClose();

}

}

Figure String Array in Internet Explorer

Reader类有专门的解释Base和BinHex编码流的方法下面的代码片断演示了怎么样用XmlTextReader类的ReadBase方法解析用Base和BinHex编码集创建的文档

XmlTextReader reader = new XmlTextReader(filename);

while(readerRead()) {

if (readerLocalName == element) {

byte[] bytes = new byte[];

int n = readerReadBase(bytes );

string buf = EncodingUnicodeGetString(bytes);

ConsoleWriteLine(bufSubstring(n));

}

}

readerClose();

从byte型转换成string型是通过Encoding类的GetString方法实现的尽管我只介绍了基于Base编码集的代码但是可以简单的用BinHex替换方法名就可以实现读基于BinHex编码的节点内容(用ReadBinHex方法)这个技巧也可以用于读任何用byte数据形式表示的二进制数据尤其是image类型的数据

设计XMLReadWriter类

如前面所说XML reader和Writer是各自独立工作的reader只读writer只写假设你的应用程序要管理冗长的XML文档且该文档有不确定的数据Reader提供了一个很好的方法去读该文档的内容另一方面Writer是一个非常有用的用于创建XML文档片断工具但是如果你想要它即能读又能写那么你就要用XMLDOM了如果实际的XML文档非常庞大又会出现了一个问题什么问题呢?是不是把这个XML文档全部加载到内存中然后进行读和写呢?让我们先看一下怎么样建立一个混合的流分析器用于分析大型的XMLDOM

像一般的只读操作一样用普通的XML reader去顺序的访问节点不同的是在读的同时你可以用XML writer改变属性值以及节点的内容你用reader去读源文件中的每个节点后台的writer创建该节点的一个拷贝在这个拷贝中你可以增加一些新的节点忽略或者编辑其它的一些节点还可以编辑属性的值当你完成修改后你就用新的文档替换旧的文档

一个简单有效的办法是从只读流中拷贝节点对象到write流中这种方法可以用XmlTextWriter类中的两个方法WriteAttributes方法和WriteNode方法 WriteAttributes方法读取当前reader中选中的节点的所有有效的属性然后把属性当作一个单独的string拷贝到当前的输出流中同样的WriteNode方法用类似的方法处理除属性节点外的其它类型的节点图十所示的代码片断演示了怎么用上述的两个方法创建一个源XML文档的拷贝有选择的修改某些节点XML树从树根开始被访问但只输出了除属性节点类型以外的其它类型的节点你可以把Reader和Writer整合在一个新的类中设计一个新的接口使它能读写流及访问属性和节点

Figure Using the WriteNode Method

XmlTextReader reader = new XmlTextReader(inputFile);

XmlTextWriter writer = new XmlTextWriter(outputFile);

// 配置 reader 和 writer

writerFormatting = FormattingIndented;

readerMoveToContent();

// Write根节点

writerWriteStartElement(readerLocalName);

// Read and output every other node

int i=;

while(readerRead())

{

if (i % )

writerWriteNode(reader false);

i++;

}

// Close the root

writerWriteEndElement();

// Close reader and writer

writerClose();

readerClose();

我的XmlTextReadWriter类并没有从XmlReader或者XmlWriter类中继承取而代之的是另外两个类一个是基于只读流(stream)的操作类另一个是基于只写流的操作类XmlTextReadWriter类的方法用Reader对象读数据写入到Writer对象为了适应不同的需求内部的Reader和Writer 对象分别通过只读的Reader和Writer属性公开图十一列出了该类的一些方法

Figure XmlTextReadWriter Class Methods

Method

Description

AddAttributeChange

Caches all the information needed to perform a change on

a node attribute All the changes cached through this method are

processed during a successive call to WriteAttributes

Read

Simple wrapper around the internal readers Read method

WriteAttributes

Specialized version of the writers WriteAttributes method

writes out all the attributes for the given node

taking into account all the changes cached through

the AddAttributeChange method

WriteEndDocument

Terminates the current document in the writer and closes

both the reader and the writer

WriteStartDocument

Prepares the internal writer to output the document and add

a default comment text and the standard XML prolog

这个新类有一个Read方法它是对Reader的read方法的一个简单的封装另外它提供了WriterStartDocument和WriteEndDocument方法它们分别初始化/释放(finalize)了内部Reader和writer对象还处理所有I/O操作在循环读节点的同时我们就可以直接的修改节点出于性能的原因要修改属性必须先用AddAttributeChange方法声明对一个节点的属性所作的所有修改都会存放在一个临时的表中最后通过调用WriteAttribute方法提交修改清除临时表

图十二所示的代码演示了客户端用XmlTextReadWriter类在读操作的同时修改属性值的优势在本期的msdn中提供了XmlTextReadWriter类的C#和VB源代码下载(见本文开头提供的链接)

Figure Changing Attribute Values

private void ApplyChanges(string nodeName string attribName

string oldVal string newVal)

{

XmlTextReadWriter rw = new XmlTextReadWriter(InputFileNameText

OutputFileNameText);

rwWriteStartDocument(true CommentTextText);

// 手工修改根节点

rwWriterWriteStartElement(rwReaderLocalName);

// 开始修改属性

// (可以修改更多节点的属性)

rwAddAttributeChange(nodeName attribName oldVal newVal);

// 循环处理文档

while(rwRead())

{

switch(rwNodeType)

{

case XmlNodeTypeElement:

rwWriterWriteStartElement(rwReaderLocalName);

if (nodeName == rwReaderLocalName)

// 修改属性

rwWriteAttributes(nodeName);

else

// deep copy

rwWriterWriteAttributes(rwReader false);

if (rwReaderIsEmptyElement)

rwWriterWriteEndElement();

break;

}

}

// Close the root tag

rwWriterWriteEndElement();

// Close the document and any internal resources

rwWriteEndDocument();

}

XmlTextReadWriter类不仅可以读XML文档也可以写XML文档你可以它来读XML文档的内容如果需要你还可以用它来做一些基本的更新操作基本的更新操作在这里是指修改某个已存在的属性的值或者某个节点的内容又或者是增加一个新的属性或节点对于更复杂的操作最好还是用XMLDOM分析器

总结

Reader和 Framework中处理XML数据的根本它们提供了对所有XML数据访问功能的原始的APIReader像一个新的分析器类它即有XMLDOM的强大又有SAX的快速简单Writer为简单的创建XML文档而设计虽然Reader和Writer都是NET Framework中的一小块但是它们是相互独立的API在本文中我们只讨论了怎么样用Reader和Writer完成一些主要的工作 介绍了验证分析器的原理机制并把Reader和writer整合在一个单独的类中上述所有的这些类都是轻量级的类似于游标式的XMLDOM分析器

               

上一篇:C#如何执行存储过程

下一篇:C#委托的同步调用和异步调用