2009/9/1

[入門][XML] XML入門系列 (1) : XML 初論

XML 在 .Net 中大概算是最重要而基本的技術之一。我將花一點時間從頭將 XML 以白話解釋一遍, 再把讀寫 XML 資料的相關技巧整理成一系列文章。

首先, 我們先來看一個完整且合法的 XML 文件:

<?xml version="1.0" encoding="utf-8"?>
<rss version="1.0"> 
    <channel>
        <title>Johnny Worker's Blog</title>
        <link>http://feeds.feedburner.com/jworker</link>
        <description>
            Johnny is a Microsoft MVP who's familiar with ASP.NET / VB.NET and C#.
        </description>
        <language>en</language>
        <image>
            <link>http://feeds.feedburner.com/jworker</link>
            <url>http://s.blog.xuite.net/_image/logo_blog.gif</url>
            <title>HiNet Blog</title>
        </image>
        <item> 
            <title>[AJAX] 使用 PageMethods 從 JavaScript 中直接呼叫 Server 端函數</title>             
            <link>http://feeds.feedburner.com/~r/Jworker/~3/375404303/19043851</link>
            <pubDate>Tue, 26 Aug 2008 10:57:52 -0500</pubDate>
            <guid isPermaLink="false">http://blog.xuite.net/johnnyle/worker/19043851</guid>
            <description>
                如果我們在 ScriptManager 中設定 EnablePageMethods 屬性, 
                而且在 Server 端某個方法上...
            </description>
        </item>
        <item> 
            <title>MSDN 的 Transfer Manager 哪裡找?</title>
            <link>http://feeds.feedburner.com/~r/Jworker/~3/369845030/18764090</link>
            <pubDate>Wed, 20 Aug 2008 04:43:22 -0500</pubDate>
            <guid isPermaLink="false">http://blog.xuite.net/johnnyle/worker/19043851</guid>
            <description>
                如果你是 MSDN 的訂閱者, 你一定知道 MSDN 的下載一律是透過  
                Microsoft File Transfer Manager 進行的...
            </description>
        </item>
        <item>
                ..... (省略)
    </channel>
</rss>

XML 基本規則

上一個範例是我從我的個人部落格中隨便截取下來的一部份, 它本身就是一個標準的 XML 文件, 是由 Feed Burner 自動產生的 RSS Feed (為求簡捷, 部份內容我已手動調整過)。現在我們一邊對照這個範例, 一邊來看看 XML 的基本組成規則:

  • 任何 XML 文件都必須有 (也只能有) 一個根元件 (root)。
  • 任何元素 (element) 都需要有一個封閉的標籤 (例如 <rss>..</rss> 或 <ABC />)。
  • 大小寫有區別。
  • 任何元素都必須以正確的巢狀結構排列。
  • Attributes 必須包在元素的標籤範圍內, 其值必須使用單引號或雙引號圈住 (例如 <rss version="1.0"> 中的 version)。 

所有的元素都必須以起始標籤 (start tag) 與結束標籤 (end tag) 包起來, 例如 <rss>..</rss>, 前者稱為起始標籤, 後者稱為結束標籤。但如果元素中沒有子元素的話, 它可以直接封閉, 使用如 <rss /> 的標示法。

如果你熟悉 HTML 標籤語法的話, 相信你一定很容易理解以上幾個規則, 因為 HTML 標籤就是依照類似的規則組成的; 不過一般瀏覽器對於 HTML 標籤的組成規則並不會嚴格檢查。在 ASP.NET 中, 我們最常接觸的 web.config 與 web.sitemap 也都是標準的 XML 文件 (這兩個文件就必須嚴格遵照規則了)。

在大多數情況下, 請不要把 HTML 文件當作平常的 XML 文件。首先, 它通常不區分大小寫, 所以內容如果寫成 <html>...</HTML> 可能不會引發錯誤。其次, 我們時常看到有起始標籤卻沒有結束標籤的情況, 特別是出現在表格裡面, 例如

<table>
    <tr>
        <td>
        <td>
    <tr>
        ....
</table>

像上面這樣明顯違反 XML 編寫規則的 HTML 文件, 卻可以在許多瀏覽器中正確顯示。

接著, HTML 文件可以在某些地方違反巢狀套疊的規則, 看看以下的例子就明白了:

這段文字含有<b>粗體字以及<i>斜體字</b>, 請注意它們的標籤標示方法</i>。

現在看看它顯示出來的效果:

這段文字含有粗體字以及斜體字, 請注意它們的標籤標示方法

