複数のXMLファイルをXSLTを使って内容に応じた別個のフォルダに振り分ける


はじめに

多数あるXMLファイルを、内容を解析した結果で個別のフォルダに振り分ける方法を考えました。
処理対象のXMLファイル群は、あらかじめ任意のフォルダに格納されています。

解決策

  • XMLを解析する必要があるため、XSLTで処理します。
  • XMLファイルの取得にはXPath2.0のCollection関数を使い、変数に収めます。
  • 変数に収めたXMLファイル群を解析し、新たにフォルダを作成してそこに新たなXMLを処理対象のXMLの名称・内容で作成します。
  • XMLの作成にはXSLTのresult-document要素を使用します。

環境

  • XPath 2.0
  • XSLT 2.0
  • XSLTプロセッサ SaxonHE 9.9.1.6J
  • OS macOS Mojave

XMLファイル

XSLTを動作させるためにはXML(便宜上トリガーXMLと呼びます)が必要です。
そのため、振り分けたいXML群(同じくターゲットXML群)とは別にトリガーXML(ここではtrigger.xml)を用意します。
ターゲットXML群はsamplesフォルダに収め、トリガーXMLと同じディレクトリに置いておきます。

ターゲットXML群

今回は文書構造は共通しているものとします。

cucumber.xml
<?xml version="1.0" encoding="UTF-8"?>
<properties>
 <type>vegetable</type>
 <color>green</color>
</properties>
tomato.xml
<?xml version="1.0" encoding="UTF-8"?>
<properties>
 <type>vegetable</type>
 <color>red</color>
</properties>
strawberry.xml
<?xml version="1.0" encoding="UTF-8"?>
<properties>
 <type>fruit</type>
 <color>red</color>
</properties>
kiwi.xml
<?xml version="1.0" encoding="UTF-8"?>
<properties>
 <type>fruit</type>
 <color>green</color>
</properties>

トリガーXML

トリガーXML自体は処理対象ではないので、整形式でありさえすればよいです。適当にルート要素を一つ作っておきます。

trigger.xml
<?xml version="1.0" encoding="UTF-8"?>
<root/>

XSLT

ターゲットXML群を格納したフォルダのパスを、パラメータとして渡します。トリガーXMLからの相対パスにします。
外部パラメータとしてこのフォルダのパスを与えることも可能です。

XMLFileSorter.xsl
<xsl:stylesheet XMLns:xsl="http://www.w3.org/1999/XSL/Transform"
    XMLns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" version="2.0">
    <!--処理したいXMLを収めたフォルダのパスを収めたパラメータ-->
    <xsl:param name="extPath" required="no" as="xs:string">./samples</xsl:param>

トリガーXMLのルートノードにマッチして、ターゲットXML群を処理するテンプレートです。

    <xsl:template match="/">
        <!--トリガーXMLが配置されたuriを変数varDirnameに格納。後々の処理で使用-->
        <xsl:variable name="varDirname" select="resolve-uri('.',document-uri(/))" as="xs:anyURI"/>

        <!--ターゲットXML群の中身を、変数varXMLFilesにリストとして収める-->
        <xsl:variable name="varXMLFiles" as="element()+">
        <!--内容は後述-->
        </xsl:variable>

        <!--変数varXMLFiles内の各要素を処理して、XMLファイルを適宜フォルダに振り分けて出力する-->
        <xsl:for-each select="$varXMLFiles">
        <!--内容は後述-->
        </xsl:for-each>
    </xsl:template>

テンプレート内の、変数varXMLFilesのパートです。
ターゲットXML群の一つ一つのXMLファイルについて、その名称と内容を取得します。
ひとXMLファイルをひとfile要素とし、その直下のname要素にファイル名を、contents要素にルートノード以下を格納します。

        <!-- 
        collection関数
        第1引数(?の前) フォルダまたはファイルのuri。ここでは$varDirnameと/と$extpathを結合したもの
        第2引数(?の後) オプション。「;」区切りで複数のオプションを指定できる
            select 対象とするファイルを指定。ここではXMLを指定。ワイルドカードが使える
            recurse 再帰の有無。今回は再帰あり。
        -->
        <xsl:variable name="varXMLFiles" as="element()+">
            <!--ターゲットXML群-->
            <xsl:for-each select="collection(concat($varDirname ,'/',$extPath, '?select=*.xml;recurse=yes'))">
                <!--ひとXMLファイル-->
                <file>
                    <!--ファイル名を格納-->
                    <name>
                        <!--ターゲットXMLのuriを / でスプリットしたリストの末尾-->
                        <xsl:value-of select="tokenize(document-uri(/), '/')[last()]"/>
                    </name>
                    <!--ルートノード以下を取得-->
                    <contents>
                        <xsl:sequence select="self::node()"/>
                    </contents>
                </file>
            </xsl:for-each>
        </xsl:variable>

