Perlはテキスト処理の高速さを売りとするのは知られていますが、XML解析や処理などにおいても大いに役立ち、過去にも専門書籍が出ていたりします。また、Perlを使用することによって柔軟にXMLを操作でき、XSLTといった便利なテンプレートモジュールを活用できる上、大量のデータでも円滑に処理できます(海外サイトでも、2023年度記事においてXML構築に最適な言語としてJAVAの次に挙げられていました)。
そして、世間で使用されているXMLモジュールですが、有名なものに以下の2つがあります。
- XML::Simple
- XML::LibXML
そして、比較されることも多いこの2つですが、根本的な目的が異なります。
XML::Simpleは単純に、データからXMLツリー構造(以下、データ構造と記述)を作成するモジュールなので操作が至ってシンプルなのですが、大きな問題があります。それはnameで紐づけたXMLタグに対し、出力される順序が保証されないので、順番を整えるためにXML::XSLTを使用するなどして対応します。
$xslt_file = "change.xslt"; #変更予定の形式
my $xslt = XML::XSLT -> new($xslt_file);
$xslt -> transform($xml); #抽出したXML
$output = $xslt -> toString;
ところが、XML::XSLTだと出力結果に対し改行コードが全部消えてしまうので、XSLT上で逐一改行コード制御が必要になり、相当手間がかかります。その上に、XML::XSLTは動作が遅いです。
対するXML::LibXSLTの場合は複雑なXML解析を前提としたモジュールなので、改行コードがしっかり保持され、動きも高速です。ですが、その場合はXML::LibXMLでないとうまくデータを読み込めません。しかも、LibXMLはかなり使用に癖があり、使いづらさがあります。
なので、一長一短の2つのモジュールを合体させてみます。
実際にやってみた
結論からいえば、libXMLでの入力を、ファイルからではなくて、XML::Simpleで出力されたXMLデータ構造を取り込むようにします。
また、LibXMLは世間で紹介されている方法だとうまくいかないことが多いので、海外サイトや公式チュートリアルなどを参考に、構築してみたのが以下のプログラムです。
※各種モジュールはCPANなどからインストールしておいてください。
use XML::Simple;
use XML::LibXSLT;
use XML::LibXML;
$xslt = "test.xslt"; #XSLTファイル
#まずはXML::SimpleでデータをXMLにする
my $x = new XML::Simple;
my $xml = qq(<?xml version="1.0" encoding="UTF-8" ?>\n);
$Val = {
'data' => $data #任意のデータ構造
};
$xml .= $x -> XMLout($Val, RootName => 'root', NoAttr=>1); #XMLデータの抽出
my $xml = XML::LibXML -> load_xml(string => $xml); #データ構造として取り込む(string設定必須)
my $xslt = XML::LibXSLT->new()->parse_stylesheet_file($xslt_file); #XSLTの取り込み
my $result = $xslt->transform($xml); #変換
$output.= $xslt -> output_as_chars($result); #出力
ポイントとしては、load_xmlのときにキー設定をstring
にしておくこと(こうしないと、オブジェクトを読み込めない)、それから出力時にはoutput_as_chars
としておくことです。
これで、各種ファイルで取り込んだXMLの値を自在に変形でき、改行コードを保持したXMLファイルが簡単に作れます。
任意のデータ構造について
任意のデータ構造はハッシュリファレンスで作成していきます(※例は閉業となった新潟市レインボータワーのもの)。
my $data = {};
$data -> {'build'} = "レインボータワー";
$data -> {'tel'} = 025-246-6426;
$data -> {'address'} = "新潟市中央区万代1丁目6−1";
$data -> {'status'} = "閉業";
$data -> {'demorished'} = 2018;
my @traffic = ("自家用車","バス","鉄道");
$data -> {'traffics'} = \@traffic;
このキーが、XML::simpleによってタグに変換されます。
<data>
<address>新潟市中央区万代1丁目6−1</address>
<build>レインボータワー</build>
<demorished>2018</demorished>
<status>2</status>
<tel>025-246-6426</tel>
<traffics>
<0>自家用車</0>
<1>バス</1>
<2>鉄道</2>
</traffics>
</data>
XML::Simpleで準備するのはここまで、あとはlibXMLの出番です。また、ここでデータが順不同になっても全く問題ありません。
XSLTファイルについて
XSLTファイルでの制御も簡潔になります。データソースをXML::Simpleで作成してあるので、XSLTファイルもデータを紐づけるnameプロパティと階層化制御のループ処理以外のこまごまとした関数は、ほとんど使用しなくて済むようになります(フォームのvalueプロパティの要領で、selectプロパティにハッシュのキー値を記述しておき、適切な位置にはめこんでいくだけ)。
今回はハッシュ変数dataに全データを代入している形なので、XMLの階層もdataタグが先頭へ来ることになります。これを逐一記述するのは面倒なので、その場合はtemplate構文を用いるといいでしょう(matchプロパティの値が対応するディレクトリ階層となります)。
<xsl:template match="data">
<概要>
<建物名><xsl:value-of select="build"/></建物名>
<住所><xsl:value-of select="address"/></住所>
<電話番号><xsl:value-of select="tel"/></電話番号>
</概要>
<状況>
<運営状況><xsl:value-of select="status"/></運営状況>
<所在><xsl:value-of select="pos"/></所在>
</状況>
</xsl:template>
XSLTの基本構文
ただ、value-of文に代入するだけで追いつかない部分に対しては、分岐や繰り返しで対応していきます。
分岐
後述するxsl:if構文というのもありますが、これはelseに対応していません。なので、xsl:choose構文を使って分岐していきます。これはSQLのwhen構文によく似ており、when文のtestプロパティの中に検査文を記述します。また、otherwise文がelseの代わりとなります。
それから、動的な値を検査対象としたい場合はvariable文を使って変数定義しておきます。nameプロパティが変数名、selectプロパティが変数に代入する値となります。その変数を展開する場合は$hogeとプレフィックスの付与が必須となります。
<xsl:variable name="flg_open" select="status">
<運営状況>
<xsl:choose>
<xsl:when test="$flg_open='閉業'">
<xsl:value-of select="2">
</xsl:when>
<xsl:when test="$flg_open='休業'">
<xsl:value-of select="1">
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="0">
</xsl:otherwise>
</xsl:choose>
</運営状況>
<xsl:variable name="demorished" select="demorished"/>
<xsl:if test="$demorished!=''">
<所在>現存せず</所在>
</xsl:if>
繰り返し
配列の中身を繰り返し展開したい場合はfor-each構文を使用します。また、その値の展開は"."
で大丈夫のようです(実際はループではなくて、並列処理のようですが)。
<交通手段>
<xsl:for-each select="traffics">
<交通><xsl:value-of select="."/><交通>
</xsl:for-each>
</交通手段>
動作速度について
使用環境に対し、ある程度メモリを開放しておくこと、そしてプログラムのループ文の中に適宜、undefを用いてメモリを開放しておくことがポイントのようです。
こうすることによって、一件あたり2000行のxmlファイルに対し、300件で3分ほどかかってた操作が3000件で数分と十分、実用的な速度となりました(環境による差異は大いに発生します)。
XML::Parserを使ってみた結果
XML::Simpleはそのままだと動作が遅いとのことで、XML::Parserを導入してみたのですが、制御タイムは変わりませんでした。XML::Simpleで操作しているのはハッシュからのXML構築だけなので干渉してこないのだと思います。
応用編(xsdを活用)
libXMLにはもう一つ利点があります。それはxsdといったxmlスキーマを活用することができるので、定義通りのデータ構造かどうか確認することができます。また、これがJSONにはなくXMLが持っているメリットにも挙げられています。
※重要なポイントは改行コードを必ず動作環境に適した形式(今回の場合はLinuxなのでCRLF)にしておくこと(こうしないと、不正なタグという警告表示が出ます)、そして精査用には必ず一度保存したXMLファイルを使用することです(出力データそのものをxsdにかけようとすると、ファイル名が長すぎますという警告が出ます)。それと、精査用のファイルは必ずcloseすること、でないとファイル占有の問題が発生して正しく動作しません。
これで構造上のエラーが発生した場合、$@にリスト表示されます。
open(DATAFILE, "> :utf8 :crlf", "hoge.xml") or die("ERR:$!");
print DATAFILE $output;
close(DATAFILE); #精査前には必ず閉じておくこと
$xsd_file = "xsd/hoge.xsd";
my $schema = XML::LibXML::Schema->new(location => $xsd_file);
my $parser = XML::LibXML->new(XML_LIBXML_LINENUMBERS => 1 );
my $tree = $parser->parse_file("hoge.xml");
eval { $schema->validate($tree) };
if ( $@ ) {
#ここにエラーの場合の処理を書く。
}