從上面兩個例子中, 我們不難得到一個結論, 那就是千萬不要把 HTML 文件當作普通的 XML 文件來分析或處理, 否則會遇到許多莫名其妙的錯誤。

如果你在 Visual Studio 中建立或編輯 XML 文件的話, 由於它提供即時的除錯及輔助能力, 所以你並不會有太多犯錯的機會 (除非你刻意那麼做)。但如果你使用 NotePad 或其它工具的話, 你最好還是多少記得並且嚴格遵守 XML 的建立規則。

命名規則

XML 文件中其實並沒有什麼太過於嚴格的命名規則。而且由於它並沒有任何保留字 (除了 "xml" 這個字以外), 所以並不會限制你為任何元素或 attribute 取什麼名字。以下是幾項最基本的命名規則:

  • 可以是任何中文或英數字元, 唯獨不能以數字或標點符號字元起頭。
  • 名稱裡不能有空格字元。
  • 名稱不能使用 "xml" 做為開頭, 因為可能與 XML 文件定義混淆。
  • 元素名稱中不能使用 ":" (冒號) 字元。 

上面的規則中只限制你不能使用冒號和空格來取名, 但為求保險, 我建議你乾脆不要使用任何符號, 以免未來在讀取或處理資料時引發無謂的問題。如果真的有需要, 最多只使用 "_" (底線) 就好了。此外, 我也不建議使用中文或任何26個英文字母以外的語言來取名, 以免未來造成資料交換的問題。

不過, 或許你有機會看到像 <d:PartitionKey>2010-01-0100:00:00</d:PartitionKey> 之類的標示 (尤其在 ATOM 格式中)。上面不是說名稱裡不能出現冒號嗎? 那麼這裡的冒號是什麼意思?

