目次


XSLT でありがちな失敗を避けるために

悪い習慣と引き換えに優れたコードを手に入れる

Comments

XSLT で XML 変換を処理するコードを作成するのは、よく使われる他のどのプログラミング言語を使うよりも遙かに簡単です。そうは言っても、XSLT 言語の構文と処理モデルは従来のプログラミング言語とはかけ離れているため、XSLT の微妙なニュアンスをすべて把握するには時間がかかります。

この記事は、包括的で複雑な XSLT チュートリアルではありません。この記事ではまず初めに、経験の浅い XML および XSLT 開発者にとって最も難解なトピックについて説明し、その後、スタイルシートの全体的な設計とそのパフォーマンスに関連するトピックに話を移します。

名前空間の処理

名前空間を使用しない XML 文書を見ることは少なくなってきていますが、それでもまだ、それぞれの技術で名前空間を適切に使用するという点に関しては混乱があるようです。多くの文書では名前空間で接頭辞を使って要素を表します。この明示的な名前空間の表記が混乱を招くことは通常ありません。リスト 1 に、2 つの名前空間を使用した単純な SOAP メッセージの例を記載します。名前空間の 1 つは SOAP エンベロープ用、もう 1 つは実際のペイロード用です。

リスト 1. 名前空間を使用した XML 文書
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope"> 
 <env:Body>
  <p:itinerary
    xmlns:p="http://travelcompany.example.org/reservation/travel">
   <p:departure>
     <p:departing>New York</p:departing>
     <p:arriving>Los Angeles</p:arriving>
     <p:departureDate>2001-12-14</p:departureDate>
     <p:departureTime>late afternoon</p:departureTime>
     <p:seatPreference>aisle</p:seatPreference>
   </p:departure>
   <p:return>
     <p:departing>Los Angeles</p:departing>
     <p:arriving>New York</p:arriving>
     <p:departureDate>2001-12-20</p:departureDate>
     <p:departureTime>mid-morning</p:departureTime>
     <p:seatPreference/>
   </p:return>
  </p:itinerary>
 </env:Body>
</env:Envelope>

ソース文書の要素には接頭辞があるため、これらの要素が名前空間に属していることは明らかです。このような文書を XSLT で処理するのに苦労する開発者は一人もいないはずです。スタイルシートに、ソース文書の名前空間の宣言を複製するだけで十分だからです。任意の接頭辞を使うこともできますが、通常は標準的な入力文書と同じ接頭辞を使ったほうが、手間がかかりません (リスト 2 を参照)。

リスト 2. 名前空間を使用した文書内の情報にアクセスするスタイルシート
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0"
    xmlns:env="http://www.w3.org/2003/05/soap-envelope"
    xmlns:p="http://travelcompany.example.org/reservation/travel">

<xsl:template match="/">
  Departure location:
  <xsl:value-of select="/env:Envelope/env:Body/p:itinerary/p:departure/p:departing"/>
</xsl:template>

</xsl:stylesheet>

ご覧のように、このコードは名前空間の接頭辞 envp を、ルート要素である xsl:stylesheet で宣言しています。このように、ルート要素での宣言はスタイルシートに含まれるすべての要素が継承するため、あらゆる組み込み XPath 式で名前空間を使用することが可能になります。また、XPath 式では、すべての要素の前に該当する名前空間の接頭辞を追加しなければならないことにも注意してください。いずれかのステップで接頭辞を明記し忘れると、式は何も返さずエラーになります。しかし、このエラーは何も返さないので原因を追跡するのが困難です。

一般に、文書が名前空間を使用することによって問題が生じるのは、名前空間の使用が一目ではわからない場合です。1 つの名前空間に多数の要素がある場合には、xmlns 属性を使用してその名前空間をデフォルト名前空間として定義することができます。しかし、デフォルト名前空間の要素は接頭辞を使わないため、これらの要素が実際には名前空間に含まれているという事実を見逃しやすくなってしまいます。例えば、リスト 3 の XHTML 文書を変換しなければならないとします。

リスト 3. デフォルト名前空間を使用した XHTML 文書
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Example XHTML document</title>
  </head>
  <body>
    <p>Sample content</p>
  </body>
</html>

上記の場合、xmlns=http://www.w3.org/1999/xhtml を見ても気付かなかったり、あるいはこのデフォルト名前空間の宣言の前に他の多数の属性があるために、ワイドスクリーンで表示しているとしても例えば 167 列目の内容を見落してしまったりする可能性があります。html/head/title のような XPath 式を作成するのはごく当然のことですが、そのような式は空のノード・セットを返すことになります。入力文書には title のような要素が含まれていないためです。入力文書に含まれるすべての要素は http://www.w3.org/1999/xhtml 名前空間に属しています。XPath 式には、その点を反映しなければなりません。

