肉とビールとパンケーキ by @sotarok

少し大人になった「肉とご飯と甘いもの」

SimpleXMLとXMLReaderのまとめ(PHP勉強会で話してきたコード)

で。

をUPしてこれでいいや、と満足していたら、即座にid:maru_ccさんからツッコミが入ってしまったので、
仕方ないので(ぉ、コードもUPします。

チューニング、というか、大規模なXMLファイルを扱うところ、もうちょっと色々検証してデータ出したいのですが、なんていうか気力がないので(ぉ、とりあえず先日はなした内容だけでも、と思ってUP。

SimpleXMLのインスタンス生成

SimpleXMLのインスタンスの生成方法は2種類×2種類あります。

文字列から、インスタンスを生成

文字列から生成する場合、一番楽なのは、以下のように、simplexml_load_string関数を使います。例えば、以下のようにします。

<?php

// $xml_string に、XML文字列が入っているものとする
$xml_string = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<hoge>
test
</hoge>
EOF;

$xml = simplexml_load_string($xml_string);

また、直接 SimpleXMLElementを new することもできます。

<?php

// $xml_string に、XML文字列が入っているものとする
$xml_string = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<hoge>
test
</hoge>
EOF;

$xml = new SimpleXMLELement($xml_string);
ファイルまたはURLからインスタンスを生成

もうひとつの方法が、ファイルやURLからインスタンスを生成する方法です。
これも、1つは関数を呼ぶ方法、もうひとつはSimpleXMLElementを直接newする方法があります。

<?php
$xml = simplexml_load_file("http://d.hatena.ne.jp/sotarok/rss2");

or

$xml = new SimpleXMLElement("http://d.hatena.ne.jp/sotarok/rss2", null, true);

SimpleXMLElementの第二引数はLIBXMLのオプションです(後述)。URLを指定する場合は第三引数に true をフラグります。

要素へのアクセス・デモ:RSSからタイトル一覧を取得

SimpleXMLを使うと要素へのアクセスを簡単に行うことができます。
で、もうまとめて、デモります。RSSからタイトル一覧を取得するデモです。

<?php
$xml = simplexml_load_file("http://d.hatena.ne.jp/sotarok/rss2");
foreach ($xml->channel->item as $val) {
    echo $val->title . "\n";
}

こうするだけで、タイトル一覧が取得できます!まぁ便利!

ちなみに、こうするとたった3行です。

<?php
foreach (simplexml_load_file("http://d.hatena.ne.jp/sotarok/rss2")->channel->item as $val) {
    echo $val->title . "\n";
}

デモ:xpath を使ってRSSからタイトル一覧を取得

いきなりxpathの話ですが、詳しくは資料参考のこと。
まぁxpathを使うとこれまた簡単にxml内の要素を取得することができます。

<?php
$xml = simplexml_load_file("http://d.hatena.ne.jp/sotarok/rss2");
foreach ($xml->xpath("//item/title") as $val) {
    echo $val . "\n";
}

DOMDocumentと連携して簡単スクレイピング

SimpleXMLは、DOMDocumentと相互に変換可能です。simplexml_import_dom 関数や、dmo_import_simplexml 関数です。
で、DOMDocumentを使ってHTMLをDOMに整形してやり、SimpleXMLに変換することで、xpathを使うことができるようになるので、スクレイピングみたいなものも簡単に行える、というわけです。

<?php
$xml = @simplexml_import_dom(DOMDocument::loadHTMLFile("http://twitter.com/sotarok"));

foreach ($xml->xpath("//span[@class='entry-content']") as $val) {
    echo trim(strip_tags($val->asXML())) ."\n";
}

このスクリプトは、twitterの私の発言の一覧を抜き出してきています。
DOMDocument::loadHTMLFileで、当該ファイル(やURL)をDOMに変換します。loadHTMLやloadHTMLFileはstaticにコールすることができますので、こうして1行でsimplexmlに変換できるわけです。
先頭に @ をつけているのは、妥当ではないHTMLを読み込ませたときにDOMDocumentがwarningを吐くためです。まぁ抑制してやっても大丈夫でしょう。

xpathで、Twitterの発言部分である、classが entry-contentであるspanを配列で取得しています。
で、次の行がとても変態的な内容ですが、実は発言の中に a タグなどがあると、$valは、さらにaタグのSimpleXMLElementを下の階層に持ちます。そうすると発言からURLなどが抜けてしまうので、一旦 asXML() で XML文字列に落とし込みます。そうすると、 aタグなどもそのまま文字列として帰ってきます。
そこに、strip_tagsでタグを除去し、trimして左右の空白を取り除くことで、純粋な発言内容だけを取り出してきている、というわけです。
まぁなんというかもっとうまい方法があれば教えてもらいたいものですが、とりあえずはこれでいいかな、という感じです。

SimpleXMLIterator

SimpleXMLIteratorはSPLで実装された、IteratorインターフェースのSimpleXML実装です。

SimpleXMLIteratorは次のように使います。

まず、simplexml_load_string や simplexml_load_file の第二引数の使い方ですが、これは、SimpleXMLElementを継承しているクラス名を指定することができます。つまり自分でSimpleXMLElementを継承させたクラスを定義してうにゃうにゃしたとしても、それを第二引数で指定してやることで、そのクラスを使うことができるようになります。
で、SimpleXMLIteratorも、そうやって指定することでインスタンスを生成します。
で、

<?php
$xml = simplexml_load_file("http://d.hatena.ne.jp/sotarok/rss2", "SimpleXMLIterator");

for ($xml->rewind(); $xml->valid(); $xml->next()) {
...
}

などと使うことができます。まぁもちろんIteratorで実装されてるので、foreachしても大丈夫なんですが・・・。

発表の中では、例えばforeachなどの繰り返しの中で、

<?php
$xml->hoge[$i];

などとアクセスしてしまうと、2万ノードを超えたあたりで(あ、もちろんこの数字は環境依存。以前私がハマったときの数字)、メモリを食いつぶし、速度もヒドイことになってきてしまう、という話をしました。
というのは、おそらく、SimpleXMLは、要素を toStringした時点、つまり比較などに使ったり、echoしたりなどなど、その時点で、内容がメモリ上に乗っかるっぽい、という実装であるからです。(全然中身ちゃんと調べてません。多分、というかんじ)

で、たとえばhogeノードの $i 番目の要素がXXXであったら・・・などと比較をする場合も、 $i でイテレートして順に比べていくと、比べた時点でメモリ上に実態が乗るので、大変なことになってしまうので、だったらイテレータなどを使い、current()で呼び出すことによって、SimpleXMLのオブジェクト本体のメモリは増加させないような工夫ができるよ、というお話でした。

えーさっきからメモリメモリ言ってますが、感覚値でしかないので、的のはずれたことを言っていたらすみません。

XMLReader

もうひとつPHP5で使える、XMLを読むためのクラスが、XMLReaderという謎のクラスです。
まぁなにが謎って、正直いってSimpleXMLのほうが楽だから・・・・まぁ、それは多分用途が違うのです。

で、なにが違うかという話は発表中にしたのですが、SimpleXMLは、simlexml_load_string/file した時点で、XMLを上から下までパースし、その構造をオブジェクト上に保持します。(ノードのvalueは保持しません、上述のとおり、これはechoなどtoStringした時点で読み込まれるものだと思われます)。つまり、大きいファイルを読めば読むほど、メモリを激しく食い、ロード時点でかなりの時間がかかる、というわけです。(ただし、構造を知っているおかげでxpathなどを使用することができるのだと思います)

対するXMLReaderは、XMLに対して、カーソル位置しか記憶しません。つまり、next()やread()といったメソッドを使うことによって、次々をノードカーソルを動かし、それによって内容を走査することができます。
非常に省メモリで高速です。

で、ノードの名前とかはスライドにあるとおりで、カーソルの進み方も、開始ノード→テキストノード→終了ノード、といったように、SimpleXMLのときは、「titleノードのvalueに「ほげほげ〜」というタイトル文字列」という直感的な流れだったのに対し、XMLReaderはDOMの仕様どおり、titleの開始ノード、直後にテキストノードでそのvalueが「ほげほげ〜」であり・・・・などと進むのです。
ちょっと扱いづらいですね。
で、ノードの種類などは、XMLReaderのマニュアルに詳しくのっているし、XMLReaderの使い方自体は、IBMのサイトに結構詳しく載っていたので、参考にされると良いかな、と思います。


で、発表中に問題となった、SignificantWhitespaceNodeについてです。例えば、次のようなXMLを見るとします。

<?php

$xml = new XMLReader();
$xml->open("http://d.hatena.ne.jp/sotarok/rss2");

$i = 0;
while($i++ <= 8) {
    $xml->read();
    echo $xml->nodeType . " : " .$xml->name . " : " . $xml->value . "\n";
}

これは、頭から順に9個だけ、ノードタイプ、ノード名、値を出力するだけのプログラムですが、例えば、RSSの例ですと、

1 : rss : 
14 : #text : 
        
1 : channel : 
14 : #text : 
                
1 : title : 
3 : #text : GRANADA Hatena @ sotarok
15 : title : 
14 : #text : 
                
1 : link : 

などと出力されます。

で、このノードタイプ 14 がSignificantWhitespaceNodeといって、改行や空白など、ノード間に存在するゴミのようなものです。と、id:maru_ccさんに第34回PHP勉強会で話してきました - maru.cc@はてなで教えてもらったのですが、まぁよく調べたら、これは、オプションで取り除くことができます。

<?php

$xml = new XMLReader();
$xml->open("http://d.hatena.ne.jp/sotarok/rss2", null, LIBXML_NOBLANKS);

$i = 0;
while($i++ <= 8) {
    $xml->read();
    echo $xml->nodeType . " : " .$xml->name . " : " . $xml->value . "\n";
}

このように、例によってopenの第三引数に、LIBXMLのオプションをつけることができます。
この実行結果は、

1 : rss : 
1 : channel : 
1 : title : 
3 : #text : GRANADA Hatena @ sotarok
15 : title : 
1 : link : 
3 : #text : http://d.hatena.ne.jp/sotarok/
15 : link : 
1 : description : 

となり、見事に中間のホワイトスペースノードを取り除くことができました。

実は、SimpleXMLやXMLReaderなど、LIBXMLが根底にあるモジュールでは、たいていLIBXMLのオプションをつける場所が用意されています。
LIBXML_COMPACTなど、若干ですが速度の向上などが見られるし、まぁ知っておいて損はないかもしれないので、一応マニュアルなどチェックされると良いかもしれません。
LIBXMLのオプションは、LIBXMLのマニュアルで見ることができます。

まとめ

SimpleXMLは、PHPXMLを読む場合には、非常に強力なツールです。
こんなに簡単にXMLを扱えるものを作るなんて、さすがPHPとしかいいようがありません。

ただし、大容量データを扱う場合や、要素を書き込んだりなんだり・・・と言った場合には、注意する必要があります。
まぁ適材適所だと思いますが。


まぁあんままとまりないですが、眠くなってきてハンパないのでこのへんで。

おまけ

書いたけど発表で使わなかった、DOMだけでHTMLを生成するコード。bodyまで行って力尽きた。

<?php

$dom = new DOMDocument('1.0', 'utf-8');
$dom->formatOutput = true;
$dom->appendChild(DOMImplementation::createDocumentType('html', 'PUBLIC', '-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'));

$html = $dom->createElement('html');
$html->setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');

$html->setAttribute('xml:lang', 'ja');
$html = $dom->appendChild($html);


$head = $dom->createElement('head');
$head = $html->appendChild($head);
    $meta = $dom->createElement('meta');
        $meta->setAttribute('http-equiv', 'Content-Type');
        $meta->setAttribute('content', 'text/html; charset=utf-8');
        $head->appendChild($meta);
    $meta = $dom->createElement('meta');
        $meta->setAttribute('http-equiv', 'Content-Style-Type');
        $meta->setAttribute('content', 'text/css');
        $head->appendChild($meta);
    $meta = $dom->createElement('meta');
        $meta->setAttribute('http-equiv', 'Content-Script-Type');
        $meta->setAttribute('content', 'text/javascript');
        $head->appendChild($meta);
    $meta = $dom->createElement('meta');
        $meta->setAttribute('name', 'robots');
        $meta->setAttribute('content', 'INDEX,FOLLOW');
        $head->appendChild($meta);
    $head->appendChild($dom->createElement('title', 'DOM で HTML かいたりしませんか'));
    

$body = $dom->createElement('body');
$body = $html->appendChild($body);

$dom->appendChild($html);

$dom->save("dom.html");