ちまちまxpathとrubyで変換していくのが面倒になってきたので、XMLを変換するならXpath+nokogiriよりもしかしたら便利かもしれないXSLT使ってみた。
オワコン臭漂うXML界隈で、さらに忘れられた存在のXSLTです。。。
XSLT ( Extensible Stylesheet Language Transformation ) です。XML Style Languaget and Tramsform 的に覚えていけばイイかと思います。
有り体に言えば、XSLTはXMLを変換していくためのものです。大雑把なたとえではXMLにとってUnixパイプみたいなもん(?)
XML --> XSLT --> XML
スタイルというわりにはCSSみたいなフォント指定だのボックスモデルなどはないわけで、XML->XHTML に変換するためのテンプレート。
XSLTはXMLで記述されます。
XMLからXML(XHTML)を作る。HTML5は絶対に作れません。
巨大なXMLから必要データを取り出したXMLを作ります。
XML文書中に埋め込んで使う。
XML文書にXSLTの場所をURLで指定する
XSLT の変換エンジン(変換プロセッサ
事実上libxml/libxslt関連しかなく、XSLT3.0などは実装してる変換プロセッサはJavaのSAXONライブラリがあるくらい。
ある意味「枯れた」実装。枯れたというより死んでるけど、ブラウザ中で生きてる
今回はxsltproc を使うことにした。
xsltproc でXMLをXSLT処理するには、次のようにコマンドを指定する。
xsltproc my.xslt input.xml > out.xml
xslt ファイルとターゲットのXMLを指定して実行すると、テンプレート適用されたXMLが得られる。
XSLTもXMLなのでXMLとして記述する。XMLなので閉じタグ忘れや、要素を超えた閉じタグは許されない。
xml version="1.0" encoding="UTF-8"
<xslstylesheet version="1.0" xmlnsxsl="http://www.w3.org/1999/XSL/Transform">
<xsltemplate match="/">
</xsltemplate>
</xslstylesheet>
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 のほうが楽だった。
<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を使えば、テンプレの再利用や部品化ができる・・・けど
<xsltemplate name="sidebar">
</xsltemplate>
<xsltemplate match="/menu">
<xslapply-templates name="sidebar"/>
</xsltemplate>
一見すると便利そうだけど、ややこしいXSLT内部を飛び回るGOTOと同じ、GOTO地獄になったのでおすすめしない。
ループ
ループは for-each を使う。
<xslfor-each select="//data">
<div class="year_list" id="year_{@year}">
</div>
</xslfor-each>
for-each内部は、カレントノードを "."(ドット) 参照できるし省略可能。
上記の例だと //dataにループを回して、 //data/@year を出力している。divが //dataの個数だけ出来る。
テキストノード作成
XSLT でテキストノードを作る
出力でテキストノードをつくるにはvalue-of を使う。select にはカレントノードからみたXpathを使う。
<xslvalue-of select="@attr_name" />
<xslvalue-of select="@path/to/node" />
<xslvalue-of select="@substring(path/to/node,10)" />
スペースを入れたい
スペース(空白)いれるのがめんどくさいので最初に覚えておく。
<xsl:text> </xsl:text>
XMLなのでスペースは読み飛ばされる。なので、あえてスペースを記述する必要がある。しかもXMLノードとして。(めんどくさい・・・
他にも
<xsltext>
</xsltext>
<xsltext>	</xsltext>
<xsltext> </xsltext>
なども必要になることが多い。
属性値 を出力
出力XMLのノード属性値を出力するには、 変数に一旦格納する。
変数に格納したら {$image-title}
のように
ただし、ダブルクオーテーション中なら、"{@width}" な "{@name}"のように埋込できる。
<xslvariable name="image-dir">/images</xslvariable>
<xsltemplate match="photograph">
<img src="{$image-dir}/{href}" width="{size/@width}"/>
</xsltemplate>
これで
<a href="リンク" >なまえ</a>
のようなXHTMLを作成できる
文字列の 置換
translate が一番カンタン
<a href="{translate(@filepath, '\', '/')}"><xslvalue-of select="@title" /></a>
条件分岐もできる
Xpathでするには、if すらOpen/Closeをいしきする
for-each 中で奇数偶数で一旦div を閉じるとか、そういうことは出来ない。
<xslfor-each select="//book">
<xslif test="position() mod 3 = 0 ">
<div>
</xslif>
<img src="{@coverImage}" alt="{@month}" width="200px" />
<xslif test="position() mod 3 = 2 ">
</div>
</xslif>
</xslfor-each>
div と if が XMLとしてネストしちゃってるので、アウト
カンタンにはifできない。XSLTはXMLとして定義されているので、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の開始直後に書くことで並べられる。
ひどい仕様だ。。。
最後の1件だけを表示したくない時。
<xsl:for-each select="(//book)[position() < count(//book) ]">
</xsl:for-each>
&でエスケープして入れます。
・・・ひどい仕様だ
数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の場合)
参考になりそうなのもの