目次


PHP で XML をプル型構文解析する

メモリー効率の良いストリーム処理を作成する

Comments

PHP 5 では、XML (Extensible Markup Language) を読むための新しいクラス、XMLReader が導入されています。XMLReader は SimpleXML や DOM (Document Object Model) とは異なり、ストリーミング・モードで動作します。つまり XMLReader は、文書を最初から最後まで読み取ります。しかも文書の最後にある内容を見てからでなくても、文書の最初にある内容に対して作業を開始することができます。このため、XMLReader は非常に高速で非常に効率的な上、ごくわずかなメモリーしか必要としません。処理対象の文書が大きければ大きいほど、XMLReader が重要になってきます。

XMLReader は SAX (Simple API for XML) とは異なり、プッシュ・パーサーではなくプル・パーサーです。つまりプログラムが主導権を持っています。プログラムは、パーサーが処理をする時に何を処理しているかを通知されるのではなく、逆にパーサーに対して、文書の次の部分をいつフェッチしに行くべきかを命令するのです。コンテンツに反応するのではなく、コンテンツを要求します。また別の考え方で言えば、XMLReader はオブザーバー・デザイン・パターンの実装ではなく、イテレーター・デザイン・パターンの実装と言うことができます。

例題

単純な例から始めましょう。XML-RPC リクエストを受信し、レスポンスを生成するPHP スクリプトを書くとしましょう。もっと具体的に、そのリクエストはリスト 1 のようなものだとしましょう。この文書のルート要素は methodCall であり、この要素には methodName 要素と params 要素が含まれています。メソッド名は sqrt です。params 要素は param 要素を 1 つ含み、この param 要素は double を 1 つ含み、この double の平方根が求められています。名前空間は使われていません。

リスト 1. XML-RPC リクエスト
<?xml version="1.0"?>
<methodCall>
<methodName>sqrt</methodName>
<params>
<param>
<value><double>36.0</double></value>
</param>
</params>
</methodCall>

PHP スクリプトでは、次のことを行う必要があります。

  1. メソッド名をチェックし、それが sqrt (このスクリプトが処理方法を知っている唯一のメソッド) ではなかったらフォールト・レスポンスを生成する。
  2. 引数を探し、見つからなかったり、型が誤っていたりしたら、フォールト・レスポンスを生成する。
  3. それ以外の場合には、平方根を計算する。
  4. リスト 2 の形式で結果を返す。
リスト 2. XML-RPC レスポンス
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><double>6.0</double></value>
</param>
</params>
</methodResponse>

ではこれを作る手順を追ってみましょう。

パーサーを初期化し、文書をロードする

最初のステップは、新しいパーサー・オブジェクトを作成することです。これは単純です。

$reader = new XMLReader();

次に、このオブジェクトに構文解析用のデータを与える必要があります。XML-RPC の場合、これは HTTP (Hypertext Transfer Protocol) リクエストのそのままの本体です。このストリングは次に、reader の XML() 関数に渡されます。

$request = $HTTP_RAW_POST_DATA;
$reader->XML($request);

任意のストリングを、どこででも、そのストリングのある場所で構文解析することができます。例えば、ストリングはプログラムのストリング・リテラルであることもあれば、ローカル・ファイルから読み込まれる場合もあります。また、open() 関数を使って外部 URL からデータをロードすることもできます。例えば下記のステートメントは、私の Atom フィードの 1 つを構文解析するための準備をします。

$reader->open('http://www.cafeaulait.org/today.atom');

生データをどこで取得するにせよ、これで reader は設定され、構文解析の準備が整いました。

文書を読む

read() 関数は、パーサーを次のトークンに進めます。そのために最も単純な方法は、文書全体を while ループで繰り返すことです。

while ($reader->read()) {
// processing code goes here...
}

これが終わったら、パーサーが使用しているリソースを解放するためにパーサーを閉じ、次の文書用にパーサーをリセットします。

$reader->close();

