1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PerlでおてがるXML(2つのモジュールを合せ技にしてみた)

Last updated at Posted at 2024-06-14

Perlはテキスト処理の高速さを売りとするのは知られていますが、XML解析や処理などにおいても大いに役立ち、過去にも専門書籍が出ていたりします。また、Perlを使用することによって柔軟にXMLを操作でき、XSLTといった便利なテンプレートモジュールを活用できる上、大量のデータでも円滑に処理できます(海外サイトでも、2023年度記事においてXML構築に最適な言語としてJAVAの次に挙げられていました)。

そして、世間で使用されているXMLモジュールですが、有名なものに以下の2つがあります。

  • XML::Simple
  • XML::LibXML

そして、比較されることも多いこの2つですが、根本的な目的が異なります。

XML::Simpleは単純に、データからXMLツリー構造(以下、データ構造と記述)を作成するモジュールなので操作が至ってシンプルなのですが、大きな問題があります。それはnameで紐づけたXMLタグに対し、出力される順序が保証されないので、順番を整えるためにXML::XSLTを使用するなどして対応します。

pl
$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などからインストールしておいてください。

pl
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'} = 0252466426;
$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プロパティの値が対応するディレクトリ階層となります)。

xslt
<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とプレフィックスの付与が必須となります。

xslt
<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>
</運営状況>
xslt
<xsl:variable name="demorished" select="demorished"/>
<xsl:if test="$demorished!=''">
  <所在>現存せず</所在>
</xsl:if>

繰り返し

配列の中身を繰り返し展開したい場合はfor-each構文を使用します。また、その値の展開は"."で大丈夫のようです(実際はループではなくて、並列処理のようですが)。

xslt
<交通手段>
<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 ( $@ ) {
	  #ここにエラーの場合の処理を書く。
	}
1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?