XPath で名前空間が設定された要素にアクセスするには、該当する名前空間に接頭辞を定義する必要があります。例えば、サンプル XHTML 文書のタイトルにアクセスするには、XHTML 名前空間に接頭辞を定義し、その接頭辞をすべての XPath ステップで使用します。その方法を、リスト 4 のサンプル・スタイルシートに示します。

リスト 4. 変換では、デフォルト名前空間を使用する入力文書にも名前空間の接頭辞を使用すること
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0"
    xmlns:h="http://www.w3.org/1999/xhtml">

<xsl:template match="/">
  Title of document:
  <xsl:value-of select="/h:html/h:head/h:title"/>
</xsl:template>

</xsl:stylesheet>

ここでも同じく、XPath 式での接頭辞には十分な注意が必要です。1 つでも接頭辞が欠けていると、誤った結果になってしまいます。

残念ながら、XSLT バージョン 1.0 にはデフォルト名前空間と同じような概念はありません。そのため、名前空間の接頭辞を何度も繰り返さなければならなくなります。XSLT バージョン 2.0 ではこの問題が修正されていて、XPath 式で接頭辞が設定されていない要素に適用するデフォルト名前空間を指定することができます。さらに XSLT 2.0では、上記のスタイルシートはリスト 5 のように単純化されます。

リスト 5. XSLT 2.0 での XPath デフォルト名前空間の宣言
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="2.0"
    xpath-default-namespace="http://www.w3.org/1999/xhtml">

<xsl:template match="/">
  Title of document:
  <xsl:value-of select="/html/head/title"/>
</xsl:template>

</xsl:stylesheet>

ノード・テスト text() の誤った使い方

大抵のスタイルシートには、入力文書のリーフ要素を処理する単純なテンプレートがいくつも含まれています。例えば、要素に価格を格納するとします。

<price>124.95</price>

上記の価格に通貨とラベルを追加した上で、HTML で新しいパラグラフとして出力するとします。

<p>Price: 124.95 USD</p>

私はこれまで、この機能を処理するテンプレートが無残にも失敗するスタイルシートを散々目にしてきました。その理由はテンプレート本体での text() ノード・テストの使い方です。そしてそのうちの 99 パーセントは、壊れたコードという結果になります。以下のテンプレートを見て、何が間違っているのかわかりますか?

<xsl:template match="price">
  <p>Price: <xsl:value-of select="text()"/> USD</p>
</xsl:template>

xsl:value-of 命令に含まれる XPath 式は、child::text() という式の省略形です。この式は、<price> 要素の子要素の間にあるすべてのテキスト・ノードを選択します。そのようなノードは一般には 1 つしかありません。その場合には、万事順調に行きます。しかし、<price> 要素の真っただ中にコメントや処理命令を挿入した場合にはどうなるでしょう。

<price>12<!-- I'm a comment. I should be ignored. -->4.95</price>

今度は式が 2 つのテキスト・ノード、124.95 を返すようになっていますが、xsl:value-of の動作は式がノード・セットの最初のノードだけを返すことになっています。そのため、以下のように誤った出力結果になります。

<p>Price: 12 USD</p>

xsl:value-of は単一のノードを期待するため、この命令には単一のノードを返す式を使用しなければなりません。多くの場合、現行ノードへの参照 (.) を使用するのが正しい方法です。上記のサンプル・テンプレートを修正すると、以下の形になります。

<xsl:template match="price">
  <p>Price: <xsl:value-of select="."/> USD</p>
</xsl:template>

これで、現行ノード (.) は <price> 要素全体を返すことになります。xsl:value-of 命令は自動的に、すべてのテキスト・ノードの子孫を連結したノードのストリング値を返します。この方法によって、コメント、処理命令、あるいはサブ要素が含まれているかどうかに関わらず、常に要素のコンテンツ全体を取得できるようになります。

XSLT 2.0 では xsl:value-of 命令の動作が変更され、最初のノードだけでなく、渡されたすべてのノードのストリング値が返されるようになりました。しかしそれでも、テキスト・ノードにコンテンツを返す要素を参照するほうが賢い方法です。こうすれば、マークアップをさらに細分化するために新しいサブ要素を追加したとしても、コードが壊れることはありません。

コンテキスト・ノードを無くさないこと