ループの中で、パーサーは特定のノード (要素の最初、要素の最後、テキスト・ノード、コメントなど) に置かれます。パーサーが現在何を処理しているかは、次のプロパティーを調べるとわかります。

  • localName は、接頭辞なしのローカル・ノード名です。
  • name は、ノード名で、接頭辞が付くこともあります。コメントなど、名前を持たないノードの場合は (DOM の場合と同じように)、#comment や #text、#document などです。
  • namespaceURI は、そのノードの名前空間の URI (Uniform Resource Identifier) です。
  • nodeType は、そのノード・タイプを表す整数です。例えば 2 は属性ノード、7 は処理命令です。
  • prefix は、そのノードの名前空間接頭辞です。
  • value は、そのノードのテキスト内容です。
  • hasValue は、ノードがテキスト値を持つ場合は真、それ以外は偽です。

もちろん、すべてのノード・タイプがこうしたプロパティー全部を持っているわけではありません。例えば、テキスト・ノードや CDATA セクション、コメント、処理命令、属性、空白、文書型、XML 宣言は値を持ちます。他のノード・タイプ (最も重要なノード・タイプは要素と文書) には値がありません。一般的にプログラムは、そのプログラムが処理している対象箇所を nodeType プロパティーを使って判断し、適切に応答します。リスト 3 は単純な while ループですが、プログラムが処理している対象箇所を、こうした関数を使って出力します。リスト 4 は、このプログラムにリスト 1 を入力した場合の出力を示しています。

リスト 3. パーサーが処理している対象箇所
while ($reader->read()) {
echo $reader->name;
if ($reader->hasValue) {
echo ": " . $reader->value;
}
echo "\n";
}
リスト 4. リスト 3 の出力
methodCall
#text: 

methodName
#text: sqrt
methodName
#text: 

params
#text: 

param
#text: 

value
double
#text: 10
double
value
#text: 

param
#text: 

params
#text: 

methodCall

ほとんどのプログラムは、あまり汎用的ではありません。ある特定形式の入力を受け付け、それを何らかの方法で処理します。この XML-RPC の例では、入力の中から 1 つだけ (double 要素) を読む必要があります。しかもそれは 1 個しかありません。これを読むには、double という名前を持つ要素の始まりを探します。

if ($reader->name == "double" 
&& $reader->nodeType == XMLReader::ELEMENT) {
// ...
}

この要素は、テキスト・ノードの子を 1 つ持っているようです。これを読むには、パーサーを次のノードに進めます。そこで次のようにします。

if ($reader->name == "double" && $reader->nodeType == XMLReader::ELEMENT) {
$reader->read();
respond($reader->value);
}

ここでは respond() 関数が XML-RPC レスポンスを作成し、それをクライアントに送信します。しかしそれを示す前に、対処する必要がある他のことがあります。リクエスト文書の double 要素が 1 つのテキスト・ノードしか含んでいないことは、絶対的に保証されているわけではありません。いくつかのテキスト・ノードを含んでいるかもしれず、またコメントや処理命令も含んでいるかもしれません。例えば、次のようになっているかもしれません。

<value><double>
<!--value follows-->6.<!--fractional part next-->0
</double></value>

確実なソリューションとするためには、double 要素のテキスト・ノードの子をすべて取得し、それらを連結し、そしてその結果を double に変換します。コメントやその他、テキスト以外のノードが現れても、それらを注意深く避ける必要があります。そうすると少し複雑になりますが、極端に複雑になるわけではありません。リスト 5 を見てください。

リスト 5. ある要素のすべてのテキスト・コンテンツを取り出して連結する
while ($reader->read()) {
if ($reader->nodeType == XMLReader::TEXT
|| $reader->nodeType == XMLReader::CDATA
|| $reader->nodeType == XMLReader::WHITESPACE
|| $reader->nodeType == XMLReader::SIGNIFICANT_WHITESPACE) {
$input .= $reader->value;
}
else if ($reader->nodeType == XMLReader::END_ELEMENT
&& $reader->name == "double") {
break;
}
}

とりあえず、文書の中にあるテキスト以外のものはすべて無視することにします。(後ほどエラー処理をさらに追加します。)

レスポンスを作成する

XMLReader は名前からもわかるように、純粋に読み取り専用です。これに対応する XMLWriter クラスは開発中ですが、まだ実際に使用できる状態にはなっていません。幸い XML を書くことは、読むことよりもずっと簡単です。まず、レスポンスのメディア・タイプを header() 関数を使って設定する必要があります。XML-RPC の場合は application/xml なので、例えば次のようになります。

header('Content-type: application/xml');

コンテンツは通常、そのままページにエコーされます。これをリスト 6 の respond() 関数に示します。

