Chapter 11. Other XML Technologies¶
System.Xml名字空间由下面的名字空间以及核心类组成:
System.Xml.*
- XmlReader与XmlWriter:用于读取或是写入XML流的高性能、前向光标
- XmlDocument:以W3C风格DOM表示的XML文档
System.Xml.XPath:用于XPath的基础结构与API(XPathNavigator),一种用于查询XML的基于字符串的语言
System.Xml.XmlSchema:用于(W3C)XSD Scheme的基础结构与API
System.Xml.Xsl:用于执行XML的XSLT转换的基础结构与API(XslCompiledTransform)
System.Xml.Serialization:支持向或是由XML类的序列化
System.Xml.XLinq:现代、简化的XmlDocument的LINQ版本
W3C是World Wide Web Consortium的缩写,在其中定义了XML标准。
用于分析或是格式化XML字符串的XmlConvert静态类在第6章中进行了讨论。
XmlReader¶
XmlReader是一个以底层前向方式读取XML流的高性能类。
考虑下面的XML文件:
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<customer id="123" status="archived">
<firstname>Jim</firstname>
<lastname>Bo</lastname>
</customer>
要实例化XmlReader,我们调用静态的XmlReader.Create方法,并向其中传递一个Stream,一个TextReader或是一个URI字符串。例如:
using (XmlReader reader = XmlReader.Create (“customer.xml”))
?...
要创建一个由字符串进行读取的XmlReader:
XmlReader reader = XmlReader.Create (
new System.IO.StringReader (myString));
我们也可以传递一个XmlReaderSettings对象来控制分析与验证选项。XmlReaderSettings上的下列三个选项对于略过多余的内容特别有用:
bool IgnoreComments // Skip over comment nodes?
bool IgnoreProcessingInstructions // Skip over processing instructions?
bool IgnoreWhitespace // Skip over whitespace?
在下面的示例中,我们指示读取器不要读取空白,在通常的应用场景中这通常是令人分心的事情:
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
using (XmlReader reader = XmlReader.Create ("customer.xml", settings))
...
XmlReaderSettings中另一个比较有用的属性是ConformanceLevel。Document的默认值指示读取器假定正确的XML文档只有一个根节点。如果我们只是希望读取XML中包含多个节点的其中一部分则会出现问题:
<firstname>Jim</firstname>
<lastname>Bo</lastname>
要读取这段内容而不抛出异常,我们必须将ConformanceLevel设置为Fragment。
XmlReaderSettings同时有一个名为CloseInput的属性,该属性用来指示当读取器关闭时是否关闭底层流(在XmlWriterSettings上有一个类似的CloseOutput)。CloseInput与CloseOutput的默认值为true。
读取节点¶
XML流的组成单位是XML节点。读取器以文本顺序(尝试优先)遍历流。读取器的Depth属性返回光标的当前深度。
由XmlReader读取的最基本方法就是调用Read。他会前进到XML流中的下一个节点,非常类似于IEnumerator中的MoveNext。Read方法的第一次调用会将光标指向第一个节点。当Read返回false时,则意味着光标已经经过最后一个节点,此时XmlReader应该被关闭。
在这个示例中,我们读取XML流中的每一个节点,同时输出每一个节点类型:
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
using (XmlReader reader = XmlReader.Create ("customer.xml", settings))
while (reader.Read())
{
Console.Write (new string (' ',reader.Depth*2)); // Write indentation
Console.WriteLine (reader.NodeType);
}
输出结果如下:
XmlDeclaration
Element
Element
Text
EndElement
Element
Text
EndElement
EndElement
NodeType是XmlNodeType类型,他是下列成员的一个枚举:
None Comment Document
XmlDeclaration Entity DocumentType
Element EndEntity DocumentFragment
EndElement EntityReference Notation
Text ProcessingInstruction Whitespace
Attribute CDATA SignificantWhitespace
XmlReader上的两个string属性提供了对节点内容的访问:Name与Value。依据节点类型,或者Name,或者Value(或同时)会被填充:
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
settings.ProhibitDtd = false; // Must set this to read DTDs
using (XmlReader r = XmlReader.Create ("customer.xml", settings))
while (r.Read())
{
Console.Write (r.NodeType.ToString().PadRight (17, '-'));
Console.Write ("> ".PadRight (r.Depth * 3));
switch (r.NodeType)
{
case XmlNodeType.Element:
case XmlNodeType.EndElement:
Console.WriteLine (r.Name); break;
case XmlNodeType.Text:
case XmlNodeType.CDATA:
case XmlNodeType.Comment:
case XmlNodeType.XmlDeclaration:
Console.WriteLine (r.Value); break;
case XmlNodeType.DocumentType:
Console.WriteLine (r.Name + " - " + r.Value); break;
default: break;
}
}
为了演示,我们扩展我们的XML文件来包含一个文档类型,实体,CDATA以及注释:
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE customer [ <!ENTITY tc "Top Customer"> ]>
<customer id="123" status="archived">
<firstname>Jim</firstname>
<lastname>Bo</lastname>
<quote><![CDATA[C#'s operators include: < > &]]></quote>
<notes>Jim Bo is a &tc;</notes>
<!-- That wasn't so bad! -->
</customer>
实体类似于宏;CDATA类似于C#中的原始字符串(@”...”)。下面是输出结果:
XmlDeclaration---> version="1.0" encoding="utf-8"
DocumentType-----> customer - <!ENTITY tc "Top Customer">
Element----------> customer
Element----------> firstname
Text-------------> Jim
EndElement-------> firstname
Element----------> lastname
Text-------------> Bo
EndElement-------> lastname
Element----------> quote
CDATA------------> C#'s operators include: < > &
EndElement-------> quote
Element----------> notes
Text-------------> Jim Bo is a Top Customer
EndElement-------> notes
Comment----------> That wasn't so bad!
EndElement-------> customer
XmlReader会自动解析实体,所以在我们的示例中,实体引用&tc;扩展为Top Customer。
读取元素¶
通常,我们已经知道我们正在读取的XML文档的结构。有鉴于此,XmlReader提供了一系列的假定特定结构的读取方法。这简化了我们的代码,同时可以执行某些验证。
ReadStartElement验证当前的NodeType为StartElement,然后调用Read。如果我们指定一个名字,他会验证其与当前元素相匹配。
ReadEndElement验证当前的NodeType为EndElement,然后调用Read。
例如要读取下面的XML文档:
<firstname>Jim</firstname>
我们可以使用下面的代码:
reader.ReadStartElement ("firstname");
Console.WriteLine (reader.Value);
reader.ReadEndElement();
ReadElementContentAsString方法一次完成所有的工作。他会读取一个开始元素,一个文本节点以及一个结束元素,并将内容作为字符串返回:
string firstName = reader.ReadElementContentAsString (“firstname”, “”);
第二个参数涉及名字空间,在这个示例中为空。同时这个方法还有其他的版本类型,例如ReadElementContentAsInt,该方法会分析结果。回到我们原始的XML文档:
我们可以使用下面的代码进行读取:
XmlReaderSettings settings = new XmlReaderSettings();
settings.IgnoreWhitespace = true;
using (XmlReader r = XmlReader.Create ("customer.xml", settings))
{
r.MoveToContent(); // Skip over the XML declaration
r.ReadStartElement ("customer");
string firstName = r.ReadElementContentAsString ("firstname", "");
string lastName = r.ReadElementContentAsString ("lastname", "");
decimal creditLimit = r.ReadElementContentAsDecimal ("creditlimit", "");
r.MoveToContent(); // Skip over that pesky comment
r.ReadEndElement(); // Read the closing customer tag
}
可选元素
在前面的示例中,假定是可选的。其解决方案非常直接:
r.ReadStartElement ("customer");
string firstName = r. ReadElementContentAsString ("firstname", "");
string lastName = r.Name == "lastname"
? r.ReadElementContentAsString() : null;
decimal creditLimit = r.ReadElementContentAsDecimal ("creditlimit", "");
随机元素顺序
本节中的示例依赖XML文件中的元素以确定顺序出现。如果我们需要处理任意顺序的元素,最简单的解决方案就是将XML部分读取到一个X-DOM中。我们会在稍后探讨如何实现。
空元素
XmlReader处理空元素的方式存在一个可怕的陷阱。考虑下面的元素:
在XML中,这与下面的写法相同:
然而,XmlReader会对其区别对待。在前一种情况下,下面的代码会按照预期的样子工作:
reader.ReadStartElement ("customerList");
reader.ReadEndElement();
在第二种情况下,ReadEndElement会抛出异常,因为并没有XmlReader所关注的单独的结束元素。解决方法就是检测空元素,如下所示:
bool isEmpty = reader.IsEmptyElement;
reader.ReadStartElement ("customerList");
if (!isEmpty) reader.ReadEndElement();
实际上,这只会出现在当元素也许会包含子元素时的情况。对于包含简单的文本的元素,我们可以通过调用如ReadElementContentAsString这样的方法来避免这些问题。ReadElementXXX方法会正确处理两种空元素类型。
其他的ReadXXX方法
表11-1总结了XmlReader中的所有ReadXXX方法。这些方法中的大多数被设计用来处理元素。以粗体显示的示例XML片段是由所描述的方法来读取的。
csharp_table_11.png
ReadContentAsXXX方法将文本分析为XXX类型。在内部,XmlConver类执行字符串到类型的转换。文本节点可以位于元素或是一个属性中。
ReadElementContentAsXXX方法是相应的ReadContentAsXXX方法的封装器。这些方法应用在元素节点上,而不是元素所包含的文本节点上。
ReadInnerXml通常应用到元素,读取并返回元素及其子节点。当应到属性上时,他会返回属性的值。
ReadOuterXml类似于ReadInnerXml,所不同的是,他会包含而不是排除光标位置处的元素。
ReadSubtree返回一个提供当前元素(及其子元素)视图的代理读取器。在原始的读取器可以被安全的读取之前,代理读取器必须被关闭。当代理读取器被关闭时,原始读取器的光标位置会移动到子树的结束处。
ReadToDescendent将光标移动到具有指定名字/名字空间的第一个子节点的起始处。
ReadToFollwing将光标移动到具有指定名字/名字空间的第一个节点的起始处-无论深度。
ReadToNextSibling将光标移动到具有指定名字/名字空间的第一个兄弟节点的起始处。
ReadString与ReadElementString的行为类似于ReadContentAsString与ReadElementContentAsString,所不同的是,如果在元素内有多个文本节点时前者会抛出异常。通常,应避免使用这些方法,因为如果一个元素包含有注释时这些方法会抛出异常。
读取属性¶
XmlReader为我们提供了通过名字或是位置直接访问元素属性的索引器。使用索引器与调用GetAttribute方法等同。
给定下面的XML片段:
我们可以使用下面的代码读取其属性:
Console.WriteLine (reader ["id"]); // 123
Console.WriteLine (reader ["status"]); // archived
Console.WriteLine (reader ["bogus"] == null); // True
尽管属性顺序在语义是无关的,我们可以通过其顺序位置来访问属性。我们可以使用下面的代码来重写前面的示例:
Console.WriteLine (reader [0]); // 123
Console.WriteLine (reader [1]); // archived
索引器也可以允许我们指定属性的名字空间-如果有。
AttributeCount返回当前节点的属性数目。
属性节点
要显式的遍历属性节点,我们必须由仅是调用Read的普通路径进行转换。这样做的一个原因就在于如果我们要将属性值分析为其他类型,使用ReadContentAsXXX方法。
转换必须由起始元素开始。要使得处理更容易,前向规则在属性遍历中得到舒缓:我们可以通过调用MoveToAttribute跳到任意的属性处。
回到我们前面的示例:
我们可以使用下面的代码进行操作:
reader.MoveToAttribute ("status");
string status = ReadContentAsString();
reader.MoveToAttribute ("id");
int id = ReadContentAsInt();
如果指定的属性不存在,MoveToAttribute会返回false。
我们也可以通过调用MoveToFirstAttribute然后调用MoveToNextAttribute方法顺序遍历所有属性:
if (reader.MoveToFirstAttribute())
do
{
Console.WriteLine (reader.Name + "=" + reader.Value);
}
while (reader.MoveToNextAttribute());
// OUTPUT:
id=123
status=archived
名字空间与前缀¶
XmlReader为引用元素与属性名字提供了两个并行系统:
- Name
- NamespaceURI与LocalName
当我们读取元素的Name属性或是调用接受单个name参数的方法时,我们正在使用第一个系统。如果没有名字空间或是前缀时,这会工作得很好;否则就变成了一种较笨的方式。名字空间会被忽略,而前缀会按其所书写的样子被包含进来。例如:
Sample fragment Name
<customer ...> customer
<customer xmlns='blah' ...> customer
<x:customer ...> x:customer
下面的代码处理前两种情况:
reader.ReadStartElement (“customer”);
要处理第三种情况则需要下面的代码:
reader.ReadStartElement (“x:customer”);
第二个系统通过两个名字空间相关的属性进行工作:NamespaceURI与LocalName。这些属性考虑了前缀以及父元素所定义的默认名字空间。前缀被自动扩展。这意味着Namespace总是为当前的元素反映语义上正确的名字空间,而LocalName总是与前缀无关。
当我们向如ReadStartElement这样的方法传递两个命名参数时,则我们在使用相同的系统。例如,考虑下面的XML:
<customer xmlns="DefaultNamespace" xmlns:other="OtherNamespace">
<address>
<other:city>
...
我们可以使用下面的代码进行读取:
reader.ReadStartElement ("customer", "DefaultNamespace");
reader.ReadStartElement ("address", "DefaultNamespace");
reader.ReadStartElement ("city", "OtherNamespace");
如果需要,我们可以通过Prefix属性来查看我们所使用的前缀并通过调用LookupNamespace将其转换为名字空间。