それぞれのテンプレート (xsl:template) または繰り返し処理 (xsl:for-each) は、現行ノードを使用してインスタンス化されます。あらゆる相対 XPath 式は、この現行ノードから評価されます。XPath 式の先頭を / にすると、式は現行ノードに対して評価されるのではなく、文書のルート・ノードから評価が開始されます。そのような式は現行ノードと関連付けられることはなく、常に同じ結果となります。

リスト 6 の単純な請求書を処理する場合を考えてみてください。

リスト 6. サンプル請求書
<invoice>
  <item>
    <description>Pilsner Beer</description>
    <qty>6</qty>
    <unitPrice>1.69</unitPrice>
  </item>
  <item>
    <description>Sausage</description>
    <qty>3</qty>
    <unitPrice>0.59</unitPrice>
  </item>
  <item>
    <description>Portable Barbecue</description>
    <qty>1</qty>
    <unitPrice>23.99</unitPrice>
  </item>
  <item>
    <description>Charcoal</description>
    <qty>2</qty>
    <unitPrice>1.19</unitPrice>
  </item>
</invoice>

現行ノードの相対式を作成し忘れると、いとも簡単に、誤ったスタイルシートになってしまいます。リスト 7 はその一例です。

リスト 7. コンテキストが失われた不適切なスタイルシートの例
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<xsl:template match="/">
  <html>
    <head>
      <title>Invoice</title>
    </head>
    <body>
      <table>
        <xsl:for-each select="/invoice/item">
          <tr>
            <td><xsl:value-of select="/invoice/item/description"/></td>             
            <td><xsl:value-of select="/invoice/item/qty"/></td>
            <td><xsl:value-of select="/invoice/item/unitPrice"/></td>
          </tr>          
        </xsl:for-each>
      </table>      
    </body>
  </html>  
</xsl:template>

xsl:for-each に含まれる式 /invoice/item は正常に請求書内のすべての項目を選択するものの、xsl:for-each 内部の式が誤っています。これらの式は / で始まっていることから、絶対式となっているためです。絶対式は現行ノード、つまり現在処理されている項目、には依存しないため、これらの式は常に最初の項目の記述、量、価格を返すことになります (前のセクションで、xsl:value-of はノード・セットの最初のノードのみを返すと説明したことを思い出してください)。

この問題を簡単に修正する方法は、リスト 8 のように xsl:for-each 内部で相対式を使用することです。

リスト 8. 繰り返し処理の本体内部での相対 XPath 式の使用
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<xsl:template match="/">
  <html>
    <head>
      <title>Invoice</title>
    </head>
    <body>
      <table>
        <xsl:for-each select="/invoice/item">
          <tr>
            <td><xsl:value-of select="description"/></td>             
            <td><xsl:value-of select="qty"/></td>
            <td><xsl:value-of select="unitPrice"/></td>
          </tr>          
        </xsl:for-each>
      </table>      
    </body>
  </html>  
</xsl:template>

</xsl:stylesheet>

XSLT は、共通タスクを自動化することを得意とします。例えば共通タスクの 1 つとして挙げられるのは、目次を作成するという単調で手のかかるタスクです。XSLT ではこうした目次を自動的に生成することができます。必要な作業は、アンカーを生成し、これらのアンカーを指すリンクを生成するだけです。アンカーを HTML で作成するには、id 属性に固有の ID を含めればよいだけのことです。

<div id="label">…</div>