リスト 6. XML をエコーする
function respond($input) {

echo "<?xml version='1.0'?>
<methodResponse>
<params>
<param>
<value><double>" .
sqrt($input)
. "</double></value>
</param>
</params>
</methodResponse>";

}

HTML の場合と同じように、レスポンスのリテラル部分を直接 PHP ページに埋め込むこともできます。リスト 7 はこの方法を示しています。

リスト 7. リテラル XML
function respond($input) {

?><?xml version='1.0'?>
<methodResponse>
<params>
<param>
<value><double>"<?php 
echo      sqrt($input);
?>
</double></value>
</param>
</params>
</methodResponse>
<?php
}

エラー処理

ここまでは、入力文書が整形式であることを暗黙の前提としてきました。しかし、そうである保証はありません。XMLReader も他の XML パーサーと同様、整形式性のエラーが見つかったら即座に処理を停止する必要があります。そうすると、read() 関数が偽を返します。

理論的には、パーサーは、そのパーサーが見つけた最初のエラーまでのデータをレポートすることができます。しかし私が小さな文書で実験した限りでは、ほとんど瞬時にエラーを検出します。下にあるパーサーが、文書の大きい塊を事前構文解析してキャッシングし、それを少しずつ取り出します。そのため、早まってエラー検出してしまう傾向があります。最初の整形式エラーが出るまではコンテンツを構文解析できる、などと考えないのが無難です。さらに、パーサー・エラーが起きるまでコンテンツは見えない、などと想定すべきでもありません。完全な整形式の文書のみを受け付けたいのであれば、文書の最後を見終わるまでは、そのスクリプトが取り消せない動作をしないように確認する必要があります。

パーサーが整形式性エラーを検出すると、read() 関数は下記のようなエラー・メッセージをエコーします (ただし冗長エラー・レポートがオンの場合です。開発サーバーではオンにすべきです)。

<br />
<b>Warning</b>:  XMLReader::read() [<a href='function.read'>function.read</a>]:       
< value><double>10</double></value> in <b>/var/www/root.php</b> 
on line <b>35</b><br />

これをユーザーに見える HTML ページにはコピーしたくないものです。もっと良い方法としては、エラー・メッセージを $php_errormsg 環境変数の中にキャプチャーすることです。そのためには、php.ini ファイルの track_errors 構成オプションをオンにします。

track_errors = On

track_errors は、デフォルトでオフであり、php.ini の中で明示的に指定されます。そのため、必ずこの行を変更します。この行を php.ini の初めの方に追加した場合には (私も最初はそうしました)、後で track_errors = Off 行がそれを無効にします。

このプログラムは、完全な整形式の入力 (妥当な入力でもありますが、これについては後で触れます) に対してのみレスポンスを送信します。そのため、(while ループを抜け) 文書の構文解析が終わるまで待つ必要があります。終わった時点で、$php_errormsg がセットされているかどうかをチェックします。もしセットされていなければ、その文書は整形式であり、XML-RPC レスポンス・メッセージが送信されます。もしこの変数がセットされていれば、その文書は整形式ではなく、この場合は XML-RPC フォールト・レスポンスが送信されます。負の数の平方根が要求された場合にも、フォールト・レスポンスが送信されます。これをリスト 8 に示します。

