XML工房

〔作品1〕PCのフォルダツリーを表示する

~第4章~ XSLTスタイルシートの応用

2005年11月号掲載記事

それでは今回は、前回の最後に示した課題を解決したXSLTスタイルシートを紹介しましょう。

■前回の最後に示した課題

(A)フォルダ階層に従ってインデントを加える(「|」や「└」記号を付与する)
(B)フォルダ容量の大きさでソートする
(C)フォルダ容量の大きさであるバイト数を、KBやMBで表示する
(D)指定の階層レベルまで処理し、その先の階層を処理しない

確認のため、対象データであるXMLファイルと、現状の変換結果を、最初に示しておきます。

XMLファイル(※インデントを加えています)
<?xml version="1.0" encoding="Shift_JIS"?>
<Dir Name="My Documents" Size="0">
  <File Name="Default.rdp" Size="1176"/>
  <File Name="desktop.ini" Size="0"/>
  <Dir Name="My Music" Size="0">
   <File Name="Desktop.ini" Size="108"/>
   <Dir Name="Sample Music" Size="0">
    <File Name="Beethoven's Symphony No. 9 (Scherzo).wma" Size="613638"/>
    <File Name="New Stories (Highway Blues).wma" Size="760748"/>
   </Dir>
  </Dir>
  <Dir Name="My Pictures" Size="0">
   <File Name="Desktop.ini" Size="107"/>
   <Dir Name="Sample Pictures" Size="0">
    <File Name="Blue hills.jpg" Size="28521"/>
    <File Name="Sunset.jpg" Size="71189"/>
    <File Name="Water lilies.jpg" Size="83794"/>
    <File Name="Winter.jpg" Size="105542"/>
   </Dir>
  </Dir>
  <Dir Name="My Webs" Size="0">
  <Dir Name="images" Size="0"/>
  <Dir Name="_private" Size="0"/>
  <Dir Name="_vti_cnf" Size="0"/>
  <Dir Name="_vti_pvt" Size="0">
  <File Name="botinfs.cnf" Size="146"/>
  <File Name="bots.cnf" Size="323"/>
  <File Name="service.cnf" Size="1072"/>
  <File Name="service.lck" Size="0"/>
  <File Name="services.cnf" Size="3"/>
  </Dir>
  </Dir>
</Dir>

 
現状の変換結果
My Documents(1,666,367)
My Music(1,374,494)
Sample Music(1,374,386)
My Pictures(289,153)
Sample Pictures(289,046)
My Webs(1,544)
images(0)
_private(0)
_vti_cnf(0)
_vti_pvt(1,544)

そして次のXSLTスタイルシートが、

(A)フォルダ階層に従ってインデントを加える(「|」や「└」記号を付与する)
(B)フォルダ容量の大きさでソートする
(C)フォルダ容量の大きさであるバイト数を、KBやMBで表示する
(D)指定の階層レベルまで処理し、その先の階層を処理しない

という機能を実装したものです。

もちろんスタイルシートの作り方はほかにもあると思います。