アンカーに戻るリンクを作成するときには、フラグメント ID (#) の後に label を追加し、このリンクが文書内の特定の場所を指していることを示してください。

<a href="#label">link to …</a>

実際のスタイルシートは通常、generate-id() 関数、または入力文書に指定された実際の ID を使ってラベルとリンクを生成します。

このリンクの設定タスクに伴う問題は、実はXSLT 自体ではなく、一部の「賢過ぎる」Web ブラウザーにあります。これまで見てきたスタイルシートのなかには、誤ってフラグメント ID (#) がアンカーに追加されているものが多くありました。しかもこのようなスタイルシートの出力が、Windows® Internet Explorer® でしかテストされないという例が数多くありました。しかし、Internet Explorer は HTML コードのさまざまなエラーから回復することが可能です。そのため、ユーザーにとってはリンクに何の問題もないように見えてしまいます。ところが同じページを Mozilla Firefox や Opera などのブラウザーで試してみると、リンク切れが発生します。これらのブラウザーは余分な # に対処できないためです。

この類の問題を回避するために、スタイルシートが生成した出力は複数のブラウザーでテストするのが最善の策です。

コンテキスト・ノードを変更してスタイルシートを単純化すること

ビジネス文書やデータ指向の XML を処理する場合に一般的な方法は、テンプレート・メカニズムに大幅に依存することはせずに、必要なコンテンツだけを選択し、それを大きな 1 つのテンプレートの中で目的のフォームにアセンブルすることです。例えば、リスト 9 の請求書を処理するとします。

リスト 9. 複雑な構造の請求書
<Invoice>
  <ID>IN 2003/00645</ID>
  <IssueDate>2003-02-25</IssueDate>
  <TaxPointDate>2003-02-25</TaxPointDate>
  <OrderReference>
    <BuyersID>S03-034257</BuyersID>
    <SellersID>SW/F1/50156</SellersID>
    <IssueDate>2003-02-03</IssueDate>
  </OrderReference>
  <BuyerParty>
    <Party>
      <Name>Jerry Builder plc</Name>
      <Address>
	<StreetName>Marsh Lane</StreetName>
	<CityName>Nowhere</CityName>
	<PostalZone>NR18 4XX</PostalZone>
	<CountrySubentity>Norfolk</CountrySubentity>
      </Address>
      <Contact>Eva Brick</Contact>
    </Party>
  </BuyerParty>
  …
</Invoice>

この文書を処理する標準的なスタイルシート (リスト 10 を参照) では、大量の情報が入力 XML ツリーの同じ部分にあることから、XPath 式のなかに同じパスの繰り返しが大量に含まれることになります。

リスト 10. XPath コードが何度も繰り返されている単純なスタイルシート
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<xsl:template match="/">
  <html>
    <head>
      <title>Invoice #<xsl:value-of select="/Invoice/ID"/></title>
    </head>
    <body>
      <h1>Invoice #<xsl:value-of select="/Invoice/ID"/>
          issued on <xsl:value-of select="/Invoice/IssueDate"/></h1>

      <div>
        <h2>Buyer:</h2>

        <p>
          <b><xsl:value-of select="/Invoice/BuyerParty/Party/Name"/></b>
        </p>

        <p>Address:<br/>
          <xsl:value-of select="/Invoice/BuyerParty/Party/Address/StreetName"/><br/>
          <xsl:value-of select="/Invoice/BuyerParty/Party/Address/CityName"/><br/>
          <xsl:value-of select="/Invoice/BuyerParty/Party/Address/PostalZone"/>
        </p>
        
        <p>Contact person: <xsl:value-of select="/Invoice/BuyerParty/Party/Contact"/></p>
        …
      </div>
    </body>
  </html>  
</xsl:template>

</xsl:stylesheet>

このような XPath 式での繰り返しは、何度も同じ作業を繰り返さなくてはならないため骨が折れます。さらに将来的な負担の種にもなります。入力文書の構造を少しでも変更すると、多くの個所で式を調整する必要が出てくるからです。スタイルシートは、式の共通部分を取り除くことで単純化することができます。それには、現行ノードを変更する命令、xsl:templatexsl:for-each を使用してください。リスト 11 のスタイルシートを見ると、そこに含まれる繰り返しの情報は大幅に少なくなっています。

リスト 11. 共通する XPath パスを取り除いたスタイルシート
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">

<xsl:template match="Invoice">
  <html>
    <head>
      <title>Invoice #<xsl:value-of select="ID"/></title>
    </head>
    <body>
      <h1>Invoice #<xsl:value-of select="ID"/>
          issued on <xsl:value-of select="IssueDate"/></h1>

      <div>
        <h2>Buyer:</h2>

        <xsl:for-each select="BuyerParty/Party">
          <p>
            <b><xsl:value-of select="Name"/></b>
          </p>

          <xsl:for-each select="Address">
            <p>Address:<br/>
              <xsl:value-of select="StreetName"/><br/>
              <xsl:value-of select="CityName"/><br/>
              <xsl:value-of select="PostalZone"/>
            </p>
          </xsl:for-each>

          <p>Contact person: <xsl:value-of select="Contact"/></p>
        </xsl:for-each>

        …
      </div>
    </body>
  </html>  
</xsl:template>

</xsl:stylesheet>

上記では、テンプレートの突き合わせを / から Invoice へ変更して、このルート要素名を各 XPath 式の先頭で繰り返さなくても済むようにしました。テンプレート内部では、xsl:for-each を使用して現行ノードを一時的に買い手 (BuyerParty/Party) に変更し、さらにその内部で住所 (Address) に変更しています。繰り返しではない要素に対して xsl:for-each を使うのは奇妙に見えるかもしれませんが、問題は全くありません。繰り返し処理の本体は一度しか呼び出されませんが、現行ノードが変更されていることから、入力を繰り返す手間は大幅に省かれます。

混合コンテンツの処理

一般に、文書指向の XML には混合コンテンツがあります。混合コンテンツとは、要素とテキスト・ノードの両方が要素の子として含まれる構造のことです。混合コンテンツの典型的な例としては、テキストに強調やリンクなどのマークアップが追加されているパラグラフが挙げられます。

<para><emphasis>Douglas Adams</emphasis> was an English author, comic
radio dramatist, and musician. He is best known as the author of the
<link url="http://en.wikipedia.org/wiki/The_Hitchhiker's_Guide_to_the_Galaxy">Hitchhiker's
Guide to the Galaxy</link> series.</para>

混合コンテンツで肝心な点は、文書の順に処理することです。そうでないと、センテンスの順序が変更されて支離滅裂な出力になってしまいます。混合コンテンツの処理をするのに最もふさわしい方法は、混合コンテンツを持つ要素またはその子すべてで xsl:apply-templates を呼び出すことです。こうすれば、それ以降のテンプレートで強調やリンクなどの組み込みマークアップを処理できるようになります。

混合コンテンツの処理に「選り好み」の手法を適用するスタイルシートを多く見かけますが、この手法は通常の構造の文書には最適なものの、混合コンテンツの内部構造はさまざまに異なるため、この手法で正しく処理することは困難です。混合コンテンツを見かけた場合は必ず、単純な xsl:value-ofxsl:for-each は忘れて、興味の対象をテンプレートに移すようにしてください。

スタイルシートの非効率性

かなり少数のデータセット (例えば、Web アプリケーションのビュー層など) に作用する小規模な変換を作成する場合、このプロセスは残りの処理に比べるとわずかな部分でしかないため、おそらく変換自体のパフォーマンスにはあまり関心を持たないことでしょう。しかし、XSLT スタイルシートが複雑な操作を行ったり、大きなサイズの入力文書を扱うとしたら、スタイルシートで使用する構成体がパフォーマンスにどのような影響を与えるかについて考え始める必要があります。

XSLT コードは特定の XSLT 実装に依存するため、XSLT コードだけを見て、スタイルシートがコードを適切に処理できるかどうか、そしてある種の最適化を使用して処理速度を向上させることが可能かどうかを判断するのは一般的に困難です。

そうは言っても、実際のスタイルシートには省略するとよいものがいくつかあります。例えば、下位軸 (//) の使用には十分慎重になってください。// を使用すると、XSLT プロセッサーはツリー (またはサブツリー) 全体をその最下位層まで検査することになるため、大規模な文書では非常にコストのかかる操作になり得ます。このような場合には、より具体的な式を作成して、ノードを検索する場所を明示的に指定することが賢明です。例えば、買い手の住所を取得するには、//BuyerParty//Address//Address とするのではなく、/Invoice/BuyerParty/Party/Address とします。最初のバリアント型によって、評価中に検査しなければならないノードが絞られるため、処理速度が遙かに向上します。また、このようにターゲットを絞った式は、文書構造が進化して、名前は同じでも異なる意味を持つ新しい要素が入力文書の異なるコンテキストに追加されたとしても、影響されにくくなるという効果があります。

大量の検索を行うときのもう 1 つの秘訣は、xsl:key で検索キーを定義し、その上で key() 関数を使って検索を実行することです。

他にもさまざまな最適化を行えますが、その効果については、使用する XSLT プロセッサーによって異なります。

XSLT 1.0 と 2.0 のどちらを使用するか

どのバージョンの XSLT を使用するかを決定する要素は複数ありますが、私は概して、XSLT 2.0 を使用することをお薦めします。この最新バージョンの XSLT には、多くのタスクを大幅に単純化する新しい命令と関数が数多くあります (簡潔でわかりやすいコードは常に管理しやすいものです)。その上、XSLT 2.0 ではスキーマ対応のスタイルシートを作成し、スキーマを使って入力文書と出力文書の両方の妥当性を検証することもできます。スキーマ対応のスタイルシートでは、スキーマに含まれる情報を使用して、スタイルシートで自動的に特定タイプのエラーと誤りを検出します。

まとめ

この記事では、XSLT で問題になりがちないくつかの領域を取り上げました。読者の皆さんが XSLT の機能についての理解を深めたこと、そしてこれからは、より優れた XSLT スタイルシートを作成できるようになることを願います。


ダウンロード可能なリソース


関連トピック

  • XSLTはどのような言語か」(Michael Kay 著、developerWorks、2005年4月): この分析と、XSLT の役割と設計についての概要を読んで、XSLT を理解してください。
  • IBM XML 認証: XML や関連技術の IBM 認定技術者になる方法について調べてください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML
ArticleID=367268
ArticleTitle=XSLT でありがちな失敗を避けるために
publish-date=12192008