用XSLT,MSXSL腳本轉換XML資料格式

  • 6003
  • 0
  • 2010-01-06

XSLT是一種用來轉換XML文件格式的文件,配合.NET可以快速的開發簡單的文件轉換程式。較早之前就有遇到朋友用XSLT轉換文件成為Excel可讀的文件,但我暫時沒有打算用XSLT轉換出OOXML,因為這樣做很複雜很花時間,用SDK會比較有效率。不過XSLT在很多地方還是很有用,能夠快速地把資料格式化成你想要的樣子。例如從網路上找到的結構化資料,把它們轉換成我們想要的格式,但有時候來源資料並不是我們預期中結構化,所以這裡就引入MSXSL腳本。

XSLT是一種用來轉換XML文件格式的文件,配合.NET可以快速的開發簡單的文件轉換程式。較早之前就有遇到朋友用XSLT轉換文件成為Excel可讀的文件,但我暫時沒有打算用XSLT轉換出OOXML,因為這樣做很複雜很花時間,用SDK會比較有效率。不過XSLT在很多地方還是很有用,能夠快速地把資料格式化成你想要的樣子。例如從網路上找到的結構化資料,把它們轉換成我們想要的格式,但有時候來源資料並不是我們預期中結構化,所以這裡就引入MSXSL腳本。

這個範例我會把從網路上得到的貨幣資料轉換成我要的XML格式,準備用來做XMLDataProvider。

<?xml version="1.0" encoding="utf-8" ?>

<root>
  <option value="ARS">Argentine Peso (ARS AR$)</option>
  <option value="AUD">Australian Dollar (AUD A$)</option>
  <option value="BOB">Bolivian Boliviano (BOB Bs)</option>
  <option value="BRL">Brazilian Real (BRL R$)</option>
  <option value="GBP">British Pound Sterling (GBP UK£)</option>
  <option value="BGN">Bulgarian Lev (BGN)</option>
  <option value="CAD">Canadian Dollar (CAD CA$)</option>
  <option value="CLP">Chilean Peso (CLP CL$)</option>
  <option value="CNY">Chinese Yuan Renminbi (CNY CN¥)</option>
  <option value="COP">Colombian Peso (COP CO$)</option>
  <option value="HRK">Croatian Kuna (HRK kn)</option>
  <option value="CZK">Czech Republic Koruna (CZK Kč)</option>
  <option value="DKK">Danish Krone (DKK Dkr)</option>
  <option value="EGP">Egyptian Pound (EGP EG£)</option>
  <option value="EEK">Estonian Kroon (EEK Ekr)</option>
  <option value="EUR">Euro (EUR €)</option>
  <option value="HKD">Hong Kong Dollar (HKD HK$)</option>
  <option value="HUF">Hungarian Forint (HUF Ft)</option>
  <option value="INR">Indian Rupee (INR Rs.)</option>
  <option value="IDR">Indonesian Rupiah (IDR Rp)</option>
  <option value="ILS">Israeli New Sheqel (ILS ₪)</option>
  <option value="JPY">Japanese Yen (JPY ¥)</option>
  <option value="LTL">Lithuanian Litas (LTL Lt)</option>
  <option value="MYR">Malaysian Ringgit (MYR RM)</option>
  <option value="MXN">Mexican Peso (MXN)</option>
  <option value="MAD">Moroccan Dirham (MAD)</option>
  <option value="TWD">New Taiwan Dollar (TWD NT$)</option>
  <option value="NZD">New Zealand Dollar (NZD NZ$)</option>
  <option value="NOK">Norwegian Krone (NOK Nkr)</option>
  <option value="PKR">Pakistani Rupee (PKR PKRs)</option>
  <option value="PEN">Peruvian Nuevo Sol (PEN S/.)</option>
  <option value="PHP">Philippine Peso (PHP Php)</option>
  <option value="PLN">Polish Zloty (PLN zł)</option>
  <option value="RON">Romanian Leu (RON)</option>
  <option value="RUB">Russian Ruble (RUB)</option>
  <option value="SAR">Saudi Riyal (SAR SR)</option>
  <option value="RSD">Serbian Dinar (RSD din.)</option>
  <option value="SGD">Singapore Dollar (SGD S$)</option>
  <option value="ZAR">South African Rand (ZAR R)</option>
  <option value="KRW">South Korean Won (KRW ₩)</option>
  <option value="SEK">Swedish Krona (SEK Skr)</option>
  <option value="CHF">Swiss Franc (CHF Fr.)</option>
  <option value="THB">Thai Baht (THB ฿)</option>
  <option value="TRY">Turkish Lira (TRY TL)</option>
  <option value="UAH">Ukrainian Hryvnia (UAH)</option>
  <option value="AED">United Arab Emirates Dirham (AED)</option>
  <option value="USD">US Dollar (USD $)</option>
  <option value="VEF">Venezuelan Bolívar Fuerte (VEF Bs.F.)</option>
  <option value="VND">Vietnamese Dong (VND ₫)</option>  

</root>

目標輸出這樣的XML。

<root>
  <Currency>
    <Code>ARS</Code>
    <Symbol>AR$</Symbol>
    <Name>Argentine Peso</Name>
  </Currency>
  .......
</root>

要做到這件事,首先了解幾個基本的xsl元素。

