XSLTはXMLからXMLを作る錬金術師

今日はXSLTって面白いよなと思って調べてみた。
XSLTというのはXMLを他の形態に変換するためのスタイルシート。
スタイルシートといえどHTMLと組み合わせて使うCSSのようなものとは全く違う。
と思うんだけど少々自信がない。しかしHTMLとXSLTを組み合わせて使う話は聞かないしなぁ。
これを使えばXMLで書かれた文書を、XHTMLとかRSSとか他のXMLや、プレーンテキストとかTeXとか、
そんな風に変換できると。これで1つのXML文書をいろんなところで使えるよと。
Mozilla FirefoxはXSLTにネイティブに対応しているのでXMLにちょっと書いてやるだけで変換して見られる。


しかし調べてたが、これはXMLをXMLに変換することばかり考えている。
当初XMLをHTMLに変換しようと思ってXSLTで書いていたが、どうも無理っぽい。
というのもXSLT自体がXMLで出来ていて、その中に生で<br>など埋め込んでも、
整形式じゃないと言われてしまう。
かといって&lt;br&gt;と書いたり、CDATAセクションに入れたりしても、それはそれでHTMLの要素として通じない。
ただそれはFirefox上での話であって、SAXONとか使って変換すれば、HTML文書として完成しているわけですが。
しかしながら、XML以外への変換はめんどくさそうだなというのはよくわかった。
だって、XMLの特殊文字のことを考えないとだめだから。


さて、実際にやってみたわけだが、XSLTは実にいろいろなことができる。
まずこれが作ったXML文書。



<my:test xmlns:my="http://libserver.ddo.jp/fdiary/?e=320" xmlns="http://www.w3.org/1999/xhtml">
<my:title>XML研究所
<my:desc>THERE IS NO DATA. THERE IS ONLY XML.
<my:url>http://xml.example.com/
<my:rss>http://xml.example.com/rss.xml
<my:user name="木村裕介" email="yusuke@example.com" id="USER1" />
<my:user name="尾崎隼人" email="hayato@example.com" id="USER2" />
<my:user name="大森翔大" email="shota@example.com" id="USER3" />
<my:article author="USER1" title="XSLT活用術" ref="http://www.example.com/~yusuke/article001.html"
year="2009" month="03" date="27" day="6" hour="23" min="59" sec="09" zone="+09:00">
<p>
くわしくは次に示すページを見て欲しい。<br />
<a href="http://libserver.ddo.jp/fdiary/?e=320">「XSLTはXMLからXMLを作る錬金術師」
</p>
</my:article>
</my:test>

my:testのようにmy:がついてるのは自分で作った要素。何もついてないのはXHTMLの要素。
ということをxmlnsで指定してあるわけね。この指定は大切な事。
まぁこんな風にXHTMLの機能を一部借りたXML文書が出来たわけだ。
この文書は少々複雑で、article要素のauthor属性に指定してあるのはその人のユーザーを定義しているuser要素のidなんですね。
こうしておけば同じ事を何度も書かなくて済むし、idからたどっていけば深い情報を知ることができるかもしれない。


