2016年2月29日月曜日

ScalaでXSLT

Scalaで半構造データ的なデータ処理に対してどのようなアプローチをとっていくのかは重要な論点の一つだと思いますが、引き続きXMLも有力な選択肢だと思います。

XMLはXML文書をプログラム内にデータ構造として取り込むだけだとあまり面白みはありませんが、XPath, XSLT, XQueryといったデータ操作用の機能を使用すると応用の範囲がぐっと広がります。

この中でもXPathとXSLTはJavaの基本APIに入っているので、Scalaからもシームレスに使うことができます。

そこでScalaでのXSLT使い方を整理してみました。

準備

処理対象とするXML文書です。

  1. <userlist>  
  2.   <user zip="221" city="Yokohama" name="Taro"/>  
  3.   <user zip="248" city="Kamakura" name="Hanako"/>  
  4. </userlist>  

XSLTのスタイルシートとして以下のものを使用します。

上記のXML文書をHTMLの表に整形します。

  1. <?xml version="1.0"?>  
  2. <xsl:stylesheet version="1.0"  
  3. xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  
  4. xmlns="http://www.w3.org/1999/xhtml">  
  5. <xsl:strip-space elements="*"/>  
  6. <xsl:template match="/">  
  7.     <html>  
  8.       <table border="true">  
  9.  <thead>  
  10.    <th>Name</th>  
  11.    <th>Zip</th>  
  12.    <th>City</th>  
  13.  </thead>  
  14.  <tbody>  
  15.           <xsl:apply-templates select="/userlist/user"/>  
  16.  </tbody>  
  17.       </table>  
  18.     </html>  
  19.   </xsl:template>   
  20.   
  21.   <xsl:template match="user">  
  22.     <tr>  
  23.       <td><xsl:value-of select="@name"/></td>  
  24.       <td><xsl:value-of select="@zip"/></td>  
  25.       <td><xsl:value-of select="@city"/></td>  
  26.     </tr>  
  27.   </xsl:template>  
  28. </xsl:stylesheet>  

xsltprocコマンドでXML文書の変換を行うと以下のようにXHTML文書が出力されます。

$ xsltproc app.xsl data.xml
<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml"><table border="true"><thead><th>Name</th><th>Zip</th><th>City</th></thead><tbody><tr><td>Taro</td><td>221</td><td>Yokohama</td></tr><tr><td>Hanako</td><td>248</td><td>Kamakura</td></tr></tbody></table></html>

このXHTML文書を見やすいように整形すると以下になります。

  1. <html xmlns="http://www.w3.org/1999/xhtml">  
  2.   <table border="true">  
  3.     <thead>  
  4.       <th>Name</th>  
  5.       <th>Zip</th>  
  6.       <th>City</th>  
  7.     </thead>  
  8.     <tbody>  
  9.       <tr>  
  10.         <td>Taro</td>  
  11.         <td>221</td>  
  12.         <td>Yokohama</td>  
  13.       </tr>  
  14.       <tr>  
  15.         <td>Hanako</td>  
  16.         <td>248</td>  
  17.         <td>Kamakura</td>  
  18.       </tr>  
  19.     </tbody>  
  20.   </table>  
  21. </html>  

XSLTプロセッサを使う

スタイルシートとXMLデータの両方をStringで受取り、変換結果をStringで返す処理を関数「StringのスタイルシートでStringのXMLデータを変換」として作成しました。

  1. def StringのスタイルシートでStringのXMLデータを変換(stylesheet: String, data: String): String = {  
  2.     val stylesource = new StreamSource(new StringReader(stylesheet))  
  3.     val transformer = TransformerFactory.newInstance().newTransformer(stylesource)  
  4.     val source = new StreamSource(new StringReader(data))  
  5.     val buf = new StringWriter()  
  6.     val result = new StreamResult(buf)  
  7.     transformer.transform(source, result)  
  8.     buf.toString  
  9.   }  

JavaのXSLTプロセッサであるjavax.xml.transform.Transformerは、スタイルシート、XML文書、変換結果のそれぞれに対して以下の入力の選択肢があります。

  • 文字列
  • DOM
  • SAX

