Sunday, April 10, 2011

初學 XSLT 心得: 將 XML 文件轉換成 MediaWiki 文件

這篇文章紀錄我接觸 XSLT 的心得,可供有興趣的朋友參考。

首先解釋一下主題,假設我們手上有一份 XML 文件,是一份紀錄書店銷售紀錄的 XML 文件,內容為各個分店所賣出的書籍和BD。

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="store2wiki.xsl"?>
<sale>
    <store name="Taipei">
        <book ISBN="9784757733299">
            <title>バカとテストと召喚獣</title>
            <author>井上堅二</author>
        </book>
        <book ISBN="9784757735057">
            <title>バカとテストと召喚獣2</title>
            <author>井上堅二</author>
        </book>
        <BD EAN="4935228096671">
            <title>Madoka Magica Vol.1"</title>
        </BD>
    </store>
</sale>

這樣的文件,正常人類是不會想看的,連我自己打起來都很累。那個角括號看起來真的很不習慣。所以啦,為了要把這份文件做成易讀的格式,我決定用 wiki 的條列方式來呈現此文件,利用縮排來呈現 XML 中的樹狀結構。
  • Store: Taipei
    • Book (IAN=978-4757733299)
      • title: バカとテストと召喚獣
      • author: 井上堅二
    • Book (IAN=978-4757735057)
      • title: バカとテストと召喚獣2
      • author: 井上堅二
    • BD: (IAN=4935228096671)
      • title: Madoka Magica Vol.1

這樣看起來是不是清楚多了?
wiki 的種類很多,我採用 Wikipedia 的系統 MediaWiki,用它的語法的話,文件必須寫成如下的形式:

* store: Taipei
:* book (IAN=9784757733299)
::* title: バカとテストと召喚獣
::* author: 井上堅二
:* book: (IAN=9784757735057)
::* title: バカとテストと召喚獣2
::* author: 井上堅二
:* BD: (IAN=4935228096671)
::* title: Madoka Magica Vol.1

每一行前面的星號是標明項目,冒號的數量表示縮排的深度。

問題定義完成。在開始之前,先解釋一些名詞
XML: 全名是 Extensible Markup Language,中文可翻成延伸標記語言。這是一種表示資料的方式。邏輯上來看,是將資料表示成樹狀結構,每個element都會有自己的屬性,並且可以擁有子element。不熟的朋友可參考 http://www.w3schools.com/xml/

XSLT: 全名是 XSL Transformations,可用來將XML文件轉換成另外一個 XML 或 HTML。不過當你了解他的轉換原理後,就會了解到他可以將XML轉成任何格式的文件。這也就是我這次嘗試使用XSLT將XML轉成MediaWiki的理由。

XSLT讓我們定義轉換XML文件的方法,簡單來說我們要提供的是一套規則,說明對於哪些element要施行何種轉換,瀏覽器會依據規則幫我們轉換文件。簡單的說,瀏覽器會從樹根出發,採用一個可以施行的規則,輸出對應的文字。一般說來在規則中會指示瀏覽器移動到子element,並繼續套用規則。

開始動工。我設計規則只有一條: 印出目前的 element 的所有屬性,每個一行,然後繼續處理子element。大約十多行 XSLT 就可以做到。

指定規則是用 template 指令: <xsl:template match="*">
我們用match來指定element的名稱,有符合的就會套用此規則,這裡採用了萬用字元*,所以可以對應到任何element。
接下來用<xsl:value-of select="name()"/>印出此element的名稱。接著是用<xsl:for-each select="@*">找出此element的所有屬性,注意到屬性的開頭是@。
以上三個指令就能讓我們作絕大多數的事,template是用來建立規則,value-of 是用來輸出文字,for-each是用來處理每個子element。


說起來簡單,麻煩的是要在每行的最前面加入冒號,這必須根據目前的深度來決定。
為了解決這個問題,我用了一個小技巧,由於在每個element我們可以使用 ancestor::* 取得他的祖先,這會是一個sequence,因此我使用 for-each 指令,每看到一個祖先element就印出一個冒號。請見第7行。
另外有兩個小地方要注意。
  1. XML 中的空白字元: 回頭看看我們的 XML 檔案,裡面含有縮排的空白字元,但是我們輸出時不想要這些空白字元(還有換行符號!)所以加入第三行指令 <xsl:strip-space elements="*"/> 以去除這些字元,各位可以把這行拿掉看看會如何 :)
  2. 將最內層的 element 和 parent 一起顯示: 你會注意到 title 和 author 這兩個 element 內部只含有文字,對於 XSLT 來說,文字也算是 element 的一個子 element,所以依照我們的規則,他會被顯示在下一行。但是我不想要這種結果,所以就採用了11~13行的手法,11行表示只套用內含文字的子element,然後12行印出換行,最後13行再套用剩餘的子element。

總計15行code搞定!

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:strip-space elements="*"/>
    <xsl:output indent="no" method="text" />
    <xsl:template match="text()">:<xsl:value-of select="."/></xsl:template>
    <xsl:template match="*">
        <xsl:for-each select="ancestor::*">:</xsl:for-each>* <xsl:value-of select="name()"/>
        <xsl:for-each select="@*">
            <xsl:text> (</xsl:text><xsl:value-of select="name(.)"/>=<xsl:value-of select="."/><xsl:text>)</xsl:text>
        </xsl:for-each>
        <xsl:apply-templates select="text()"/>
        <xsl:text>&#10;</xsl:text>
        <xsl:apply-templates select="*"/>
    </xsl:template>
</xsl:stylesheet>

參考資料
[1] W3C 的官方文件 http://www.w3.org/TR/xslt
[2] FAQ 專區 http://www.dpawson.co.uk/xsl/sect2/sect21.htm