それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

XSLTについてのまとめ

ちまちまxpathrubyで変換していくのが面倒になってきたので、XMLを変換するならXpath+nokogiriよりもしかしたら便利かもしれないXSLT使ってみた。

オワコン臭漂うXML界隈で、さらに忘れられた存在のXSLTです。。。

XSLT

XSLT ( Extensible Stylesheet Language Transformation ) です。XML Style Languaget and Tramsform 的に覚えていけばイイかと思います。

有り体に言えば、XSLTXMLを変換していくためのものです。大雑把なたとえではXMLにとってUnixパイプみたいなもん(?)

XML --> XSLT --> XML 

スタイルというわりにはCSSみたいなフォント指定だのボックスモデルなどはないわけで、XML->XHTML に変換するためのテンプレート。

XSLTXMLで記述されます。

XSLT で出来ること

XMLからXML(XHTML)を作る。HTML5は絶対に作れません。

巨大なXMLから必要データを取り出したXMLを作ります。

XSLT を使える場所

XML文書中に埋め込んで使う。

XML文書にXSLTの場所をURLで指定する

XSLT の変換エンジン(変換プロセッサ

事実上libxml/libxslt関連しかなく、XSLT3.0などは実装してる変換プロセッサはJavaのSAXONライブラリがあるくらい。

ある意味「枯れた」実装。枯れたというより死んでるけど、ブラウザ中で生きてる

今回はxsltproc を使うことにした。

xslt を使う

xsltproc でXMLXSLT処理するには、次のようにコマンドを指定する。

xsltproc  my.xslt input.xml > out.xml

xslt ファイルとターゲットのXMLを指定して実行すると、テンプレート適用されたXMLが得られる。

XSLTの基本構文

XSLTXMLなのでXMLとして記述する。XMLなので閉じタグ忘れや、要素を超えた閉じタグは許されない。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/">
 <!-- ここにかく -->
  </xsl:template>
</xsl:stylesheet>

XSLTでHTMLを出力するには

XMLを変換してHTML(XHTML)へ変換することができる。多分一番使うし一番便利。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" encoding="utf-8"  />
  <xsl:template match="/">
    <html>
      <body>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

output で method="html"で指定する。

これで、XMLではなく、HTMLで出力される。

単に<?xmlが出力されないだけなんだけどね・・・

ブラウザに変換させる

ブラウザでXMLを開けば、XSLTが実行されてHTML(XML)に変換される。

XMLの先頭にXSLTの場所を記述してやる

 <?xml version='1.0' encoding='UTF-8'?>
   <?xml-stylesheet type="text/xsl" href="./my-transform.xslt" ?>

ブラウザで処理してると変換結果のチェックが大変だったのでxmlproc のほうが楽だった。

XSLT の変換スタート

<xsl:template match="/">を書いたところから処理が始まる。

match="{xpath}" を書くことで、条件にマッチしたXMLノードで処理ができる。

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/"><!-- ここから処理開始 -->
(中略
  </xsl:template>
</xsl:stylesheet>

さいしょは、<xsl:template match="/"> から </xsl:template>の間に処理を書くと憶える。

template は相互に呼び出しできる

apply-templateを使えば、テンプレの再利用や部品化ができる・・・けど

<xsl:template name="sidebar">
  <!-- サイドバーなんちゃら -->
</xsl:template>
<xsl:template match="/menu">
  <xsl:apply-templates name="sidebar"/>
</xsl:template>

一見すると便利そうだけど、ややこしいXSLT内部を飛び回るGOTOと同じ、GOTO地獄になったのでおすすめしない。

ループ

ループは for-each を使う。

<xsl:for-each select="//data"><!-- for-each .year/ -->
  <div class="year_list" id="year_{@year}"><!--- ここはカレントノードが data -->
  </div>
</xsl:for-each><!-- for-each ./year -->

for-each内部は、カレントノードを "."(ドット) 参照できるし省略可能。

上記の例だと //dataにループを回して、 //data/@year を出力している。divが //dataの個数だけ出来る。

テキストノード作成

XSLT でテキストノードを作る

出力でテキストノードをつくるにはvalue-of を使う。select にはカレントノードからみたXpathを使う。

<xsl:value-of   select="@attr_name" />
<xsl:value-of   select="@path/to/node" />
<xsl:value-of   select="@substring(path/to/node,10)" />

スペースを入れたい

スペース(空白)いれるのがめんどくさいので最初に覚えておく。

 <xsl:text>&nbsp;</xsl:text>

XMLなのでスペースは読み飛ばされる。なので、あえてスペースを記述する必要がある。しかもXMLノードとして。(めんどくさい・・・

他にも

<xsl:text>&#xa;</xsl:text><!-- 改行 -->
<xsl:text>&#x9;</xsl:text><!-- タブ-->
<xsl:text>&#x20;</xsl:text><!-- スペース-->

なども必要になることが多い。

属性値 を出力

出力XMLのノード属性値を出力するには、 変数に一旦格納する。

変数に格納したら {$image-title} のように

ただし、ダブルクオーテーション中なら、"{@width}" な "{@name}"のように埋込できる。

<xsl:variable name="image-dir">/images</xsl:variable>

<xsl:template match="photograph">
<img src="{$image-dir}/{href}" width="{size/@width}"/>
</xsl:template>

これで

<a href="リンク" >なまえ</a>

のようなXHTMLを作成できる

文字列の 置換

translate が一番カンタン

<a href="{translate(@filepath, '\', '/')}"><xsl:value-of select="@title" /></a>

XSLTでif を使う

条件分岐もできる

Xpathでするには、if すらOpen/Closeをいしきする

for-each 中で奇数偶数で一旦div を閉じるとか、そういうことは出来ない。

        <xsl:for-each select="//book">
            <xsl:if test="position() mod 3 = 0 ">
              <div>
            </xsl:if>
            <img src="{@coverImage}" alt="{@month}" width="200px" />
            <xsl:if test="position() mod 3 = 2 ">
              </div>
            </xsl:if>
        </xsl:for-each>

div と if が XMLとしてネストしちゃってるので、アウト

カンタンにはifできない。XSLTXMLとして定義されているので、XMLとしてただしくないので出来ない。めんどくさい for-each毎に、Xpathで同時に3個取り出せばできるらしい。

解決策はいくつかある。 disable-output-escaping="yes"を使う。CDATAを使う。クエリを工夫して3つ毎のノードでループ3つ同時に取り出す。ApplyTemplateを使うなど

if に関する解決策 ↓

並び替え

Xpathの結果を並び替えてループできる

<xsl:for-each select="//data"><!-- for-each .year/ -->
  <xsl:sort select="position()" data-type="number" order="descending"/>
</xsl:for-each><!-- for-each ./year -->

この場合、//dataを降順に並び替えている。なぜか for-eachの開始直後に書くことで並べられる。

ひどい仕様だ。。。

Xpathの比較演算子はどうするのか

最後の1件だけを表示したくない時。

        <xsl:for-each select="(//book)[position() &lt; count(//book) ]">
        </xsl:for-each>

&でエスケープして入れます。

・・・ひどい仕様だ

XSLTの変換速度

数GB級の巨大なXMLを扱うと遅い

takuya@:~/Desktop$ ls -hl main.xml
-rw-r--r-- 1 takuya staff 165M  9  2 11:16 main.xml

takuya@:~/Desktop$ time xmllint --noout  main.xml

real    0m3.573s
user    0m3.112s
sys    0m0.444s

takuya@:~/Desktop$ time xsltproc  test.xslt main.xml  > /dev/null

real    0m6.865s
user    0m6.204s
sys    0m0.586s

予想通り、メモリに乗るならそこそこ我慢できる速度になる。 メモリ量の30%くらいじゃない?限界は。 巨大なXMLを処理するのは不可能

メモリ8GBのMBPで、5GBで数分掛かってギブアップする

やっぱり、巨大データ処理を手元でやるならインデックスつけたRDB(SQL)が最強

match="/" から始める。必ず。

match="/" を書かなかった場合

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:template match="/rss/item"><!-- match="/" がない場合-->
(中略
  </xsl:template>
</xsl:stylesheet>

目的のノード以外に余計な情報がおおいと、ついつい目的ノードXpath書いちゃうけどそれはダメ。

XML 文書中の /* のノードのうち、match="/rss/item"のみ変換され、それ以外はテキストとして処理される(xmlprocの場合)

参考になりそうなのもの