コンテンツにスキップ

XSLTによる基本的な変換例

最近、プライベートでXSLTを使っている。 変換でよく使うパターンがあるけれど、毎回その方法を考えたり調べたりするのに時間がかかるので、それらをここにまとめておくことにした。

以下の例はすべてPythonのlxmlライブラリを使って動作を確認した。 lxmlの公式ドキュメントによると、lxmlはXSLT 1.0をサポートしているとのこと。 lxmlのXSLTのサポート状況については以下のリンク先を参照。

XSLT 2.0や3.0の機能を使用すればもっと簡潔に書くこともできるかもしれないけれど、ここではlxmlがサポートしている範囲の機能のみを使用している。

要素をカンマで結合する

兄弟要素のテキストをカンマで結合したい。

例えば、以下のようなXMLが与えられているとする。

input.xml
<?xml version="1.0" encoding="UTF-8"?>
<users>
  <user>
    <name>Alice</name>
    <groups>
      <group>admin</group>
      <group>users</group>
      <group>guests</group>
    </groups>
  </user>
  <user>
    <name>Bob</name>
    <groups>
      <group>users</group>
      <group>guests</group>
    </groups>
  </user>
  <user>
    <name>Charlie</name>
    <groups>
      <group>guests</group>
    </groups>
  </user>
</users>

ここで、各user要素はユーザーを表している。 user要素の子要素であるname要素は、ユーザーの名前を表している。 また、user要素の子要素であるgroups要素は、ユーザーが所属するグループを表している。 ユーザーは複数のグループに所属することができる。

このとき、各group要素のテキストをカンマで結合して、以下のようなXMLを出力したい。

output.xml
<?xml version="1.0" encoding="UTF-8"?>
<users>
  <user name="Alice" groups="admin,users,guests" />
  <user name="Bob" groups="users,guests" />
  <user name="Charlie" groups="guests" />
</users>

この場合、以下のようなXSLTで変換できる。

transform.xslt
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:template match="/">
    <xsl:apply-templates select="users" />
  </xsl:template>

  <xsl:template match="users">
    <users>
      <xsl:apply-templates select="user" />
    </users>
  </xsl:template>

  <xsl:template match="user">
    <user>
      <xsl:attribute name="name">
        <xsl:value-of select="name" />
      </xsl:attribute>
      <xsl:attribute name="groups">
        <xsl:call-template name="join-elements">
          <xsl:with-param name="elements" select="groups/group" />
          <xsl:with-param name="separator" select="','" />
        </xsl:call-template>
      </xsl:attribute>
    </user>
  </xsl:template>

  <xsl:template name="join-elements">
    <xsl:param name="elements" />
    <xsl:param name="separator" />
    <xsl:for-each select="$elements">
      <xsl:value-of select="text()" />
      <xsl:if test="position() != last()">
        <xsl:value-of select="$separator" />
      </xsl:if>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

テキストをカンマで分割する

上の「要素をカンマで結合する」とは逆に、カンマで区切られたテキストを要素に分割したい。

例えば、以下のようなXMLが与えられているとする。

input.xml
<?xml version="1.0" encoding="UTF-8"?>
<users>
  <user name="Alice" groups="admin,users,guests" />
  <user name="Bob" groups="users,guests" />
  <user name="Charlie" groups="guests" />
</users>

このとき、各user要素ごとにgroups属性の値をカンマで分割して、以下のようなXMLを出力したい。

output.xml
<?xml version="1.0" encoding="UTF-8"?>
<users>
  <user>
    <name>Alice</name>
    <groups>
      <group>admin</group>
      <group>users</group>
      <group>guests</group>
    </groups>
  </user>
  <user>
    <name>Bob</name>
    <groups>
      <group>users</group>
      <group>guests</group>
    </groups>
  </user>
  <user>
    <name>Charlie</name>
    <groups>
      <group>guests</group>
    </groups>
  </user>
</users>

この場合、以下のようなXSLTで変換できる。

transform.xslt
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:template match="/">
    <xsl:apply-templates select="users" />
  </xsl:template>

  <xsl:template match="users">
    <users>
      <xsl:apply-templates select="user" />
    </users>
  </xsl:template>

  <xsl:template match="user">
    <user>
      <name>
        <xsl:value-of select="@name" />
      </name>
      <groups>
        <xsl:call-template name="split-text">
          <xsl:with-param name="text" select="@groups" />
          <xsl:with-param name="separator" select="','" />
          <xsl:with-param name="element-name" select="'group'" />
        </xsl:call-template>
      </groups>
    </user>
  </xsl:template>

  <xsl:template name="split-text">
    <xsl:param name="text" />
    <xsl:param name="separator" />
    <xsl:param name="element-name" />
    <xsl:choose>
      <xsl:when test="contains($text, $separator)">
        <xsl:element name="{$element-name}">
          <xsl:value-of select="substring-before($text, $separator)" />
        </xsl:element>
        <xsl:call-template name="split-text">
          <xsl:with-param name="text" select="substring-after($text, $separator)" />
          <xsl:with-param name="separator" select="$separator" />
          <xsl:with-param name="element-name" select="$element-name" />
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:element name="{$element-name}">
          <xsl:value-of select="$text" />
        </xsl:element>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

要素の位置情報を出力する

各要素の親要素の中でのテキスト位置を出力したい。

例えば、以下のようなXMLが与えられているとする。

input.xml
<?xml version="1.0" encoding="UTF-8"?>
<functions>
  <function><return-type>FILE *</return-type><name>fopen</name>(const char *<param>filename</param>, const char *<param>mode</param>);</function>
  <function><return-type>int</return-type> <name>fclose</name>(FILE *<param>stream</param>);</function>
