经常需要处理文本文件,命名空间 System.IO 提供的类使得这种任务更容易,但是使用这些类不能处理和操作 XML 格式的结构化文本。XML 指的是可扩展标记语言(Extensible Markup Language),这是一种简单而灵活的文本格式,使用它可以独立于平台的方式交换数据。
将XML用作数据交换格式很常见,不仅.NET Framework这样做,其他Microsoft产品也如此。.NET Framework 将其用于通过 SOAP 提供的 Web 服务和 WCF(Windows Communication Foundation),将其用作WPF(Windows Presentation Foundation)和XAML (Silverlight Extensible Application Markup Language,Silverlight可扩展应用程序标记语言)文件的格式,将其用作Windows WF(Workflow Foundation)文件的格式;它还是ADO.NET的组成部分。
虽然 XML 是基于文本的,并且对人来说很容易理解,但是必须能够通过编程操作XML。这是使用XML分析器完成的。.NET Framework提供了两个XML分析器:一个是基于流的,可即时读取XML流;另一个是基于树的,它必须将整个流读入内存后才能构建树。
一、XML DOM
要以编程方式读取和操作XML文档,就必须在内存中使用XML文档对象模型(Document Object Model,DOM)表示它。DOM提供了一种在内存中表示XML数据的结构化方式,常用于将XML数据读入内存,以调整其结构、添加或删除元素以及修改元素包含的数据。
举个栗子:
<books>
<book>
<title>note</title>
<isbn-10>010</isbn-10>
<author>zz</author>
<price currency="EU">250.00</price>
<publisher>
<name>xml</name>
<state>china</state>
</publisher>
</book>
</books>
二、LiNQ to XML
LINQ to XML通过LINQ扩展方法暴露了XML DOM,能够操作和查询已加载到内存中的XML文档。使用LINQ to XML创建和操作XML文档时,需要的所有类都包含在命名空间System.Xml.Linq中。
下图列出了最常用的类:
2.1 XDocument
XDocument 类表示 XML 文档实例。除非需要指定文档类型声明、处理指令(供 XML分析器使用)或顶级注释,否则很少使用XDocument实例,而应使用XElement类。
2.2 XElement和XAttribute
XElement类表示XML元素,是最常用的类之一,它提供了很多很有用的方法和属性,可用于创建、修改和查询XML数据。XML属性(attribute)是与XML元素相关联的名称-值对,用 XAttribute 类表示。不同于元素,属性不是 XML 树中的节点。由于属性是与元素相关联的名称-值对,因此其名称在相应的元素中必须是唯一的。
XElement实例包含一系列元素的属性。在XAttribute类中,最常用的属性是NextAttribute和PreviousAttribute,它们对浏览元素的属性序列很有帮助。
如下示例使用LINQ to XML创建XML文档
XElement document = new XElement("books",
new XElement("book",
new XElement("title","note"),
new XElement("isbn-10","010"),
new XElement("author","zz"),
new XElement("price", new XAttribute("currency", "EU"),250),
new XElement("publisher",
new XElement("name","xml"),
new XElement("state","china"))));
ps:SetElementValue和SetAttributeValue
创建XML时,并非只能使用如上所示的构造函数语法,也可使用XElement类提供的方法SetElementValue和SetAttributeValue。
这些方法让您能够使用名称-值对设置元素和属性,从而添加、修改或删除它们。如果指定的元素或属性名不存在,就创建它;否则,将其值修改为指定的值。如果值为null,就删除指定的元素或属性。修改或删除时,将修改或删除第一个与指定名称匹配的元素或属性。
如下示例与上一个示例创建的 XML 相同,但使用的是方法SetElementValue和SetAttributeValue:
XElement document = new XElement("books",
new XElement("book",
new XElement("publisher")));
bookElement.SetElementValue("title","note");
bookElement.SetElementValue("isbn-10","010");
bookElement.SetElementValue("author","zz");
bookElement.SetElementValue("price", 250);
bookElement.Element("price").SetAttributeValue("currency", "EU");
bookElement.SetElementValue("name","xml");
bookElement.SetElementValue("state","china”);
ps:XML字符编码
对于包含非法XML字符的文本,XElement和XAttribute类自动处理其编码和解码。对于下面的语句:
XElement comments = new XElement("comments",
"This line contains special characters <node> & </node>");
将自动编码成如下语句:
<comments>This line contains special characters <node>
&
</node></comments>
检索值时,将自动进行解码
2.3 XName和XNamespace
XML名称指的是XML文档中元素或属性的名称,由两部分组成:XML命名空间和本地名称。XML命名空间能够确保元素和属性的名称是唯一的,以免XML文档的不同部分发生冲突。声明XML命名空间后,可选择仅在该命名空间内唯一的本地名称。
使用XML命名空间时,可利用XML前缀来创建XML命名空间的缩写。虽然XML前缀提高了XML文档的可读性,但是增加了复杂度,因为其含义依赖于上下文。.NET Framework提供了Xnamespace,用于表示XML命名空间。
XName类表示本地名称。在LINQ to XML中,每当需要XML名称时,都使用XName。所幸的是,XName提供了将string隐式转换为XName的功能,因此很少直接使用XName。每个XName都包含一个XNamespace,如果元素不在任何命名空间内,那么其XNamespace为XNamespace.none。
ps:XML命名空间
声明XML命名空间的语法与声明XML属性的语法相同,因此它们常被视为属性,虽然它们不是属性。在XML树中,LINQ to XML将命名空间表示为属性,以简化编程接口。要判断属性实际上是不是命名空间声明,可使用IsNamespaceDeclaration属性。
如下示例使用LINQ to XML创建包含命名空间的XML:
XNamespace ns = "http://www.w3.org/1999/xhtml";
XElement document = new XElement(ns + "books",
new XElement(ns + "book",
new XElement(ns + "title","note"),
new XElement(ns + "isbn-10","010"),
new XElement(ns + "author","zz"),
new XElement(ns + "price",new XAttribute("currency", "EU") 250),
new XElement(ns + "publisher",
new XElement(ns + "name","xml"),
new XElement(ns + "state","china"))));
这将生成如下一个示例所示的XML,
包含命名空间的XML文档:
<books xmlns="http://www.w3.org/1999/xhtml">
<book>
<title>note</title>
<isbn-10>010</isbn-10>
<author>zz</author>
<price currency="EU">250.00</price>
<publisher>
<name>xml</name>
<state>china</state>
</publisher>
</book>
</books>
虽然LINQ to XML类自动处理命名空间声明,但是有时需要提供命名空间前缀,以控制如何在 XML 数据中表示命名空间。为此,可显式地指定表示命名空间的前缀,方法是添加一个xmlns属性,如下所示:
XNamespace ns = "http://www.w3.org/1999/xhtml";
XElement document = new XElement(ns + "books",
new XElement(ns + "book", new XElement(ns + "title","note"),
new XElement(ns + "isbn-10","010"),
new XElement(ns + "author","zz"),
new XElement(ns + "price",new XAttribute("currency", "EU") 250),
new XElement(ns + "publisher",
new XElement(ns + "name","xml"),
new XElement(ns + "state","china"))));
生成如下示例的XML:
<ns:books xmlns:ns="http://www.w3.org/1999/xhtml">
<ns:book>
<ns:title>note</ns:title>
<ns:isbn-10>010</ns:isbn-10>
<ns:author>zz</ns:author>
<ns:price currency="EU">250.00</ns:price>
<ns:publisher>
<ns:name>xml</ns:name>
<ns:state>china</ns:state>
</ns:publisher>
</ns:book>
</ns:books>
处理使用命名空间的文档时,通常通过URI而不是命名空间前缀来引用命名空间。这让您能够使用全限定名(也叫扩展名称,expanded name),其格式如下:
{namespacename}name
ps:原子化
XNamespace对象被原子化(atomized),这意味着如果两个对象的URI相同,它们就将共享一个实例。虽然创建XElement或XAttribute实例时可以使用扩展名称,但是这样做可能影响性能。每当遇到包含扩展名称的字符串时,都必须对其进行分析,以找出原子化的命名空间和名称。
三、选择和查询XML
通过XElement实例将XML文档载入内存后,几乎总是需要选择或查询信息。从XNode派生而来的所有类都提供了这样的方法和属性,即可用于直接导航到XML树中特定的节点。
属性 FirstNode 和 LastNode 分别返回第一个和最后一个子节点;属性 NextNode 和PreviousNode让您能够在节点集合中向前和向后移动;而属性Parent让您能够直接导航到父节点。
如下示例演示了使用XElement的导航属性:
XElement document = new XElement("books",
new XElement("title","note");
new XElement("isbn-10","010");
new XElement("author","zz");
new XElement("price",new XAttribute("currency", "EU"), 250));
Console.WriteLine(document.LastNode);
Console.WriteLine(document.FirstNode);
Console.WriteLine(document.LastNode.Parent);
Console.WriteLine(document.LastNode.PreviousNode);
Console.WriteLine(document.FirstNode.NextNode);
XElement还提供了属性FirstAttribute和LastAttribute,它们分别返回元素的第一个和最后一个属性。如果元素没有属性,它们就都返回 null;如果元素只有一个属性,它们返回的值就相同。检索第一个或最后一个属性后,可使用NextAttribute和PreviousAttribute在属性集合中向前和向后移动。
虽然这些属性很方便,但是没有提供太高的灵活性。如果将XML树中的每个节点都视为一个节点序列,就可使用LINQ查询,这是因为节点集合是一个IEnumerable<T>实例。
如下程序演示了如何对上述示例创建的XElement执行简单的LINQ查询:
foreach (var o in document.Elements().Where(e => (string)e.Element("author") == "zz"))
{
Console.WriteLine(o);
}
如果没有指定名称,就将返回所有的子元素;否则,只返回该名称指定的子元素。
ps:LINQ to XML查询和XPath查询
如果使用命名空间 System.XML中的传统XML DOM类,就必须使用XPath查询来选择节点集合或单个节点。在LINQ to XML中,不再需要这样做,而由命名空间System.Xml.XPath中的一组扩展方法提供查询支持。
扩展方法如下:
方法 | 描述 |
---|---|
CreateNavigator | 创建一个对应于XNode的XPathNavigator。 |
XPathEvaluate | 计算XPath表达式,返回一个包含表达式结果的对象。 |
XPathSelectElement | 使用XPath表达式选择XElement。 |
XPathSelectElements | 使用XPath表达式选择一个元素集合。 |
给方法Where传递了一个Lambda表达式,它使得结果序列只包含这样的元素:包含子元素author,且该子元素的值为字符串Dorman。方法Element返回第一个与指定名称匹配的XElement。
选择属性也一样容易,但是使用方法Attributes和Attribute。方法Attribute返回一个属性,其名称与指定的名称相同。如果没有匹配的属性,就返回 null。方法 Attributes 返回一个IEnumerable<XAttribute>,其中包含当前XElement的属性。将名称作为参数传递给Attributes时,如果没有找到匹配的属性,就返回一个空集合;否则,将返回包含一个属性的集合,因为在每个元素中,属性的名称必须是唯一的。
四、修改XML
虽然创建和选择XML很重要,但是修改XML也同样重要。通过使用XNode及其派生类提供的方法,这种任务很容易完成。修改 XML 时,导航到目标节点的技术将影响修改发生的时间。如果使用前一节开头介绍的属性(如FirstNode或LastNode),修改将在调用方法时进行。删除或替换一个节点,修改结果就将立刻在内存中的 XML 树中反映出来。如果使用的是 XML 查询,调用的修改方法就将在查询结果被遍历时执行,这与 LINQ 查询的默认行为(推迟执行)一致。可使用方法SetElementValue和SetAttributeValue来添加元素或属性、删除元素或属性以及修改元素或属性的值。还可使用方法SetValue来修改当前元素或属性的值,如下程序使用了SetValue来修改元素price的内容:
XElement books = XElement.Load("books.xml");
XElement book = books.Elements("book").FirstOrDefault(b => (string)b.Element("author") == "zz");
book.Element("price").SetValue(25);
替换数据也很简单,但是使用方法 ReplaceAll、ReplaceAttributes、ReplaceNodes 或ReplaceWith。方法 ReplaceAll 替换当前元素的所有子节点和属性,而方法 ReplaceAttributes和ReplaceNodes分别替换所有属性和所有子节点。
ps:ReplaceWith和子节点
ReplaceWith 只用指定的元素替换当前元素。如果要替换的元素有子节点,就不会自动将这些子节点包含在新元素中。
ReplaceWith只用指定的元素替换当前元素。如下程序使用ReplaceWith将元素price替换为新元素:
XElement books = XElement.Load("books.xml");
XElement book = books.Elements("book").FirstOrDefault(b => (string)b.Element("author") == "zz");
book.Element("price").ReplaceWith(25);
要删除当前元素或属性,可使用方法 Remove;要删除当前元素的所有属性,可使用方法 RemoveAttributes;要删除所有的子节点,可使用 RemoveNodes;要删除当前元素的所有子节点和属性,可使用方法RemoveAll。如下所示:
XElement books = XElement.Load("books.xml");
books.Elements("book").FirstOrDefault(b => (string)b.Element("author") == "zz").Remove();
最后,要添加新元素,可使用方法 Add、AddAfterSelf、AddBeforeSelf 或 AddFirst。方法Add将指定的内容作为子节点加入到当前元素中,而AddFirst将指定的内容作为第一个子节点。方法AddAfterSelf和AddBeforeSelf分别将指定的内容作为兄弟节点加入到当前节点后面和前面。如下所示:
XElement books = XElement.Load("books.xml");
XElement book = books.Elements("book").FirstOrDefault(b => (string)b.Element("author") == "zz");
book.Add(new XElement("season","winter”));