リスト 8. 整形式性をチェックする
// set up the request
$request = $HTTP_RAW_POST_DATA;
error_reporting(E_ERROR | E_WARNING | E_PARSE);
if (isset($php_errormsg)) unset(($php_errormsg);
// create the reader
$reader = new XMLReader();
// $reader->setRelaxNGSchema("request.rng");
$reader->XML($request);

$input = "";
while ($reader->read()) {
if ($reader->name == "double" && $reader->nodeType == XMLReader::ELEMENT) {

while ($reader->read()) {
if ($reader->nodeType == XMLReader::TEXT
|| $reader->nodeType == XMLReader::CDATA
|| $reader->nodeType == XMLReader::WHITESPACE
|| $reader->nodeType == XMLReader::SIGNIFICANT_WHITESPACE) {
$input .= $reader->value;
}
else if ($reader->nodeType == XMLReader::END_ELEMENT
&& $reader->name == "double") {
break;
}
} 
break;
}
} 

// make sure the input was well-formed
if (isset($php_errormsg) ) fault(21, $php_errormsg);
else if ($input < 0) fault(20, "Cannot take square root of negative number");
else respond($input);

これは XML の一般的なストリーミング処理パターンの簡単な場合です。パーサーはデータ構造を埋め、文書が終了すると、そのデータ構造に対して操作が行われます。通常は、データ構造は文書そのものよりも単純です。ここではデータ構造が特に単純で、1 つのストリングしかありません。

妥当性検証

ここまでは、あると思ったところにデータがあるかどうかの検証を考慮しませんでした。この検証を行うためには、スキーマに対して文書をチェックするのが一番簡単です。XMLReader は RELAX NG スキーマ言語をサポートしています。リスト 9 は、この特別な形式の XML-RPC リクエストのための単純な RELAX NG スキーマを示しています。

リスト 9. XML-RPC リクエスト
<element name="methodCall" xmlns="http://relaxng.org/ns/structure/1.0" 
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<element name="methodName">
<value>sqrt</value>
</element>
<element name="params">
<element name="param">
<element name="value">
<element name="double">
<data type="double"/>
</element>
</element>
</element>
</element>
</element>

setRelaxNGSchemaSource() を使うことで、スキーマをストリング・リテラルとして直接 PHP スクリプトに埋め込むことができます。あるいは、setRelaxNGSchema() を使って外部ファイルあるいは URL から読み込むこともできます。例えば、リスト 9 がファイル sqrt.rng の中にあるとすると、スキーマをロードするためには次のようにします。

reader->setRelaxNGSchema("sqrt.rng")

これを、文書の構文解析を始める前に行います。パーサーは、文書を読みながらスキーマに対して文書をチェックします。文書が妥当であるかどうかをチェックするには、isValid() をコールします。isValid() は、文書が (その時点まで) 妥当であれば真を返し、そうでなければ偽を返します。リスト 10 は完成したプログラムであり、すべてのエラー処理を含んでいます。このプログラムは、適切な入力であれば受け付けて正しい値を返し、また不正なリクエストであれば拒否します。ここには、不具合が起きたときに XML-RPC フォールト・レスポンスを送信する fault() メソッドも追加しました。

リスト 10. 完全な XML-RPC 平方根サーバー
<?php
header('Content-type: application/xml');

// try grammar
$schema = "<element name='methodCall' 
xmlns='http://relaxng.org/ns/structure/1.0' 
datatypeLibrary='http://www.w3.org/2001/XMLSchema-datatypes'>
<element name='methodName'>
<value>sqrt</value>
</element>
<element name='params'>
<element name='param'>
<element name='value'>
<element name='double'>
<data type='double'/>
</element>
</element>
</element>
</element>
</element>";


if (!isset($HTTP_RAW_POST_DATA)) {
fault(22, "Please make sure always_populate_raw_post_data = On in php.ini");
}
else {

// set up the request
$request = $HTTP_RAW_POST_DATA;
error_reporting(E_ERROR | E_WARNING | E_PARSE);
// create the reader
$reader = new XMLReader();
$reader->setRelaxNGSchema("request.rng");
$reader->XML($request);

$input = "";
while ($reader->read()) {
if ($reader->name == "double" && $reader->nodeType == XMLReader::ELEMENT) {

while ($reader->read()) {
if ($reader->nodeType == XMLReader::TEXT
|| $reader->nodeType == XMLReader::CDATA
|| $reader->nodeType == XMLReader::WHITESPACE
|| $reader->nodeType == XMLReader::SIGNIFICANT_WHITESPACE) {
$input .= $reader->value;
}
else if ($reader->nodeType == XMLReader::END_ELEMENT
&& $reader->name == "double") {
break;
}
} 
break;
}
} 

if (isset($php_errormsg) ) fault(21, $php_errormsg);
else if (! $reader->isValid()) fault(19, "Invalid request");
else if ($input < 0) fault(20, "Cannot take square root of negative number");
else respond($input);

$reader->close();
}


function respond($input)
{
?>
<methodResponse>
<params>
<param>
<value><double><?php 
echo      sqrt($input);
?></double></value>
</param>
</params>
</methodResponse>
<?php
}


function fault($code, $message)
{

echo "<?xml version='1.0'?>
<methodResponse>
<fault>
<value>
<struct>
<member>
<name>faultCode</name>
<value><int>" . $code . "</int></value>
</member>
<member>
<name>faultString</name>
<value>
<string>" . $message . "</string>
</value>
</member>
</struct>
</value>
</fault>
</methodResponse>";

}

属性

通常のプル型構文解析を行っている間は、属性を読むことはありません。属性を読むためには、要素の最初で停止し、名前あるいは数字によって特定の属性をリクエストします。

必要な属性の名前を getAttribute() に渡すと、現在の要素での、その属性の値を得ることができます。例えば次のステートメントは、現在の要素の id 属性を要求します。

$id = $reader->getAttribute("id");

もし属性が名前空間の場合には (例えば xlink:href など)、getAttributeNS() をコールし、ローカル名と名前空間 URI をそれぞれ第 1 と第 2 の引数として渡します。(接頭辞は関係ありません。) 例えば次のステートメントは、名前空間 http://www.w3.org/1999/xlink/ の xlink:href 属性の値を要求します。

$href = $reader->getAttributeNS("href", "http://www.w3.org/1999/xlink/");

どちらのメソッドも、その属性が存在しない場合には空のストリングを返します。(これは正しくありません。ヌルを返すべきなのです。現在の設計では、値が空のストリングである属性と、そもそも属性が存在しない場合とを区別することが困難です。)

単純に要素の全属性を知りたい場合、そしてそれらの名前が事前にわからない場合には、reader がその要素の位置にある時に、moveToNextAttribute() をコールします。いったんパーサーが属性ノードに来れば、属性の名前、名前空間、そして要素に使われているものと同じプロパティーを持つ値を読むことができます。例えば次のコード・フラグメントは、現在の要素の全属性を出力します。

  if ($reader->hasAttributes and $reader->nodeType == XMLReader::ELEMENT) {
    while ($reader->moveToNextAttribute()) {
      echo $reader->name . "='" . $reader->value . "'\n";
    }
    echo "\n";
  }

XMLReader は XML API としては非常に変わっており、要素の最初からでも最後からでも属性を読むことができます。2 重カウントを避けるために、ノード・タイプが XMLReader::ELEMENT であり、XMLReader::END_ELEMENT (属性を持っているかもしれません) ではないことをチェックすることが重要です。

まとめ

XMLReader は、PHP プログラマーのツールキットに追加された便利なクラスです。SImpleXML とは異なり、XMLReader は、文書の一部ではなく全文書を扱う、完全な XML パーサーです。また DOM とも異なり、使用可能なメモリーよりも大きいサイズのドキュメントを扱うことができます。さらに、SAX とも異なるのは、プログラムに主導権があるという点です。PHP で XML 入力を処理する必要がある場合には、XMLReader の使用を検討する価値は十分にあります。


ダウンロード可能なリソース


関連トピック

  • PHP 5 のマニュアルから、XMLReader についての正式な文書を読んでください。
  • libxml の C XMLReader API の上に置かれた薄いレイヤー、PHP XMLReader クラスについて読んでください。
  • この API を生む元となった .NET の AjaxSystem.Xml.XmlTextReader を調べてみてください。
  • 『XML in a Nutshell』(Elliotte Rusty Harold と W. Scott Means の共著、O'Reilly 刊、2005年) は、XML を深く掘り下げ、包括的にまとめたコンパクトな本です。完璧な入門書、参照資料として好適です。
  • 「PHP での SimpleXML 処理」(Elliotte Rusty Harold 著、developerWorks、2006年10月) を読み、SimpleXML と SimpleXML エクステンションを使ってマークアップ専用の RSS リーダーを PHP で作成してください。
  • 「XMLの論考: RELAX NGによる逆襲 第1回」(David Mertz 著、developerWorks、2003年2月) を読み、妥当な XML インスタンスを記述する強力で簡潔、単純なセマンティックのクラスを、RELAX NG を使って作成してください。
  • 『デザインパターン―オブジェクト指向における再利用のための (単行本)』(Addison-Wesley 刊、1995 年) はデザイン・パターンの出発点となった本として、GoF (4 人組) がオブザーバー・デザイン・パターンとイテレーター・デザイン・パターンを解説しています。
  • XML および関連技術において IBM 認証開発者になる方法については、IBM XML certification を参照してください。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source
ArticleID=249253
ArticleTitle=PHP で XML をプル型構文解析する
publish-date=01112008