テンプレート内の、変数varXMLFilesを処理してXMLファイルを振り分けるパートです。
ここでは、各ターゲットXMLがもっていたtype要素ならびにcolor要素についてそれらの値ごとにフォルダを作り、その中に新たにXMLファイルを出力します。
フォルダの作成位置はトリガーXMLと同じディレクトリです。
この出力されたXMLファイルは、名称・内容ともにターゲットXMLに等しく、結果としてターゲットXML群を振り分けたことになります。
今回は単一の構造の複数のターゲットXMLを処理していますが、このfor-each内部の記述次第で構造が異なるXMLでも仕分けることが可能です。

        <xsl:for-each select="$varXMLFiles">
            <xsl:variable name="varType" select="child::contents/child::properties/child::type/text()" as="text()"/>
            <xsl:variable name="varColor" select="child::contents/child::properties/child::color/text()" as="text()"/>
            <!--typeごとにフォルダを作り、name要素に格納しておいた名称でファイルを出力-->
            <xsl:result-document
                href="{concat($varDirname ,'/',$varType,'/',child::name)}"
                encoding="UTF-8" method="XML" indent="yes">
                <!--contents要素下に置いたノードを出力-->
                <xsl:sequence select="child::contents/child::node()"/>
            </xsl:result-document>
            <!--colorごとにフォルダを作り、name要素に格納しておいた名称でファイルを出力-->
            <xsl:result-document
                href="{concat($varDirname ,'/',$varColor,'/',child::name)}"
                encoding="UTF-8" method="XML" indent="yes">
                <xsl:sequence select="child::contents/child::node()"/>
            </xsl:result-document>
        </xsl:for-each>

XSLTの全体は以下のとおりです。

XMLFileSorter.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet XMLns:xsl="http://www.w3.org/1999/XSL/Transform"
    XMLns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs" version="2.0">
    <xsl:param name="extPath" required="no" as="xs:string">./samples</xsl:param>
    <xsl:template match="/">
        <xsl:variable name="varDirname" select="resolve-uri('.',document-uri(/))" as="xs:anyURI"/>
        <xsl:variable name="varXMLFiles" as="element()+">
            <xsl:for-each select="collection(concat($varDirname ,'/',$extPath, '?select=*.xml;recurse=yes'))">
                <file>
                    <name>
                        <xsl:value-of select="tokenize(document-uri(/), '/')[last()]"/>
                    </name>
                    <contents>
                        <xsl:sequence select="self::node()"/>
                    </contents>
                </file>
            </xsl:for-each>
        </xsl:variable>
        <xsl:for-each select="$varXMLFiles">
            <xsl:variable name="varType" select="child::contents/child::properties/child::type/text()" as="text()"/>
            <xsl:variable name="varColor" select="child::contents/child::properties/child::color/text()" as="text()"/>
            <xsl:result-document
                href="{concat($varDirname ,'/',$varType,'/',child::name)}"
                encoding="UTF-8" method="XML" indent="yes">
                <xsl:sequence select="child::contents/child::node()"/>
            </xsl:result-document>
            <xsl:result-document
                href="{concat($varDirname ,'/',$varColor,'/',child::name)}"
                encoding="UTF-8" method="XML" indent="yes">
                <xsl:sequence select="child::contents/child::node()"/>
            </xsl:result-document>
        </xsl:for-each>
    </xsl:template>
</xsl:stylesheet>

実行

ターミナルでSaxonを実行します。
$ java -jar path/to/saxonhe.jar -s:path/to/trigger.xml -xsl:path/to/XMLFileSorter.xsl
処理したいXMLを収めたフォルダのパスを指定する場合はパラメータを使用します。
$ java -jar path/to/saxonhe.jar -s:path/to/trigger.xml -xsl:path/to/XMLFileSorter.xsl extPath=path/to/folder

実行結果

このように、typeとしてfruit・vegetable、colorとしてgreen・redのフォルダが作成され、その中に適宜振り分けられたターゲットXMLがコピーされています。