レベル: 初級 Jim Dixon, Writer, Freelance
2007年 01月 30日 この連載は、手軽な XML と Perl ソリューションを必要とする読者を対象としたガイドです。意外なことに、多くのケースではたった 1 つのツール、XML::Simple だけで XML を Perl アプリケーションに統合できます。第 1 回では、このツールを取得して使用する方法、そして拡張する方法を紹介します。この記事を読んで Perl で XML を操作する意欲が湧いたら、次の 2 回の記事を読んで新しく身に付けたスキルをさらに磨いてください。
はじめに
Perl と XML についての 3 回連載の第 1 回目では、XML::Simple に焦点を絞ります。Perl プログラマーにとっては、構成ファイルからパラメーターを取得する際に初めて XML を使用するのが一般的でしょう。この記事では、そんなパラメーターを 2 行のコードで読み込む方法を説明します。最初の行で XML::Simple が使用されていることを Perl に伝え、2 番目の行で変数にファイル内の値を設定します。構成ファイルの名前を指定する必要すらありません。それは、XML::Simple がインテリジェントに推測します。
さらに、複雑な例としてペット・ショップを覗いてみます。ペット・ショップのセクションで説明するのは、最小限の作業で無名配列と無名ハッシュが混在する階層型 Perl データ構造に XML ファイルを読み込む方法です。この記事では、元の XML 文書に含まれる情報を Perl がいかに簡潔に変換して再構成するかを披露し、この情報をさまざまな形式に書き直す方法を説明します。
最後に取り上げるのは、XML::Simple の制約事項です。この説明は、連載の次の 2 回の記事で取り上げる話題につながります。その話題とは、より高度な解析、高機能のツールを使用した XML 形式間の変換、そして DOM やその他のメモリー内の形式から XML をシリアライズする手法です。
この記事は主に Perl にあまり慣れていない Perl プログラマーを対象としていますが、XML 文書を一層プログラマチックに操作する方法を検討したいという XML エキスパートにも有意義なものとなるはずです。
手順の開始
手順を開始する前に、Perl をインストールする必要があります。Perl をまだ入手していない場合は、「参考文献」でリンクを参照してください。
次に必要となるのは XML::Simple です。UNIX または Linux を使用している場合は、cpan を使って CPAN から取得するのが最も簡単です。このプロセスを開始するには、リスト 1 に示すコマンドを使って cpan をマシンにインストールします。通常はルートとして実行し、Perl モジュールをすべてのユーザーが使用できるようにします。
リスト 1. cpan のインストールと XML::Simple の取得方法
$ perl -MCPAN -e shell
cpan> ...
cpan> install XML::Simple
cpan> quit
|
上記のコマンドを初めて実行すると長々としたダイアログになりますが、リスト 1 では省略しています。結果的な構成を編集できることを知っておきたいというユーザーのために言っておくと、構成ファイルは /etc/perl/CPAN/Config.pm にあります。
Windows を使用している場合は、同様の手順を PPM (PPM がない場合は、「参考文献」を参照) を使用して行ってください。この場合、モジュールをインストールするコマンドはリスト 2 のようになります。
リスト 2. Windows: PPM を使った XML::Simple の取得方法
$ ppm install XML::Simple |
cpan と ppm はどちらもインストール中に依存関係をチェックし、欠けている依存関係を保管場所からフェッチします。これは、cpan の前提条件ポリシーを「follow」に設定することで自動的に行われます。モジュールは通常、インストール中にコンパイルされ、メッセージ・ページを生成します。これには時間がかかることがありますが、心配する必要はありません。
もう 1 つの前提条件
XML::Simple は XML 文書をハッシュおよびハッシュの配列への参照に変換します。そのため、Perl での参照、ハッシュ、そして配列の相互作用をしっかり理解しておかなければなりません。この前提条件に関してヘルプが必要な場合は、「参考文献」に記載した、優れた Perl のリファレンス・チュートリアルを参照してください。
XML::Simple
Grant McLean の XML::Simple には基本的に、XML テキスト文書から Perl データ構造 (無名ハッシュと無名配列の混合) への変換、そして Perl データ構造から XML テキスト文書への再変換という 2 つの機能があります。
この限定された非常に便利な機能については、次の 2 段階で説明します。まず、最初の段階では XML 形式の構成ファイルからデータをインポートする方法を説明します。次に、地元のペット・ショップでの複雑な例を取り上げ、大規模で複雑な XML ファイルをメモリーに読み込み、従来の XML ツール (XSLT など) で処理できるように変換してからディスクに書き込み直す方法を説明します。
ほとんどの場合は、XML::Simple さえあれば Perl で XML を処理するために必要なすべてのものが揃います。
XML 構成ファイル
ここで、あなたは世界中のプログラマーが毎日のように直面している問題を抱えているとします。それは、多少複雑な構成情報をプログラムに渡す必要がある一方、これをコマンド・ラインの引数で処理するにはあまりにも厄介だという問題です。そこであなたは構成ファイルを使うことにします。XML は結局のところ、このような場合に対処する標準的な方法であるため、ファイルを XML のフォーマットにすることにし、その結果、構成ファイルはリスト 3 のようになりました。このファイルを XML::Simple で処理します。
リスト 3. 構成ファイル part1.xml
<config>
<user>freddy</user>
<passwd>longNails</passwd>
<books>
<book author="Steinbeck" title="Cannery Row"/>
<book author="Faulkner" title="Soldier's Pay"/>
<book author="Steinbeck" title="East of Eden"/>
</books>
</config>
|
コンストラクターの他、XML::Simple には XMLin() および XMLout() という 2 つのサブルーチンがあります。ご想像の通り、最初のサブルーチンは XML ファイルを読み取って参照を返します。該当するデータ構造への参照を指定された 2 番目のサブルーチンは、パラメーターに従ってこれをストリング・フォーマットまたはファイルとして XML 文書に変換します。
通常、XML::Simple は賢明なデフォルト値を持っているため、例えば入力ファイル名を指定しなかった場合は、part1.pl という Perl プログラム (リスト 4) が part1.xml という名前のファイルを読み取ります。
リスト 4. part1.pl
#!/usr/bin/perl -w
use strict;
use XML::Simple;
use Data::Dumper;
print Dumper (XML::Simple->new()->XMLin());
|
part1.pl を実行すると、リスト 5 のような出力結果となります。
リスト 5. part1.pl の出力結果
$VAR1 = {
'passwd' => 'longNails',
'user' => 'freddy',
'books' => {
'book' => [
{
'title' => 'Cannery Row',
'author' => 'Steinbeck'
},
{
'title' => 'Soldier\'s Pay',
'author' => 'Faulkner'
},
{
'title' => 'East of Eden',
'author' => 'Steinbeck'
}
]
}
};
|
上記では、XMLin() によってハッシュへの参照を返しています。これが $config という変数に割り当てられていたとすると、$config->{user} を使ったユーザー名と $config->{passwd} を使ったパスワードが返されることになります。簡潔に記述するという視点を持った人なら、構成ファイルを読み取って単一のパラメーターを 1 行にも満たないコード、XML::Simple->new->{user} で返せることに気付くことでしょう。
このダンプで明らかなのは、XML::Simple は慎重に扱わなければならないということです。
- 第一に、ルート要素の名前がありません。
- 第二に、複数の要素が同じ名前で 1 つの無名配列への参照に組み込まれています。その結果、最初の本のタイトルが @{$config->{books}->{book}}[0]->{title} または 'Cannery Row' となっています。
- 第三に、属性とサブ要素がまったく同じように扱われています。
上記のそれぞれの振る舞いを変更するには、XMLin() のオプションを使用します。オプションについての詳細は、「参考文献」および以下の説明を参照してください。
もっと複雑な例: ペット・ショップ
XML::Simple が得意とするのは、構成ファイルの効率的解析の他にもまだまだたくさんあります。実際、XML::Simple は大規模で複雑な XML ファイルを処理して、整然としたデータ構造にファイルを変換することができます (たいていは変換の内容によく従ったデータ構造になります)。このような変換は Perl では極めて簡単に行えますが、XSLT のような標準的な XML 変換ツールを使うのは困難であったり、あるいは不可能であったりします。
例として、あなたはペット・ショップで働いているとします。このペット・ショップでは、ペットに関する情報を XML ファイルに保管しています。リスト 6 は、その文書のほんの一部です。マネージャーは文書を以下のように変更したいと思っています。
- スペースを節約するため、サブ要素をすべて属性に変更する
- 価格を 20% 引き上げる
- すべての価格の表示形式を同じにして、小数位 2 桁まで表示させる
- リストをソートする
- 誕生日を年齢に置き換える
Perl に対する新たな信頼、そして XSLT では計算ができない (Xpath を使ってシフト演算を行ってみたことがありますか? ) という認識に基づき、あなたはこの変更を XML::Simple で行うことにしました (リスト 6 を参照)。
リスト 6. pets.xml でのペットの一部
<?xml version='1.0'?>
<pets>
<cat>
<name>Madness</name>
<dob>1 February 2004</dob>
<price>150</price>
</cat>
<dog>
<name>Maggie</name>
<dob>12 October 2005</dob>
<price>75</price>
<owner>Rosie</owner>
</dog>
<cat>
<name>Little</name>
<dob>23 June 2006</dob>
<price>25</price>
</cat>
</pets>
|
最初の検討
XML::Simple を使用するという挑戦は、リスト 7 のように始めました。
リスト 7. 大胆な新規 Perl
#!/usr/bin/perl -w
use strict;
use XML::Simple;
use Data::Dumper;
my $simple = XML::Simple->new();
my $data = $simple->XMLin('pets.xml');
# DEBUG
print Dumper($data) . "\n";
# END
|
慎重を期して Data::Dumper でメモリーに読み込まれる内容を調べたところ、リスト 8 のように意外な結果がわかりました。
リスト 8. 実行結果
$VAR1 = {
'cat' => {
'Little' => {
'dob' => '23 June 2006',
'price' => '25'
},
'Madness' => {
'dob' => '1 February 2004',
'price' => '150'
}
},
'dog' => {
'owner' => 'Rosie',
'dob' => '12 October 2005',
'name' => 'Maggie',
'price' => '75'
}
};
|
残念なことに、これでは猫と犬の表示方法がまったく違います。2 匹の猫が名前別にキーが付けられたハッシュに二重にネストされて保管されている一方、犬に関する情報は単一のハッシュに保管され、その名前は他のすべての属性と同じように処理されています。さらに、ルート要素の名前も無くなっています。そこであなたは作業を中断してとっておきの資料 (「参考文献」を参照) を読んだところ、オプションがあることを発見しました。なかでも特に目を引いたのは ForceArray=>1 と KeepRoot=>1 です。前者はネストされたすべての要素を配列として表します。入力時には後者のオプションがルート要素の名前を維持するため、出力時 (後で詳しく説明) にはデータのメモリー内表現にルート要素の名前が含まれることになります。これらの変更を加えた結果が、リスト 9 です。メモリーの使用量は増えますが、こちらのほうがプログラマーにとっては遥かに扱いやすいものとなっています。
リスト 9. オプションを追加した後の Data::Dumper 出力 (読みやすくするためクリーンアップ済み)
$VAR1 = {
'pets' => [
{
'cat' => [
{
'dob' => [ '1 February 2004' ],
'name' => [ 'Madness' ],
'price' => [ '150' ]
},
{
'dob' => [ '23 June 2006' ],
'name' => [ 'Little' ],
'price' => [ '25' ]
}
],
'dog' => [
{
'owner' => [ 'Rosie' ],
'dob' => [ '12 October 2005' ],
'name' => [ 'Maggie' ],
'price' => [ '75' ]
}
]
}
]
};
|
メモリー内データ構造の変換
これで、プログラムで処理しやすい整然とした構造がメモリー内に出来上がりました。要素を属性に変換するという上司の最初の目標を達成するには、配列への参照を置き換える必要があります (リスト 10)。
リスト 10. 単一要素配列への参照
次に、単純な値への参照を置き換えます (リスト 11)。
リスト 11. 単純な値への参照
上記の変更を加えると、XML::Simple はサブ要素ではなく、属性と値のペアを出力するようになります。出力するタイプに複数のインスタンスがある場合 (この例では、猫は 2 匹いるが、犬は 1 匹しかいないという場合)、複数のハッシュを無名ハッシュからなる 1 つの無名配列としてまとめる必要があります。リスト 12 に、このちょっとした魔法を実現する方法を示します。
リスト 12. ハッシュを配列にまとめて要素を属性に変換する方法
sub makeNewHash($) {
my $hashRef = shift;
my %oldHash = %$hashRef;
my %newHash = ();
while ( my ($key, $innerRef) = each %oldHash ) {
$newHash{$key} = @$innerRef[0];
}
return \%newHash;
}
|
個別のペットを記述する XML への参照を指定された上記のコードは、参照をハッシュに「折り畳み」ます。該当タイプのペットが 1 匹しかいない場合は、これで完了です。新しいハッシュへの参照は $data に書き込みます。一方、該当タイプのペットが複数いる場合には、個別のペットを記述する無名ハッシュへの参照が含まれる無名配列への参照を書き込みます。この方法は、リスト 16 の完成したソリューションにある foldType() を見るとわかります。
その他の要件: Perl の醍醐味
上司がその他に要求していることは、リストのソート、20% の価格引き上げ、小数位 2 桁での価格表示、そして誕生日から年齢への置き換えです。最初の目標は、XML::Simple を使えば結果的にデフォルト出力となります。2 番目と 3 番目の目標は、Perl ではどちらもお手のものです。Perl は幸いにも多様な形態を持っているため、価格は 20% の価格引き上げを計算している間は数値ですが、これをストリングとして書き込むと、書き込んだフォーマットがそのまま維持されます。リスト 13 では、このストリングから数値への変換、そして数値からストリングへの再変換を行っています。
リスト 13. 価格のフォーマット変更と引き上げ
sprintf "%6.2f", $amt * (1 + $change) |
誕生日を年齢に変換するのはそれよりも困難でしたが、CPAN を使った簡単なチェックで、Date::Calc には必要なすべての機能 (それ以上の機能も) が揃っていることがわかりました。Decode_Date_EU は、例えば 13 January 2006 のような「ヨーロッパ式」の日付形式を、パッケージが標準として使用する 3 つの要素からなる配列 (YMD) に変換します。この YMD 形式の日付を 2 つ指定すると、Delta_YMD($earlier, $later) は同じ形式で異なる結果 (この例では年齢) になります。残念ながら Delta_YMD には多少のバグが出やすいのが難点で、日や月が負の値になることもありますが、Google で検索すればすぐにパッチが見つかって、すべてが再び順調に運ぶはずです。この処理方法は、完成したソリューション (リスト 16) の deltaYMD が示しています。
猫と犬のディスパッチ
コードを拡張しやすくするには、リスト 14 に示すディスパッチ・テーブルを使います。ディスパッチ・テーブルについては、Jason Dominus の卓越した著書『Higher Order Perl』(「参考文献」を参照) に詳しく説明されています。
リスト 14. ディスパッチ・テーブル
my $DISPATCHER = {
'cat' => sub { foldType(shift); },
'dog' => sub { foldType(shift); },
'hippo' => \&hippoFunc,
};
|
ディスパッチャーには、特定の要素を無名サブルーチンとして処理するための実際のコードを組み込むことも、他の場所で定義された名前付きサブルーチンへの参照を組み込むこともできます。switch-case を別の言語で使用した構文を使用することも可能です。
ここで扱った例には、猫と犬という 2 つの要素タイプしかありませんが、実際の XML 文書にはさまざまなレベルに多数の要素タイプがあるのが普通です。1 つまたは複数のディスパッチ・テーブルを使うと、if ... elsif ... elsif 構文の行が次から次へと続く Perl での場合より、はるかに簡潔で管理しやすくなります。
XML のディスクへの書き込み
出力に関する XML::Simple のデフォルトは一般的に賢明です。XMLout() にオプションを指定していなければ、ストリングが作成されます。出力を代わりにファイルに書き込みたいという場合は、OutputFile オプションを使用してください。他に何も指定しなければ、ルート要素として <opt> が使用されます。メモリー内データ構造にルート要素の名前がある場合には、KeepRoot オプションを追加して、true に設定するか Perl では既知のように 1 を設定します。このすべては、リスト 15 のコードだけで行えます。
リスト 15. XML ファイルへの出力
$simple->XMLout($data,
KeepRoot => 1,
OutputFile => 'pets.fixed.xml',
XMLDecl => "<?xml version='1.0'?>",
);
|
完成したソリューション
リスト 16 に連なる 112 行のコードが上司の要求に応えます。XML::Simple の簡潔さは感動的で、たった 8 行のコードで XML の読み取りと書き込みをこなしています。構造の変換に関わっているのは、残りのコードの半分以下です。
リスト 16. 最終バージョンのコード
#!/usr/bin/perl -w
use strict;
use XML::Simple;
use Date::Calc qw(Add_Delta_YM Decode_Date_EU Delta_Days Delta_YMD);
use Data::Dumper;
my $simple = XML::Simple->new (ForceArray => 1, KeepRoot => 1);
my $data = $simple->XMLin('pets.xml');
my @now = (localtime(time))[5, 4, 3];
$now[0] += 1900; # Perl years start in 1900
$now[1]++; # months are zero-based
sub fixPrice($$) {
my ($amt, $change) = @_;
return sprintf "%6.2f", $amt * (1 + $change);
}
sub deltaYMD($$) {
my ($earlier, $later) = @_; # refs to YMD arrays
my @delta = Delta_YMD (@$earlier, @$later);
while ( $delta[1] < 0 or $delta[2] < 0 ) {
if ( $delta[1] < 0 ) { # negative month
$delta[0]--;
$delta[1] += 12;
}
if ( $delta[2] < 0 ) { # negative day
$delta[1]--;
$delta[2] = Delta_Days(
Add_Delta_YM (@$earlier, @delta[0,1]), @$later);
}
}
return \@delta;
}
sub dob2age($) {
my $strDOB = shift;
my @dob = Decode_Date_EU($strDOB);
my $ageRef = deltaYMD( \@dob, \@now );
my ($ageYears, $ageMonths, $ageDays) = @$ageRef;
my $age;
if ( $ageYears > 1 ) {
$age = "$ageYears years";
} elsif ($ageYears == 1) {
$age = '1 year' . ( $ageMonths > 0 ?
( ", $ageMonths month" . ($ageMonths > 1 ? 's' : '') )
: '');
} elsif ($ageMonths > 1) {
$age = "$ageMonths months";
} elsif ($ageMonths == 1) {
$age = '1 month' . ( $ageDays > 0 ?
( ", $ageDays day" . ($ageDays > 1 ? 's' : '') ) : '');
} else {
$age = "$ageDays day" . ($ageDays != 1 ? 's' : '');
}
return $age;
}
sub makeNewHash($) {
my $hashRef = shift;
my %oldHash = %$hashRef;
my %newHash = ();
while ( my ($key, $innerRef) = each %oldHash ) {
my $value = @$innerRef[0];
if ($key eq 'dob') {
$newHash{'age'} = dob2age($value);
} else {
if ($key eq 'price') {
$value = fixPrice($value, 0.20);
}
$newHash{$key} = $value;
}
}
return \%newHash;
}
sub foldType ($) {
my $arrayRef = shift;
# if single element in array, return simple hash
if (@$arrayRef == 1) {
return makeNewHash(@$arrayRef[0]);
}
# if multiple elements, return array of simple hashes
else {
my @outArray = ();
foreach my $hashRef (@$arrayRef) {
push @outArray, makeNewHash($hashRef);
}
return \@outArray;
}
}
my $dispatcher = {
'cat' => sub { foldType(shift); },
'dog' => sub { foldType(shift); },
};
my @base = @{$data->{pets}};
my %types = %{$base[0]};
my %newTypes = ();
while ( my ($petType, $arrayRef) = each %types ) {
my @petArray = @$arrayRef;
print "type $petType has " . @petArray . " representatives \n";
my $refReturned = &{$dispatcher->{$petType}}( $arrayRef );
$newTypes{$petType} = $refReturned;
}
$data->{pets} = \%newTypes; # overwrite existing data
$simple->XMLout($data,
KeepRoot => 1,
OutputFile => 'pets.fixed.xml',
XMLDecl => "<?xml version='1.0'?>",
);
|
Perl をより簡潔にすることもできますが、上記のコードは Perl で XML を操作するのがいかに簡単であるかも示しています。特にディスパッチャー・テーブルの使用により、さまざまに構成された要素タイプを極めてわかりやすく、管理しやすい方法で処理できるようになります。
制約事項
残念ながら、XML::Simple ではどうしてもできないこともあります。これについては、第 2 回と第 3 回で詳しく説明しますが、XML::Simple には 2 つの主要な制約事項があります。まず、入力時に XML ファイル全体をメモリーに読み込むため、ファイルが大きすぎたり、XML データ・ストリームを扱っている場合にはモジュールを使用できません。2 つ目の制約は、リスト 17 のようにテキストとサブ要素の両方が要素の本体にあるような XML 混合コンテンツは処理できないということです。
リスト 17. 混合コンテンツ
<example>of <mixed/> content</example> |
それではどうやって、ファイルが XML::Simple で処理するには大きすぎると判断するのでしょう。経験則では、XML はメモリーに読み込むと 10 倍に膨らみます。つまり、ワークステーションに数百メガバイトの空きメモリーがあるとすると、XML::Simple は数十メガバイトまでの XML ファイルを処理できるということです。
まとめ
コンピューターの世界で普及した XML は、現代のアプリケーションとオペレーティング・システムに次第に深く溶け込んでいます。そのため、Perl プログラマーにとっては、XML の使用方法を十分に理解することが必須となってきました。XML::Simple のようなツールを使えば、簡単に XML 文書を理解しやすい Perl データ構造に変換し、そのデータ構造を再び XML に変換できます。それぞれの操作は通常、わずか 1 行のコードで実現します。
その一方、XML スペシャリストが XML コンテンツへの変換と応答で Perl がどんなに役に立つかを知れば、嬉しい驚きになるはずです。
第 2 回では、Perl 開発者向けに主流となっている 2 つの XML 解析方法、ツリー解析とイベント駆動型解析を利用する方法を紹介します。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | |  | Jim Dixon は最近サフランシスコに戻り、独立したコントラクターとして Perl と Ruby の驚異的機能に基づく Web 2.0 の立ち上げをアドバイスしています。以前は 7 年間にわたり、イギリスとアメリカのインターネット・サービス・プロバイダーで技術主任を務め、多数の Java/Java EE ソフトウェアを開発しました。 |
記事の評価
|