ちまちまxpathとrubyで変換していくのが面倒になってきたので、XMLを変換するならXpath+nokogiriよりもしかしたら便利かもしれないXSLT使ってみた。
オワコン臭漂うXML界隈で、さらに忘れられた存在のXSLTです。。。
XSLT
XSLT ( Extensible Stylesheet Language Transformation ) です。XML Style Languaget and Tramsform 的に覚えていけばイイかと思います。
有り体に言えば、XSLTはXMLを変換していくためのものです。大雑把なたとえではXMLにとってUnixパイプみたいなもん(?)
XML --> XSLT --> XML
スタイルというわりにはCSSみたいなフォント指定だのボックスモデルなどはないわけで、XML->XHTML に変換するためのテンプレート。
XSLT で出来ること
XMLからXML(XHTML)を作る。HTML5は絶対に作れません。
XSLT を使える場所
XML文書中に埋め込んで使う。
XSLT の変換エンジン(変換プロセッサ
事実上libxml/libxslt関連しかなく、XSLT3.0などは実装してる変換プロセッサはJavaのSAXONライブラリがあるくらい。
ある意味「枯れた」実装。枯れたというより死んでるけど、ブラウザ中で生きてる
今回はxsltproc を使うことにした。
xslt を使う
xsltproc でXMLをXSLT処理するには、次のようにコマンドを指定する。
xsltproc my.xslt input.xml > out.xml
xslt ファイルとターゲットのXMLを指定して実行すると、テンプレート適用されたXMLが得られる。
XSLTの基本構文
XSLTもXMLなので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 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> </xsl:text>
XMLなのでスペースは読み飛ばされる。なので、あえてスペースを記述する必要がある。しかもXMLノードとして。(めんどくさい・・・
他にも
<xsl:text>
</xsl:text><!-- 改行 --> <xsl:text>	</xsl:text><!-- タブ--> <xsl:text> </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できない。XSLTはXMLとして定義されているので、XMLとしてただしくないので出来ない。めんどくさい for-each毎に、Xpathで同時に3個取り出せばできるらしい。
解決策はいくつかある。 disable-output-escaping="yes"を使う。CDATAを使う。クエリを工夫して3つ毎のノードでループ3つ同時に取り出す。ApplyTemplateを使うなど
if に関する解決策 ↓
- http://www.getsymphony.com/discuss/thread/639/
- https://our.umbraco.org/forum/developers/xslt/9354-Add-wrapper-div-conditionally
- https://our.umbraco.org/forum/developers/xslt/17167-creating-unclosed-tag-in-xslt
並び替え
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() < 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の場合)