これだけのことを解釈するXSLTスタイルシートを作る必要があるということだ。

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xml"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://www.w3.org/1999/xhtml" xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:my="http://libserver.ddo.jp/fdiary/?e=320" exclude-result-prefixes="my xhtml">
<xsl:output method="xml" media-type="application/xhtml+xml" encoding="UTF-8"
doctype-public="-//W3C//DTD XHTML 1.1//EN"
doctype-system="http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" />
<xsl:template match="/">
<html xml:lang="ja">
<xsl:apply-templates />
</html>
</xsl:template>
<xsl:template match="my:test">
<head>
<title><xsl:value-of select="my:title" /></title>
<style type="text/css">
<![CDATA[
h1 { padding:0.2em; font-size:2em; background-color:white; color:blue; }
h2 { color:purple; font-size:1.5em; border-left:solid 1.5em green; padding:0 0.2em; }
#footer { border:solid 2px aqua; background-color:#FFFF99; margin:1em; }
.article { border:solid 2px fuchsia; background-color:#CCFFFF; margin:1em; }
p.info { font-size:0.8em; text-align:right; margin:0.2em 3em; }
]]>
</style>
</head>
<body>
<h1><xsl:value-of select="my:title" /></h1>
<xsl:apply-templates select="my:article" />
<div id="footer">
<h2>構成員一覧</h2>
<xsl:choose>
<xsl:when test="my:user"><ul><xsl:apply-templates select="my:user" /></ul></xsl:when>
<xsl:otherwise><p>誰もいません</p></xsl:otherwise>
</xsl:choose>
</div>
</body>
</xsl:template>
<xsl:template match="my:article">
<div class="article">
<h2><xsl:value-of select="@title" /></h2>
<p class="info">
<xsl:variable name="userid" select="@author"/>
作者 : <xsl:value-of select="/.//my:user[@id=$userid]/@name" /><br />
作成日時 : <xsl:value-of select="@year" />年<xsl:value-of select="@month" />月
<xsl:value-of select="@date" />日(<xsl:value-of select="substring('日月火水木金土',@day,1)" />)
<xsl:value-of select="@hour" />時<xsl:value-of select="@min" />分<br />
(<a href="{@ref}">Link)
</p>
<xsl:apply-templates />
</div>
</xsl:template>
<xsl:template match="xhtml:*">
<xsl:element name="{local-name()}">
<xsl:copy-of select="@*" />
<xsl:apply-templates />
</xsl:element>
</xsl:template>
<xsl:template match="my:user">
<li><xsl:value-of select="@name" /> (<a href="mailto:{@email}">E-mail</a>)</li>
</xsl:template>
</xsl:stylesheet>

長いですね。知らない人が見ればなんのことやらですが、わかれば多少は読める。
ここではXSLTの要素はxsl:、自作要素はmy:、XHTMLの要素は何もつけないかxhtml:をつけて表す。
xsl:output要素でxhtmlとして出力することについていろいろ書いてある。
まぁいろいろ考えるところはあるのだがコピペでOK。これでXHTML 1.1の文書となる。
基本的な使い方はxsl:template要素でmatch要素に書いたXPath式に一致する要素を処理するという調子。
XPath式は生で書けば要素名、@をつければ属性名、/で連ねて書いていくことが基本。
ここで重要なのは名前空間を決めてある場合は必ずmy:testやxhtml:pのように:を使ってかかないといけない。
単にpと書けば何も名前空間が指定されていないpでXHTMLのpということにはならないので注意。
まず/、文書全体に対するテンプレートを決める。
それでその子要素を別のテンプレートに当てはめるということでxsl:apply-templates要素を置くと。
実際にXMLの文書の内容をどうやって変換先の文章に当てはめるのかと言えば、xsl:value-of要素を使う。
select属性でXPath式を書いて、それの内容を当てはめると。
ちなみにselect属性はxsl:apply-templates要素にもあり、これを使えば当てはまる子要素だけ投げられる。
属性値に当てはめるときはhref=”{@ref}”のように{}でXPath式を入れて書けばいいと。
これが基本だが、さらに進んだ使い方もたくさんできる。
まず条件分岐。xsl:choose・xsl:when・xsl:otherwise要素、これはswitch文のようなものです。
これを使えば条件に合うかどうかで表示を変えることができる。
xsl:whenのtest要素にXPath式を書く。このXPath式が真か偽かで動作を変えられると言うこと。
ここではmy:userという式が条件だが、これはmy:usersがあるかないかという条件ね。
あとxsl:if要素という条件分岐の要素があるな。これも使い方はほぼ同じでtestを指定して使う。
author属性の属性値はその人を定義するuser要素のidとしてあると書いたが、
これを解釈してそのユーザーの名前を取り出すことも工夫すればできる。
実はXSLTでは変数を使えて、それはxsl:variable要素で定める。name属性に変数名、select属性に値をXPath式で書く。
これでauthor属性の値をuserid変数に入れて、あとは任意のXPath式で$useridと使えると。
それでuser要素のidがそれであるもののXPath式は少々難しい。
/.//my:user[@id=$userid]/@nameだが、これの意味を読みほどいてみる。
/は文書全体、.はそれ自身、//は子孫、すなわち/.//my:useridで全部のmy:userid要素ということ。
.を入れてるのは.を入れないで///と書くと認識しないから。
[@id=$userid]をつけると条件を満たすそれということになる。
/.//my:user[@id=$userid]で、全部のmy:userid要素のうちid要素が$useridであるもの。ということであとはこれのname属性を取ればいいと。
こんな複雑なことをXPathはやれるんです。えらいもんです。
日時だが、year・month・date・day・hour・min・sec属性でバラバラに入れてある。
というのもXSLT 1.0では日付関係の対応が不十分だから。だからバラバラにしてある。
曜日をどうやって日本語に変えるか、というのでsubstringを使った。これもXPath式です。
XSLT 2.0ではもうちょっとスマートにできると思いますけど。
XHTMLの要素が混ざってるが、これをそのまま移し替えるために、
記事の内容全体をxsl:apply-templates要素で投げて、
xhtmlの要素についてテンプレートを用意しておく。match=”xhtml:*”のテンプレートがそれです。
これも必要ならコピペでOK。こうすれば要素を再構築してくれる。
これを用意しなければXHTMLの要素は中身の文章だけが移し替えられる。


このXSLTスタイルシートを使うということをXML文書に少し書いてあります。
<?xml-stylesheet type=”text/xsl” href=”test-html.xsl”?>ってところです。
こうしておけばMozilla Firefoxなどで見たときXHTMLに変換して見れると。
いままで何が書いたあるかわかりにくかったXML文書が読みやすくなりましたね。
すばらしい!!XSLTが用意されてればよくわからないXML文書でも役立てられますね。
また、初めの方で紹介したSAXONを使ってXSLTスタイルシートを適用してみる。
これはJavaで動くんですね。実は.NET版もあるんだけどね。ここでJava版で話を進める。
Saxon-B 9をダウンロードして、展開するとjarがたくさんある。使い方。

java -jar saxon9.jar -s:test1.xml -xsl:test-html.xsl -o:out.xhtml

これで変換されたのが出てくる。
ところで、さっき取り上げなかったのだが、実は少し工夫がしてある。
というのはxsl:stylesheet要素のexclude-result-prefixes属性ですね。
ここで新しく作られるXML文書で出現しないプレフィックスを書いてある。
あたらしく出来る文書では何もつかないXHTMLの要素ばっかりなのでmyとxhtmlは出てこない。
というわけで書いておくと、新しくできる文書にはmyとxhtmlの名前空間の宣言が移されない。
もしこれを書かなければ使いもしない名前空間宣言が移されてしまうのだね。
ちょっと厄介。しかしこんなおせっかいからしてXMLからXMLを作ることに特化してるなぁと感じさせてくれる。
あとstyle要素のCDATAセクションが新しい文書では消えてるが、
これは大した問題ではない。
XSLTスタイルシートに書いたCDATAセクションは、スタイルシートの解釈の時に消えたわけだが、
もし特殊文字があっても、その特殊文字をエスケープして新しいXML文書に書いてくれる。
というわけでCDATAセクションは消えたが、CDATAセクションの心は残ったままだと。
なので安心してCDATAセクションを使ってくれてOKです。


RSSにも加工できるぞと言ったのでRSSに加工するXSLTスタイルシートも作ってみた。

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xml"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://purl.org/rss/1.0/" xmlns:xhtml="http://www.w3.org/1999/xhtml"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:my="http://libserver.ddo.jp/fdiary/?e=320" exclude-result-prefixes="my">
<xsl:output method="xml" media-type="application/rss+xml" encoding="UTF-8" />
<xsl:template match="/">
<rdf:RDF xml:lang="ja">
<xsl:apply-templates />
</rdf:RDF>
</xsl:template>
<xsl:template match="my:test">
<channel rdf:about="{my:rss}">
<title><xsl:value-of select="my:title" /></title>
<link><xsl:value-of select="my:url" /></link>
<description><xsl:value-of select="my:desc" /></description>
<dc:language>ja</dc:language>
<items>
<rdf:Seq>
<xsl:for-each select="my:article">
<rdf:li rdf:resource="{@ref}" />
</xsl:for-each>
</rdf:Seq>
</items>
</channel>
<xsl:apply-templates select="my:article" />
</xsl:template>
<xsl:template match="my:article">
<item rdf:about="{@ref}">
<link><xsl:value-of select="@ref" /></link>
<title><xsl:value-of select="@title" /></title>
<dc:date>
<xsl:value-of select="@year" />-<xsl:value-of select="@month" />-<xsl:value-of select="@date" />
<xsl:text>T</xsl:text><xsl:value-of select="@hour" />:<xsl:value-of select="@min" />
<xsl:text>:</xsl:text><xsl:value-of select="@sec" /><xsl:value-of select="@zone" />
</dc:date>
<description><xsl:value-of select="substring(normalize-space(.) ,1,30)" /></description>
<xsl:variable name="userid" select="@author"/>
<dc:creator><xsl:value-of select="/.//my:user[@id=$userid]/@name" /></dc:creator>
</item>
</xsl:template>
</xsl:stylesheet>

相変わらず長い。基本はさっきと同じです。
名前空間の宣言は全部ルート要素でまとめて行うのがポイントです。
xsl:for-each要素なんてのもあります。selectで取り出したのをそれぞれ処理していくと。
これ使えば、数字の和とかも求められますよね。なかなかおもしろい。
日時の生成の中でxsl:text要素を使ってますが、これは結構重要なところです。
というのも要素の中に文字を書けば、それはそのままテキストなんですが、
前後に空白があるとそれもまとめてそのままテキストとして出してしまう。
それを避けるためにはxsl:text要素を使うみたいです。だから改行直後だけ使ってるのね。
まぁそんなところです。これでRSSも作れちゃいます。


なかなか便利なものだなと思いましたね。あまりに難しすぎるけど。
Blogのテンプレートとかで使うと楽しそう。ほとんどXSLTで済むなぁと。
ただサーバー側でこれを処理する必要があるかなという気もする。
PHPでXSLTProcessorクラスを使って、DOMツリーを用意しておいたXSLTスタイルシートでもって加工して、
これを表示するというのはなかなかよさそうだ。
まぁXMLの解釈のコストがかかりかかりそうだが。XMLって重いんですよ。
Blognplusのテンプレートを編集してて、都合が悪いところがあれば本体を改造してるが、
そもそもテンプレートの能力がそうも高くないから改造が必要なんであって、
テンプレートの能力がすごく高ければなぁと思うところではあるわけです。
XSLTなら完璧ですね。それぐらいのテンプレートの方がありがたいのではないかなと。
まぁちょっと検討してみようかな。結構面白いですしね。