[入门][XML] XML入门系列 (3) : 巡览 XML 文档

在本节中我将介绍对于 XML 数据最重要也最常用的技巧, 也就是所谓的巡览 (navigation)。我所说的巡览至少包括两个部分, 一是定位/搜寻想要找的节点和数据, 二是在树状结构数据中以一个节点一个节点的方式向前或向后巡回停驻...


在本节中我将介绍对于 XML 数据最重要也最常用的技巧, 也就是所谓的巡览 (navigation)。我所说的巡览至少包括两个部分, 一是定位/搜寻想要找的节点和数据, 二是在树状结构数据中以一个节点一个节点的方式向前或向后巡回停驻。

逐点巡览

在本文中, 我仍将采用在上文中所建立的简单 XML 数据以做为范例:




   吴大宝


   郑小胖

以树状结构来讲, 如果我们要从最外围的根节点由上而下巡览过每一个子节点, 像这种数据结构最适合以递归 (recursive) 的方式来进行了。所谓的递归, 其典型的做法就是一个函数会不停的调用自己, 并以下一层的子项目作为参数, 直到探巡到最底层或没有东西为止。如果你学过数据结构的话, 这种做法应该是再熟悉不过了, 但是对于非电脑本科系的人来讲, 你可能必须花一点脑筋去思考它的逻辑。不过还好, 我在这里所举的范例都非常的简单, 而且树状结构的深度很浅, 你只需要顺着程序的流程走过一趟, 一定可以搞懂的。

以下是这个递归程序的列表:

using System.Xml;
...
XmlDocument xdoc = new XmlDocument();
xdoc.Load(MapPath("~/App_Data/Test02.xml"));
navigate(xdoc.DocumentElement); // 传入根节点
...
protected void navigate(XmlNode node)
{
   if (node.NodeType==XmlNodeType.Text) // 如果是文字节点则直接印出
       Response.Write(node.InnerText + "
");
   else // 如果是一般节点则另外处理
   {
       Response.Write("<" + node.Name);
       if (node.Attributes != null) // 如果是 attribute 则逐个印出
           foreach (XmlAttribute attr in node.Attributes)
               Response.Write(" " + attr.Name + " = "" + attr.Value+""");
       Response.Write(">
");
       foreach (XmlNode child in node.ChildNodes) // 如果还有子节点则继续处理
           navigate(child);
       Response.Write("</" + node.Name + ">
");
   }
}

以上这个程序执行后, 即可将整个 XML 从头到尾巡览一遍并将读取的结果打印出来:




吴大宝




郑小胖


在上面的程序中, 如果你把所有的 Response.Write 叙述先拿掉不看的话, 剩下的就是核心的部分, 而其中最关键的就在以下这段:

foreach (XmlNode child in node.ChildNodes) // 如果还有子节点则继续处理
   navigate(child);

使用下层子节点做为参数去调用自己, 这就是这个递归程序的运行逻辑。如果你去追踪程序走向的话, 你会发现它真的就是从根节点为起点, 一个一个巡览子节点, 再回到上一层的下一个节点, 直到所有节点都被探访过为止。由于这个范例很简短, 你应该很容易掌握这个逻辑。

相对于 XmlDocument 对象的巡览方式, LINQ 的巡览方式非常的不一样。若使用 LINQ, 你根本不必写什么递归程序 (也很难写得出来; 我试过了), 重点是根本没有需要。LINQ 的主要价值, 在于它提供了对不同数据来源 (包括 Database、文字数据、XML 与 Object 等等) 的相同的数据筛选方式。对 LINQ to XML 而言, 你不需使用递归程序就能简单的以 XDocument.Descendants 或 XElement.Descendants 来把根节点和所有子节点一次通通列出来。以下我们来看看如何列出范例 XML 文档里面所有的数据:

using System.Xml.Linq;
...

XDocument xdoc = XDocument.Load(MapPath("~/App_Data/Test02.xml"));
var eles = from ele in xdoc.Descendants() where ele.Name.LocalName == "Employee"
          select new {
              att = (string)ele.Attribute("Department"),
              name = (string)ele.Element("Name") };
foreach (var ele in eles)
   Response.Write("Department = " + ele.att + ", Name = " + ele.name + "
");

以下就是这个程序的输出结果:

Department = 研发部, Name = 吴大宝
Department = 总务部, Name = 郑小胖

这个 LINQ 程序的逻辑和我们在上面使用 XmlDocument 的程序的逻辑当然是不一样的。不过, 我们如果对一个 XML 文档进行巡览, 我们不就是为了捞出当中有用的数据吗? 在实际应用中, 我们对于 XML 数据的处理多半就是如此进行的。

在上一个范例中, 我们使用 XDocument.Descendants() 或 XElement.Descendants() 来取得 XML 文档或某一节点的所有子节点, 我们可以使用 XNode 来枚举:

foreach (XNode node in xdoc.Descendants())
{
   if (node is XDocument)
       Response.Write("XDocument: " + ((XDocument)node).ToString() + "
");
   if (node is XDocumentType)
       Response.Write("XDocumentType: " + ((XDocumentType)node).ToString() + "
");
   if (node is XText)
       Response.Write("XText: " + ((XText)node).Value + "
");
   if (node is XElement)
       Response.Write("XElement: " + ((XElement)node).Value + "
");
   if (node is XContainer)
       Response.Write("XContainer: " + ((XContainer)node).ToString() + "
");
   if (node is XComment)
       Response.Write("XComment: " + ((XComment)node).Value + "
");
}

上面这个范例纯粹只是为了示范如何判断各个子节点的类型而已。在实际情况中, 我们几乎只会取出 XElement 与 XText 两个类型的对象的子节点或值。此外, XContainer (继承自 XNode)、XNode 以及 XNode 所继承的 XObject 都是抽象类 (在 C# 称为 abstract, VB 中称为 MustInherit), 所以不能以实例副本 (instance) 方式建立或直接引用。

由于这些对象之间多半是继承得来的, 所以在上一个范例当中, 我们才可以直接以 is 关键字来判断类型, 并且直接以 (XElement) 这种方法 (或 VB 中的 CType) 来做类型之间的转换。

如果我们已经知道 XML 文档中各元素的结构的话 (在实际应用中, 我们多半会针对已知结构的 XML 进行处理), 我们可以直接使用节点的名称以取得该节点, 如以下范例所示:

IEnumerable eles = xdoc.Element("Employees").Elements("Employee");
foreach (XElement ele in eles)
   Response.Write(ele.Name + " = " + ele.Value + "
");

如果你的 XML 文档里面有加上 namespace 的 话, 以上这个程序需要稍为变更一下:

XNamespace xns = "http://phone.idv.tw/cs2/";
IEnumerable eles = xdoc.Element(xns + "Employees").Elements(xns + "Employee");
foreach (XElement ele in eles)
   Response.Write(ele.Name.LocalName + " = " + ele.Value + "
");

为求简单, 我在以后的范例中会假设一律都不使用 namespace。

XPath 巡览

我们曾经在“[入门文章][XML] XML入门系列 (1) : XML 初论 ”一文中提到过 XPath。如果你的 XML 内容很复杂, 那么要从里面捞出想要的数据, 使用 XPath 描述式来进行筛选, 那是最方便的了。

在 .NET 程序中对 XML 内容以 XPath 描述式来进行筛选, 有好几个方法可以办到。我们仍旧使用本文最上方的那个 XML 文件作为内容范例。假设我们要找到 这个节点所包含的所有子节点, 那么, 考量到这个 XML 的架构, 我们可以设定 XPath 内容为 /Employees/Employee[@Department="总务部"]。而程序则可以这样写:

using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
...
List list = new List();
XPathNavigator xnav = xdoc.CreateNavigator();
XDocument xdoc = XDocument.Load(schemaFile);            
XPathExpression xpxp = xnav.Compile("/Employees/Employee[@Department="总务部"]/Name");
XPathNodeIterator it = xnav.Select(xpxp);
while (it.MoveNext())
   list.Add(it.Current.Value);

如果不使用 XPathNavigator, 我们也可以直接透过 XDocument.XPathSelectElements() 方法来进行筛选动作:

using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
...
List list = new List();
XPathNavigator xnav = xdoc.CreateNavigator();
XDocument xdoc = XDocument.Load(schemaFile);            
IEnumerable eles =
   xdoc.XPathSelectElements("/Employees/Employee[@Department="总务部"]/Name");
foreach (var ele in eles)
   list.Add(ele.Value); 

XPath 的详细语法可以参考 MSDN。

相关文章:

  • [入门][XML] XML入门系列 (1) : XML 初论
  • [入门][XML] XML入门系列 (2) : 以动态方式建立或产生 XML 文档
  • [入门][XML] XML入门系列 (3) : 巡览 XML 文档


Dev 2Share @ 点部落