<?xml version="1.0" encoding="Shift_JIS"?>
<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text" encoding="Shift_JIS"/>
 
  <!--★[1]この階層レベルまで処理する:
      "-1"は無制限、"0"はルート要素のみ、"1"は1階層まで処理する、… -->
  <xsl:param name="stoplevel" select="-1" />
 
  <!--★[2]エントリポイント-->
  <xsl:template match="/">
   <xsl:apply-templates select="Dir"/>
  </xsl:template>
 
  <!--★[3]Dir要素(ノード)のためのテンプレート(再帰処理)-->
  <xsl:template match="Dir">
 
   <!--★[4]ノードのインデントレベルのための変数:
     初期値は"0"で、インデントレベルに従ってインクリメントされる([11]に対応)-->
   <xsl:param name="indent" select="0"/>
   <!--★[5]ノードに対するインデント文字列のための変数:
     初期値は""で、インデントレベルに従ってインデント文字が追加される([10]に対応)-->
   <xsl:param name="string" select=""/>
 
   <!--★[6]ノードのインデントの出力:ルート要素の場合はインデントの出力は不要-->
   <xsl:if test="$indent != 0">
    <xsl:value-of select="$string"/><xsl:text>└ </xsl:text>
   </xsl:if>
  
   <!--★[7]ノード情報の出力-->
   <!--「フォルダ名(」の出力-->
   <xsl:value-of select="@Name"/>
   <xsl:text>(</xsl:text>
   <!--「フォルダ容量+単位」の出力-->
   <xsl:variable name="size"><xsl:value-of select="sum(.//@Size)"/></xsl:variable>
   <xsl:choose>
    <xsl:when test="$size &gt;= 1024 * 1024">
     <xsl:value-of select="format-number($size div (1024 * 1024), '###.#')"/>
     <xsl:text>MB</xsl:text>
    </xsl:when>
    <xsl:when test="$size &gt;= 1024">
     <xsl:value-of select="format-number($size div (1024), '###.#')"/>
     <xsl:text>KB</xsl:text>
    </xsl:when>
    <xsl:when test="$size = 0">
     <xsl:value-of select="$size"/>
    </xsl:when>
    <xsl:otherwise>
     <xsl:value-of select="$size"/><xsl:text>byte</xsl:text>
    </xsl:otherwise>
   </xsl:choose>
   <!--「)改行」の出力-->
   <xsl:text>)&#xA;</xsl:text>
   <!--★★ここまでがノード情報の出力-->
 
   <!--★[8]子要素としてDir要素があれば、さらに処理する(再帰呼出)-->
   <!--★[9]ただし、次の階層を処理するかしないかを判断する([1]に対応)-->
   <xsl:if test="($stoplevel = -1) or ($indent &lt; $stoplevel)">
     <xsl:apply-templates select="Dir">
     <!--★[10]ノードに対するインデント文字列の設定:
         対象ノード以降に兄弟ノードがなければ「  」を追加
         対象ノード以降に兄弟ノードがあれば「| 」を追加 -->
     <xsl:if test="$indent != 0">
      <xsl:if test="position() = last()">
       <xsl:with-param name="string" select="concat($string, '  ')"/>
      </xsl:if>
      <xsl:if test="position() != last()">
       <xsl:with-param name="string" select="concat($string, '| ')"/>
      </xsl:if>
     </xsl:if>
     <!--★[11]ノードのインデントレベルをインクリメント-->
     <xsl:with-param name="indent" select="$indent + 1"/>
     <!--★[12]フォルダ容量の大きさでソート-->
     <xsl:sort select="sum(.//@Size)" data-type="number" order="ascending"/>
    </xsl:apply-templates>
   </xsl:if>

 </xsl:template>
</xsl:stylesheet>

このスタイルシートを使用して対象のXMLファイルを変換すると、結果は次のようなものになります。

My Documents(1.6MB)
└ My Webs(1.5KB)
| └ images(0)
| └ _private(0)
| └ _vti_cnf(0)
| └ _vti_pvt(1.5KB)
└ My Pictures(282.4KB)
| └ Sample Pictures(282.3KB)
└ My Music(1.3MB)
  └ Sample Music(1.3MB)

ポイントを絞って解説します。

まずは簡単なところから。。
課題(B)に対しては[12]の1行で実現できます。「フォルダ容量の大きさ」を計算するには、XPathのsum関数を使用できます。このスタイルシートでは昇順(ascending)でソートしていますが、order属性値をdescendingに変えれば降順でソートできます。
そして課題(C)は[7]で実装しています。xsl:choose要素を使用し、フォルダ容量の大きさで条件判断し、「MB」で表示するか、「KB」で表示するか、あるいは「0」か「byte」かを決めています。

ちょっと複雑なのが(A)と(D)ですが、まずは(D)から。
XMLファイルの階層構造には、スタイルシートの再帰処理でうまく対応できます。このときノードの階層が深くなるごとに、indent変数の値をインクリメントすれば、処理対象ノードの階層レベルを常に把握することができます。indent変数の宣言が[4]、indent変数のインクリメントが[11]です。そして次の階層のノードを処理するかしないかを判断しているのが、[9]のxsl:if要素です。つまりここの条件判断がFalseであるときは、再帰呼出を行いません。どの階層レベルまで処理するかは、先頭[1]のstoplevel変数の値で決まります。

最後に(A)についてですが、この考え方は(D)の解説と重なります。つまりノードの階層が深くなるごとに、インデント文字を追加していきます。インデント文字の追加は[10]で行っていますが、ここでは対象ノード以降に兄弟ノードがあるかないかによって、追加文字を変えています。この違いは、変換結果の最終行とその他の行での表示の違いとなります。インデント文字を保持するstring変数の宣言が[5]で、実際にインデントを出力しているのが[6]です。

いかがでしょうか。

このようにXSLTスタイルシートの機能はかなり高機能で、XMLファイルからデータを抽出したり、並べ替えたり、または関数を使用した計算などもできます。XSLTスタイルシートをうまく使用できると、プログラミングの負担を大きく削減できると思いますので、ぜひ参考にしてみてください。

「XML工房」のIndexへ戻る

ページトップへ▲

HOMEへ戻る