<xsl:template /> 輸出文件的樣板定義在這個元素裡。

<xsl:for-each/ select="xpath" />對用xpath取得的所有節點做迭代。

<xsl:value-of select="xpath" />用xpath取得內容。

其他元素定義可以到W3C查閱

接下來簡單說一下怎樣取得你想要的內容,假設你已在一個節點的scope裡,要取得該節點的屬性,就用select="@attributeName",如果要取得元素中間的內容,即<element>內容</element>,就用select="."。

XPATH的定義也可以在W3C查閱

簡化版的XSLT定義如下

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
    <xsl:output method="xml" indent="yes"/>

  <xsl:template match="/">
    <root>
      <xsl:for-each select="root/option">
        <Currency>
          <Code>
            <xsl:value-of select="@value"/>
          </Code>        
          <Name>
            <xsl:value-of select="."/>
          </Name>
        </Currency>
      </xsl:for-each>
    </root>
  </xsl:template>
</xsl:stylesheet>

但這樣我們還有一個問題,要怎樣把內容裡的名字和幣值符號分離呢?方法有不止一個,XSLT 2.0裡有常規表示式的功能。但我決定用MSXSL腳本,更快的完成我要的功能,畢業用C#比去學XSLT 2.0的Regex要快得多。

要使用MSXSL Script,有幾個地方是必需要定義的。首先<xsl:stylesheet />裡要加入script的namespace。然後<msxsl:script implements-prefix="在stylesheet剛才加入的namespace">也一定要定義。接下來在裡面寫一般的C#程式。如果要引用using,可以在<msxsl:script />裡用<msxml:assembly />配合<msxsl:using />來達成,但我在這裡偷懶用長名稱。

至於傳參數至腳本,有兩種方法,一種是直接用xpath,跟<xsl:value-of select="xpath"/>的沒有不一樣,另一種是自己定義<xsl:variable />,再用$variableName去使用。下面的XSL文件兩種都會用到。

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" 
                xmlns:user="urn:my-scripts"
>

  <msxsl:script language="C#" implements-prefix="user">   
    <![CDATA[
    public string splitFullName(string innertext)
    {
    string m_inputPattern = @"^(?<FullName>.+)\s\((?<Symbol>.+)\)$";
    System.Text.RegularExpressions.Regex l_parser = new System.Text.RegularExpressions.Regex(m_inputPattern);
    Match l_matchParser = l_parser.Match(innertext);
    if(l_matchParser.Success)
    {
    return l_matchParser.Result(@"${FullName}");
    }
    else
    {return "match fail";}
    }
    
    public string splitSymbol(string innertext)
    {
    string m_inputPattern = @"^(?<FullName>.+)\s\((?<Symbol>.+)\)$";
    System.Text.RegularExpressions.Regex l_parser = new System.Text.RegularExpressions.Regex(m_inputPattern);
    Match l_matchParser = l_parser.Match(innertext);
    if(l_matchParser.Success)
    {
    return l_matchParser.Result(@"${Symbol}");
    }
    else
    {return "match fail";}
    }
    
    public string splitSymbol(string innertext, string shortName)
    {
    string m_inputPattern = @"^(?<FullName>.+)\s\((?<Symbol>.+)\)$";
    System.Text.RegularExpressions.Regex l_parser = new System.Text.RegularExpressions.Regex(m_inputPattern);
    Match l_matchParser = l_parser.Match(innertext);
    if(l_matchParser.Success)
    {
    string l_raw = l_matchParser.Result(@"${Symbol}");
       if(l_raw.StartsWith(shortName) && l_raw.Length > shortName.Length)
       {
          l_raw = l_raw.Remove(0, shortName.Length).Trim();
       }
       return l_raw;
    }
    else
    {return "match fail";}
    }
    ]]>    
  </msxsl:script>
  
  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="/">
    <root>
      <xsl:for-each select="root/option">
        <Currency>
          <Code>
            <xsl:value-of select="@value"/>
          </Code>
          <xsl:variable name="innertext" select="." />
          <Symbol>
            <xsl:value-of select="user:splitSymbol($innertext, @value)"/>
          </Symbol>
          <Name>
            <xsl:value-of select="user:splitFullName($innertext)"/>
          </Name>
        </Currency>
      </xsl:for-each>
    </root>
  </xsl:template>
</xsl:stylesheet>

XSL已準備好,接下來就要用程式對XML輸入做轉換。.NET提供了兩個類別XslTransform和XslCompiledTransform來處理XSL。後面再說明有甚麼差別,我這裡為求方便使用XslTransform類別。

XslCompiledTransform l_xsl = new XslCompiledTransform();            
l_xsl.Load("XSLT文件路徑");
l_xsl.Transform("輸入文件路徑","輸出文件路徑");

就是這樣,成功把XML文件轉換成我想要的結構。至於為甚麼會提到XslCompiledTransform是因為用到MSXSL腳本的話,在伺服器環境下會造成Memory Leak,所以用XslCompiledTransform去自己控制腳本的編譯來防止Leak。其實XslTransform已過時,請使用XslCompiledTransform。Memory Leak這方面我還沒有實際去使用,有需要的人要自己處理了。如果有這方面經驗的人歡迎指教。感謝大家。

My WP Blog with english technical docs.