</functions>

ここで、各function要素はC言語の関数を表している。 function要素の子要素は、関数の戻り値の型、関数名、引数名などを表している。 ただし、function要素は孫要素を持たないものとする。

このとき、function要素の子要素に対して、function要素の中でのテキスト位置を取得し、以下のようなXMLを出力したい。 ただし、テキスト位置は1から始まるものとする。 (XSLTの文字列関数のインデックスは1始まりのため、それに合わせてここでも1始まりとしている。)

output.xml
<?xml version="1.0" encoding="UTF-8"?>
<functions>
  <function>
    <code>FILE *fopen(const char *filename, const char *mode);</code>
    <tokens>
      <token name="return-type" start="1" length="6">FILE *</token>
      <token name="name" start="7" length="5">fopen</token>
      <token name="param" start="25" length="8">filename</token>
      <token name="param" start="47" length="4">mode</token>
    </tokens>
  </function>
  <function>
    <code>int fclose(FILE *stream);</code>
    <tokens>
      <token name="return-type" start="1" length="3">int</token>
      <token name="name" start="5" length="6">fclose</token>
      <token name="param" start="18" length="6">stream</token>
    </tokens>
  </function>
</functions>

この場合、以下のようなXSLTで変換できる。

transform.xslt
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:template match="/">
    <xsl:apply-templates select="functions" />
  </xsl:template>

  <xsl:template match="functions">
    <functions>
      <xsl:apply-templates select="function" />
    </functions>
  </xsl:template>

  <xsl:template match="function">
    <function>
      <code>
        <xsl:value-of select="string()" />
      </code>
      <tokens>
        <xsl:apply-templates select="*" mode="tokenize" />
      </tokens>
    </function>
  </xsl:template>

  <xsl:template match="*" mode="tokenize">
    <token>
      <xsl:attribute name="name">
        <xsl:value-of select="name()" />
      </xsl:attribute>
      <xsl:attribute name="start">
        <xsl:variable name="preceding-text">
          <xsl:for-each select="preceding-sibling::* | preceding-sibling::text()">
            <xsl:value-of select="." />
          </xsl:for-each>
        </xsl:variable>
        <xsl:value-of select="string-length($preceding-text) + 1" />
      </xsl:attribute>
      <xsl:attribute name="length">
        <xsl:value-of select="string-length(.)" />
      </xsl:attribute>
      <xsl:value-of select="." />
    </token>
  </xsl:template>
</xsl:stylesheet>

位置情報をもとに要素を作成する

上の「要素の位置情報を出力する」とは逆に、位置情報をもとに要素を作成したい。

例えば、以下のようなXMLが与えられているとする。

input.xml
<?xml version="1.0" encoding="UTF-8"?>
<functions>
  <function>
    <code>FILE *fopen(const char *filename, const char *mode);</code>
    <tokens>
      <token name="return-type" start="1" length="6">FILE *</token>
      <token name="name" start="7" length="5">fopen</token>
      <token name="param" start="25" length="8">filename</token>
      <token name="param" start="47" length="4">mode</token>
    </tokens>
  </function>
  <function>
    <code>int fclose(FILE *stream);</code>
    <tokens>
      <token name="return-type" start="1" length="3">int</token>
      <token name="name" start="5" length="6">fclose</token>
      <token name="param" start="18" length="6">stream</token>
    </tokens>
  </function>
</functions>

ここで、token要素のstart属性は開始位置、length属性は長さを表している。

このとき、start、length属性の情報をもとに要素を作成して、以下のようなXMLを出力したい。

output.xml
<?xml version="1.0" encoding="UTF-8"?>
<functions>
  <function><return-type>FILE *</return-type><name>fopen</name>(const char *<param>filename</param>, const char *<param>mode</param>);</function>
  <function><return-type>int</return-type> <name>fclose</name>(FILE *<param>stream</param>);</function>
</functions>

この場合、以下のようなXSLTで変換できる。

transform.xslt
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:template match="/">
    <xsl:apply-templates select="functions" />
  </xsl:template>

  <xsl:template match="functions">
    <functions>
      <xsl:apply-templates select="function" />
    </functions>
  </xsl:template>

  <xsl:template match="function">
    <function>
      <xsl:call-template name="detokenize">
        <xsl:with-param name="code" select="code/text()" />
        <xsl:with-param name="tokens" select="tokens/token" />
      </xsl:call-template>
    </function>
  </xsl:template>

  <xsl:template match="*" name="detokenize">
    <xsl:param name="code" />
    <xsl:param name="tokens" />
    <xsl:param name="position" select="1" />
    <xsl:choose>
      <xsl:when test="count($tokens) = 0">
        <xsl:value-of select="substring($code, $position)" />
      </xsl:when>
      <xsl:otherwise>
        <xsl:variable name="tokens-head" select="$tokens[1]" />
        <xsl:variable name="tokens-tail" select="$tokens[position() > 1]" />

        <xsl:value-of select="substring($code, $position, $tokens-head/@start - $position)" />
        <xsl:element name="{$tokens-head/@name}">
          <xsl:value-of select="substring($code, $tokens-head/@start, $tokens-head/@length)" />
        </xsl:element>

        <xsl:call-template name="detokenize">
          <xsl:with-param name="code" select="$code" />
          <xsl:with-param name="tokens" select="$tokens-tail" />
          <xsl:with-param name="position"
            select="$tokens-head/@start + $tokens-head/@length" />
        </xsl:call-template>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>