2009/9/22

[XML] XML入門系列 (3) : 巡覽 XML 文件

在本節中我將介紹對於 XML 資料最重要也最常用的技巧, 也就是所謂的巡覽 (navigation)。我所說的巡覽至少包括兩個部份, 一是定位/搜尋想要找的節點和資料, 二是在樹狀結構資料中以一個節點一個節點的方式向前或向後巡迴停駐。

在本文中, 我仍將採用在上文中所建立的簡單 XML 資料以做為範例:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Employees>
  <Employee Department="研發部">
    <Name>吳大寶</Name>
  </Employee>
  <Employee Department="總務部">
    <Name>鄭小胖</Name>
  </Employee>
</Employees>

以樹狀結構來講, 如果我們要從最外圍的根節點由上而下巡覽過每一個子節點, 像這種資料結構最適合以遞迴 (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 + "<br />");
    else // 如果是一般節點則另外處理
    {
        Response.Write("&lt;" + node.Name);
        if (node.Attributes != null) // 如果是 attribute 則逐個印出
            foreach (XmlAttribute attr in node.Attributes)
                Response.Write(" " + attr.Name + " = \"" + attr.Value+"\"");
        Response.Write("&gt;<br />");
        foreach (XmlNode child in node.ChildNodes) // 如果還有子節點則繼續處理
            navigate(child);
        Response.Write("&lt;/" + node.Name + "&gt;<br />");
    }
}

以上這個程式執行後, 即可將整個 XML 從頭到尾巡覽一遍並將讀取的結果列印出來:

<Employees>
<Employee Department = "研發部">
<Name>
吳大寶
</Name>
</Employee>
<Employee Department = "總務部">
<Name>
鄭小胖
</Name>
</Employee>
</Employees>

在上面的程式中, 如果你把所有的 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 + "<br />");

以下就是這個程式的輸出結果:

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() + "<br />");
    if (node is XDocumentType)
        Response.Write("XDocumentType: " + ((XDocumentType)node).ToString() + "<br />");
    if (node is XText)
        Response.Write("XText: " + ((XText)node).Value + "<br />");
    if (node is XElement)
        Response.Write("XElement: " + ((XElement)node).Value + "<br />");
    if (node is XContainer)
        Response.Write("XContainer: " + ((XContainer)node).ToString() + "<br />");
    if (node is XComment)
        Response.Write("XComment: " + ((XComment)node).Value + "<br />");
}

上面這個範例純粹只是為了示範如何判斷各個子節點的型別而已。在實際情況中, 我們幾乎只會取出 XElement 與 XText 兩個型別的物件的子節點或值。此外, XContainer (繼承自 XNode)、XNode 以及 XNode 所繼承的 XObject 都是抽象類別 (在 C# 稱為 abstract, VB 中稱為 MustInherit), 所以不能以實體副本 (instance) 方式建立或直接引用。

在右圖中, 我把這些物件的繼承關係畫出來給大家參考。各位可以看到, 就是因為這些物件之間多半是繼承得來的, 所以在上一個範例當中, 我們才可以直接以 is 關鍵字來判斷型別, 並且直接以 (XElement) 這種方法 (或 VB 中的 CType) 來做型別之間的轉換。

如果我們已經知道 XML 文件中各元素的結構的話 (在實際應用中, 我們多半會針對已知結構的 XML 進行處理), 我們可以直接使用節點的名稱以取得該節點, 如以下範例所示:

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

如果你的 XML 文件裡面有加上 namespace 的 話, 以上這個程式需要稍為變更一下:

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

為求簡單, 我在以後的範例中會假設一律不使用 namespace。

沒有留言:

張貼留言