這是 XML 中命名空間 (name space) 的精簡表示方式。出現在冒號左邊的文字稱為命名空間前置, 在冒號後面的文字稱為區域名稱 (local name); 當這三項元素寫在一起 (就如同上例中的 "d:PartitionKey", 它就稱為全域名稱 (universal name)。在這裡, 無論是命名字間前置, 或者區域名稱, 都必須嚴格遵守上面提到的命名原則。

至於何謂命名空間, 我們在以下章節中會再說明。

表格狀文件與階層狀文件

如果從某一種角度來思考, 我們大致上可以把資料區分成兩大類:

  • 表格狀 (Tabular) 樣式
  • 階層狀 (Hierarchical) 樣式

所謂的表格狀, 指的就是如同表格般整齊排列的樣子 (例如以 GridView 所呈現的資料)。像這種資料一律是以方塊狀排列的, 也就是有橫列 (row) 與縱行 (column) 交互排列。而所謂的階層狀, 有時也稱為樹狀 (tree-like), 則不一定是四四方方的; 相反的, 它可能排列得像一棵樹, 或更正確的講, 像是植物的根。

如果你還是不了解到底階層狀資料到底是什麼東西的話, 那麼你不妨參考一下 MSDN 網站左側的 TreeView, 就是最典型的階層式資料, 或者稱為樹狀資料。此外, 心智圖也是很典型的例子。

對於表格狀資料, 我們通常可以使用表格、資料庫、試算表等工具來儲存及展示。那麼, 對於階層式資料呢? 我想你應該猜想得到, XML 自然是用來儲存該種資料的絕佳格式了。

不過表格狀資料和階層式資料並不完全是互斥的。如果把階層式資料完全攤平之後, 我們就能夠把它轉換成表格狀資料。至於階層式資料, 如果我們把空白的格子消掉, 剩下的資料也有機會轉換成階層式資料。不過一般來講, 階層式資料是一定可以轉換成表格式資料的, 反之卻不一定。

但不管如何, XML 絕對是最適合用來表示階層式資料的結構。如同你在右圖中看到的, 配合 TreeView 控制項, XML 可以完美的表示樹狀資料的結構, 而這並不是表格式資料所能取代的。

不過, 話說回來, XML 也很適合用來表示表格狀資料結構, 但是, 如果資料不是「廋高」型 (亦即階層不多) 而是「寬胖」型 (亦即階層很多) 的話, 使用 XML 表現就不太容易閱讀而且效率很差。但不管資料是廋高或寬胖型, 如果使用 XML 來儲存表格式資料, 其效率都遠遠不及資料庫。這就是為什麼我們時常聽到 XML 被拿來當作小量資料「交換」的媒介, 卻鮮少聽說 XML 被拿來做為大量資料「儲存」的媒介。

元素 (Elements)

XML 文件是由元素為單位而組成的。前面已經提到, 凡是以起始標籤 (start tag) 與終止標籤 (end tag) 所包圍起來的 (像是 <employee> John </employee>) 的資訊單位就稱為「元素」 (element)。一個元素裡面還可以再包含子元素 (child), 而子元素的上一層稱為父元素 (parent); 最上層的父元素就叫做「根元素」 (root)。整個 XML 文件裡面, 根元素只能有一個。

Attributes

放置在元素標籤裡面, 用以描述元素特性的, 稱為 attribute。我不想翻譯這個英文字, 因為「屬性」這個翻譯一般已經給 property 用掉了, 而「特性」或「特色」通常又用來翻譯 feature。為免混淆, 我特別保留這個英文字不予翻譯。

如果你看到某個 XML 元素長得像 <employee job="Administrator" salary="30000">John</employee> 那麼這個元素的名稱 (name) 叫做 employee, 它的值 value 是 John, 而它有兩個 attributes, 第一個 attribute 的名稱是 job, 值是 Administrator, 第二個 attribute 的名稱是 salary, 值是 30000。

如果你在看到這篇文章之前並不曉得上面這一段, 那麼你值得花點時間把它搞得很清楚, 因為未來在處理 XML 文件時, 這幾個名稱將會重複出現。

Element 還是 Attribute?

這可以算是一個 To be or not to be 的問題。不管是元素還是 attribute, 我們都可以拿來描述一個結構。如果我們把上一個範例中的 XML 元素改寫如下, 也沒什麼不可以:

<employee>
    <name>John</name>
    <job>Administrator</job>
    <salary>30000</salary>
</employee>

事實上不管使用哪一種方式, 我們都可以輕鬆的取出或寫入所要的值。那麼, 到底哪種寫法比較好呢?

依我的看法, 我們可以直接參考 HTML 的做法, 看哪些東西可以放進 Attribute, 哪些東西又可以放進子元素。基本上, 具修飾性意義的東西會做成 Attribute, 就像 <INPUT> 元素裡面的 id、name、style 等等; 而比較偏向專屬於另一個元素的東西就做成子元素, 像是 <table> 之下的 <tr>、<td> 等等。由於一個元素下面可以再加入子元素, 但 Attribute 卻不可能再加入什麼子元素, 所以如果你在設計 XML 文件時稍為思考這一點, 應該就可以做出適當的決定

如果再拿上個範例來看的話, 那麼, 像 <name> 底下可能可以再加入 FirstName 與 LastName 兩種屬性 (不論寫成 Element 還是 Attribute), 而 <salary> 之下可以再區分為 BasicSalary、TrafficGrant 與 Bonus 等等。如果你一開始就把它們都做成 Attribute 的話, 未來如果想改, 就得更動結構了。

此外, Element 還有一個很大的優勢, 就是你可以在一個元素之下加入多個相同性質的子元素。例如, 假設一個員工身兼兩個職稱, 例如部門經理兼任銷售部門主任的這種情形, 那麼你可以很方便的把資料改成如下的樣子:

<employee>
    <name>John</name>
    <title>Manager</title>
    <title>Sales Lead</title>
</employee>

當然, 我們不能隨時變更資料的結構, 否則會導致整個程式邏輯大亂而難以收拾。但是如果是在設計的初期, XML 是特別具備這種設計上的極高彈性的 (想像一下, 如果你使用 SQL Server 來做為儲存媒介, 此種需求變更將額外花掉你多少工夫?)。

「節點」 又是什麼?

「節點」(node) 是一個比較通用型的名詞。相較於 element 和 attribute, node 可以說被使用得比較頻繁、比較通俗易懂。簡單的說, 凡是 XML 裡面可以被巡覽的單元都可以稱為 node。因此一個 element 可以稱為一個 element node, 一個 attribute 可以稱為一個 attribute node, 而一個 element 連同其下的所有 child elements 都可以算是「一個」 node。如果你去看看 MSDN 網站左側導覽區, 那麼裡面所有可以點選項目都可以稱為節點 (不管有沒有包含子項目、能不能收合起來)。

或許你可以再回頭看看 心智圖 這種樹狀結構的長相, 我相信你會更容易明白到底所謂「節點」是用來指什麼。

根據 W3C 對於 XPath 1.0 的定義, 它認定一個 element node 所包含的所有 attribute node 都算是該 element node 的 child node。然而根據 DOM 的定義, 卻沒有這樣的說法。像這樣的矛盾點, 有可能是未來引起錯誤的地方。

CDATA 和 PCDATA 是什麼東西?

在一些 XML 檔案中, 你可能看過內容中有 CDATA 的標示。例如在 Visual Studio 所提供的 code snippet 中就有這種語法:

<Code Language="csharp">
    <![CDATA[[$SystemAttributeUsage$($SystemAttributeTargets$.$target$, Inherited  ... (省略) ... ]]>
</Code>

CDATA 指的是 character data 的意思, 而 PCDATA 則是 parsed character data。這兩者都可以在 XML 檔案中夾帶文字資料, 差別在於 CDATA 特別用來表示不會被 XML parser 所解析的文字, 而 PCDATA 則是將被解析的文字。那麼是什麼文字會被解析呢? 例如 &, <, > 等符號, 由於這些符號在 XML 有其固定的意義, 所以如果你的文字裡面如果有類似 if (x < y) 這種組合出現的話, 裡面的 < 符號可能會讓這個文件的正確性給破壞了。在這種情況下, 如果你使用 PCDATA 來標示這段文字, 你必須把上面那一段文字變更成 if (x &lt; y)。但如果你使用 CDATA 來標注, 則可以不用變更。

在大部份情況下, 我們最常使用 CDATA 而較少使用 PCDATA。

XSL (EXtensible Stylesheet Language)

XSL「可擴展樣式表語言」之於 XML, 就有點像是網頁之於資料一樣。以白話來講, XSL 就是為 XML 資料進行格式化, 讓它依照所們所要的樣子展示與排列的一種描述語言。舉個實際的例子好了, 如果你看過我的部落格入口 http://feeds.feedburner.com/jworker 的話, 你可能會發現它雖然是指向一個 RSS Feed, 但是它呈現在瀏覽器中的樣子, 看起來似乎就是一個不折不扣的已格式化的網頁, 而不像其他人的 RSS Feed 通常只是一篇 XML 文件的原始碼而已。

如果你去看看這個部落格的原始碼 (建議使用 IE 開啟), 你可以找到它在開頭處有以下這一段:

<?xml-stylesheet href="http://feeds.feedburner.com/~d/styles/rss2full.xsl" type="text/xsl" media="screen"?>

這一段註解事實上是有意義的, 它指示瀏覽器前往  http://feeds.feedburner.com/~d/styles/rss2full.xsl 下載 rss2full.xls 這個 XSL 文件, 並依據這個 XSL 的定義來格式化這個 RSS Feed, 使得它看起來就像瀏覽器上看到的樣子。

事實上, XSL 並不只是將 XML 中的資料進行美化而已。你注意到了嗎? 在這個部落格中, 透過 XSL, 它不僅影響了 RSS Feed 呈現在畫面中的樣子, 它還能自動讀取 RSS Feed 中所有的重複項目, 而無需撰寫程式。換句話說, XSL 就像是一個 template, 不僅扮演了類似 CSS 的角色, 還同時扮演了 data reader 的角色。

XSL 實際上包含兩個部分: XSLT 與 XPath, 請看以下的介紹。

XSLT – 轉換 XML 文件的語言 (XSL Transformations)

XSLT 可以說是 XSL 最核心的部份。它除了可以影響 XML 的呈現方式之外, 也可以對 XML 樹狀結構執行各種操作:

  • 新增和移除元素
  • 新增和移除 attribute
  • 重新排列和排序元素
  • 隱藏或顯示某些元素
  • 尋找或選取特定元素

XSLT 文件主要是由 templates 所組成的, 其標示方法如:

<xsl:template match="XPath">
    ....
</xsl:template>

以上範例中的 XPath 指的是 XML 文件中特定的階層位置, 請看下一小節的解釋。

如果你檢視這個部落格中的 XSL 文件 (http://feeds.feedburner.com/~d/styles/rss2full.xsl) 的話, 你可以看到裡面有好幾個 xsl:template 段落, 各自對應 RSS 原始檔中不同的 XPath 位置。所以 XSL 基本上就是運用這種定義各個 template 的方法把整個頁面組起來的。

至於 XSLT 樣板的定義方式其實也並不難懂。我們來看看它最外層的結構好了:

<xsl:template match="/">
  <xsl:element name="html">
    <head>
      <title>
          <xsl:value-of select="$title" />
          powered by FeedBurner
      </title>
      <link href="http://www.feedburner.com/fb/lib/stylesheets/undohtml.css" rel="stylesheet" type="text/css" media="all" />
      <link href="http://www.feedburner.com/fb/feed-styles/bf30.css" rel="stylesheet" type="text/css" media="all" />
      <link rel="alternate" type="application/rss+xml" title="{$title}" href="{$feedUrl}" />
      <xsl:element name="script">
          <xsl:attribute name="type">text/javascript</xsl:attribute>
          <xsl:attribute name="src">http://www.feedburner.com/fb/feed-styles/bf30.js</xsl:attribute>
      </xsl:element>
    </head>
    <xsl:apply-templates select="rss/channel" />
  </xsl:element>
</xsl:template>

我想, 即便你還沒學過 XSLT 的表示法, 光是看上面的結構, 你也應該不難猜出它到底打算組出什麼 HTML 標籤出來。

其中唯一值得注意的, 就是倒數第三行裡面的 <xsl:apply-templates select="rss/channel" /> 這段標示。如果你看不出來它代表什麼意思的話, 那麼我可以提醒你, 在這份 XSLT 裡面另外有一個 <xsl:template match="channel"> ... </xsl:template> 這個樣板。我想你應該不難猜出這個樣板將會被對應並放置到什麼地方去了吧!

如果你想深入了解 XSL/XSLT 的話, Adam Heyman 有一段很棒的線上教學影片可以看。雖然是英語的, 但是他講得很清楚, 而且很慢, 仔細聽的話應該是沒有太大困難的。

XPath – 巡覽 XML 文件的表示式

我們在上一段解釋 XSLT 的地方已經使用過好幾次 XPath 了, 例如在 <xsl:apply-templates select="rss/channel" /> 這段標籤裡面的 "rss/channel"。它具體描述了 XML 文件中某個階層的位置, 有點像是指出磁碟機中的某個子目錄一樣。關於 XPath, 有幾個不可不知的重點:

  • 如果表示式是以單斜線 (/) 開頭, 代表是 XML 文件的絕對路徑 (從 XML 文件的根目錄開始)。
  • 如果表示式是以雙斜線 (//) 開頭, 代表所有符合名稱的元素, 不管它們在 XML 文件中的位置在哪裡。例如, //item 會查詢 XML 文件內所有的 item 節點。
  • 星號 (*) 會選取路徑後所有 XML 元素。例如, /rss/item/* 會選取從 /rss/item 以下的所有子節點。
  • 一個句點 (.) 代表當前的節點, 兩個句點 (..) 則代表父節點。
  • 如果要選取 attribute 的話, 可以使用 @ 字元。例如 /rss//@version 可以找出 /rss 元素中所有名稱為 "version" 的那個 attribute。
  • 使用方括號 [] 可以加上蒒選條件, 例如 /Cars/Van[@Vendor='BMW'] 將會搜尋所有在 /Cars/Van 裡面所有 Vendor attribute 為 BMW 的那一項。
  • 使用方括號 [] 並加上 indexer 時可以當作集合中的索引。例如 //Employee 傳回一個集合, 而 //Employee[1] 指集合中的第一筆。請注意, 這裡的 indexer 是從 1 開始算而不是 0。
  • 使用圓括號 () 以明確標示所欲選取的群組。例如你可以使用 //Department/Employee[4] 來表示所有 /Deparment 之下的每個第四號 Employee。但是如果使用 (//Department/Employee)[4] 的話, 它將表示所有 /Department/Employee 節點中的第四個。請注意, 這個群組括號只適用最外層, 所以像 //Department/(Employee[4]) 的標示法是錯誤的。
  • 使用 | 代表聯集。例如 //LastName | //FirstName 可以得到 //LastName 與 //FirstName 的聯集。由於是聯集, 所以並不包含重複的部份。

XSLT 取值

我們已經知道如何透過 XPath 表示式定位 XML 中的元素和 attribute 了, 那麼我們該如何取得它們的值呢? 我們可以使用像在 http://feeds.feedburner.com/~d/styles/rss2full.xsl 中使用 <xsl:value-of select="$title" />  的敘述來取得某個元素的值。

不過在這裡你可能看不懂怎麼會臨時跑出來一個寫做 $title 的 XPath 表示式; 在上面所列的表示法規則裡面, 似乎沒有這一項。其實, 帶 "$" 符號的字代表「變數」, 而 $title 這個變數是先前已經定義過的:

<xsl:variable name="title" select="/rss/channel/title" />

如果你還想更深入了解 XPath 的話, 你可以參考 CodeGuru 以及 WIKI 上的詳細介紹, 或者 W3 Schools 的教學課程

此外, 在上面提過的那個 Adam Heyman 錄製的線上教學影片中, 在 Chapter 1. Part 2 這一節中以極為傳神的方法介紹了 XPATH 的基本用法, 我推薦你去看看。雖然它使用到了 XMLSPY 這套軟體, 而它並不是免費的; 但是實際上你不一定要去買或者下載試用版, 你應該光是看教學影片就可以看出作者想要表達的意思了。

相關文章:

沒有留言:

張貼留言