ここでは、スタイルシート、XML文書、変換結果のすべてに文字列を使う組合せを実装しています。

XMLリテラルとDOM

ScalaでXMLを扱う時に少しややこしいのはXMLリテラルの存在です。

XMLリテラルはScalaの文法に組み入れられており、さらにScala的、DSL的なAPIで木構造の操作を簡単に行えるようになっているメリットがある反面、XMLの共通機能であるXPath、XSLTを使用することができない、というデメリットがあります。

この問題への対応方法は、DOMやSAXといったJavaが提供しているパーサーとXMLリテラルの相互変換です。

以下ではこの相互変換で一番楽な方法である文字列を媒介とした変換を採用しました。スタイルシートをXMLリテラルのオブジェクトであるscala.xml.Nodeで受取り、それを文字列表現に変換して「StringのスタイルシートでStringのXMLデータを変換」関数に渡しています。

  1. def XMLリテラルのスタイルシートでStringのXMLデータを変換(stylesheet: scala.xml.Node, data: String): String =  
  2.     StringのスタイルシートでStringのXMLデータを変換(stylesheet.toString, data)  

文字列表現を媒介にする方法は一般的には十分ですが、大規模データ操作などで性能が求められる場合は適さないかもしれません。

そのようなケースでは、javax.xml.transform.sax.SAXSourceをextendsしたXMLリテラル用のSourceを作成して対処するとよいでしょう。XMLリテラル用のSAXパーサーorg.xml.sax.XMLReaderを作成し、このSAXSourceでラップするような形になります。

使ってみる

スタイルシートをXMLリテラルで、XML文書を文字列で受け取って、変換結果を文字列で受け取る関数「XMLリテラルのスタイルシートでStringのXMLデータを変換」で処理するプログラムは以下になります。

生文字列リテラルでも問題はないですが、やはりXML専用のXMLリテラルでXSLTによる変換処理を記述できると便利です。

  1. def main(args: Array[String]) {  
  2.     val stylesheet = <xsl:stylesheet version="1.0"  
  3. xmlns:xsl="http://www.w3.org/1999/XSL/Transform"  
  4. xmlns="http://www.w3.org/1999/xhtml">  
  5. <xsl:strip-space elements="*"/>  
  6. <xsl:template match="/">  
  7.     <html>  
  8.       <table border="true">  
  9.  <thead>  
  10.    <th>Name</th>  
  11.    <th>Zip</th>  
  12.    <th>City</th>  
  13.  </thead>  
  14.  <tbody>  
  15.           <xsl:apply-templates select="/userlist/user"/>  
  16.  </tbody>  
  17.       </table>  
  18.     </html>  
  19.   </xsl:template>   
  20.   
  21.   <xsl:template match="user">  
  22.     <tr>  
  23.       <td><xsl:value-of select="@name"/></td>  
  24.       <td><xsl:value-of select="@zip"/></td>  
  25.       <td><xsl:value-of select="@city"/></td>  
  26.     </tr>  
  27.   </xsl:template>  
  28. </xsl:stylesheet>  
  29.     val data = """<userlist>  
  30.   <user zip="1234567" city="Yokohama" name="Taro"/>  
  31.   <user zip="1234567" city="Kamakura" name="Hanako"/>  
  32. </userlist>  
  33. """  
  34.     val result = XMLリテラルのスタイルシートでStringのXMLデータを変換(  
  35.       stylesheet, data)  
  36.     println(result)  
  37.   }  

プログラム実行の結果、無事以下の出力が得られました。

<?xml version="1.0" encoding="UTF-8"?><html xmlns="http://www.w3.org/1999/xhtml"><table border="true"><thead><th>Name</th><th>Zip</th><th>City</th></thead><tbody><tr><td>Taro</td><td>1234567</td><td>Yokohama</td></tr><tr><td>Hanako</td><td>1234567</td><td>Kamakura</td></tr></tbody></table></html>

諸元

  • Java 1.7.0_75
  • Scala 2.11.7