phpのDOMDocumentで断片的なhtmlを扱うならxmlとして読み込むのがよさそう(PHP Advent Calendar jp 2010 Day 23++)

PHP Advent Calendar jp 2010 23++日目

PHP Advent Calendar jp 2010 : ATND

CakePHP Advent Calendar 2010 に続いて PHP Advent Calendar jp 2010 の順番が回ってきてたんですが・・・すいません、昨日は日付がかわってから帰宅する事になってしまいました。という事で、23++日目*1として、急ぎエントリーを書いてます。

phpで断片的なhtmlをDOMで扱いたい

phpで何かを作っている時、断片的なhtmlをDOMで操作したい場合が割とありました。http://jp.php.net/manual/ja/class.domdocument.phpで読み込んだりするんですが、日本語が文字化けしたり数値文字参照になったり、余計なタグが付いたりしてはまったりしました。DOMDocumentは、xmlとしてロードするのとhtmlとしてロードするので、動きがちがいます。そこで、とりあえず一通り試してみました。

読み込みたいhtml断片のサンプル
<dt>foo</dt>
<dd>バー</dd>

例として、こんなhtml断片を扱ってみました。

一応の結論

先に、試してみた結果一番よさそうだなと思ったタイプを書いておきます。html断片をXML宣言のあるXMLにして、loadXML()してsaveXML()します。文字化け、数値文字参照も無く、余計な要素は付いちゃうものの、比較的取り出しやすかったです。

<?xml version="1.0" encoding="UTF-8"?>
<root>
<dt>foo</dt>
<dd>バー</dd>
</root>

html断片にXML宣言とroot要素をつけます。

<?php
$dom = new DOMDocument();
$dom->loadXML($string);
var_dump($dom->saveXML());
string(83) "<?xml version="1.0" encoding="UTF-8"?>
<root>
<dt>foo</dt>
<dd>バー</dd>
</root>
"

loadXML()で読み込んでsaveXML()しました。

<?php
$result='';
foreach($dom->documentElement->childNodes as $node)
    $result .= $dom->saveXML($node);
var_dump($result);

root要素の中だけ取り出せば、html断片に戻ります。このサンプルでは、ただ読み込んで書き出してますが、あれこれDOMで操作したhtml断片が取れるはずです。

他の読み込み・書き出しタイプ

HTML断片をloadHTML()してsaveHTML()
<dt>foo</dt>
<dd>バー</dd>
string(200) "<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<dt>foo</dt>
<dd>&atilde;&#131;&#144;&atilde;&#131;&frac14;</dd>
</body></html>
"

html断片を何も考えずそのまま読み込ませます。数値文字参照になってるので分かりにくいですが、ブラウザを通してみると文字化けしてます。日本語データを壊してしまっているので使えません。

metaタグ付きHTML断片をloadHTML()してsaveHTML()
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<dt>foo</dt>
<dd>バー</dd>
string(247) "<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head><meta http-equiv="content-type" content="text/html; charset=UTF-8"></head>
<body>
<dt>foo</dt>
<dd>バー</dd>
</body>
</html>
"

DOMDocument->loadHTML()は基本的にちゃんとしたhtmlを想定してるっぽいので、metaタグで文字コードが指定されてるとちゃんと動きます。この通り、文字化けも数値文字参照にもならず良い感じですが、saveHTML()するとhtml断片だったはずがhtmlタグとかbodyタグとか付けられちゃいます。

XMLをloadXML()してsaveHTML()
<root>
<dt>foo</dt>
<dd>バー</dd>
</root>
string(54) "<root>
<dt>foo</dt>
<dd>&#12496;&#12540;</dd>
</root>
"

loadHTML()を諦めてloadXML()を使ってみました。xmlとして扱うためにroot要素だけ追加してます。文字化けなしで余計な要素も付かないですが、数値文字参照になりました。saveHTML()で書き出したのにhtmlタグとかbodyタグとか付かなかったのは良いんですけど。

XMLをloadXML()してsaveXML()
string(76) "<?xml version="1.0"?>
<root>
<dt>foo</dt>
<dd>&#x30D0;&#x30FC;</dd>
</root>
"

逆にsaveXML()してみたらxml宣言が付いちゃいました。数値文字参照xmlだからか16進数にかわってます。

XML宣言のあるXMLをloadXML()してsaveHTML()
<?xml version="1.0" encoding="UTF-8"?>
<root>
<dt>foo</dt>
<dd>バー</dd>
</root>
string(54) "<root>
<dt>foo</dt>
<dd>&#12496;&#12540;</dd>
</root>
"

じゃぁxml宣言を付けてsaveHTML()してみたらというと、やっぱり数値文字参照になってしまいます。saveHTML()なのでxml宣言は消えたみたいですけど。

XML宣言のあるXMLをloadXML() saveXML()
string(83) "<?xml version="1.0" encoding="UTF-8"?>
<root>
<dt>foo</dt>
<dd>バー</dd>
</root>
"

逆にxml宣言付けてsaveXML()したのが、一応の結論に書いたタイプです。文字化けや数値文字参照にならず。xml宣言は残っちゃうけど、前述の通りroot要素の子だけ取り出せばhtml断片になります。

html断片が未整形の場合

<dt>foo</dt>
<dd>バー

おまけですが、htmlだと未整形なのが割りとあったりします。例えばddタグの閉じ忘れとか。こういうhtml断片を、root要素つけてxml宣言つけてDOMDocument->loadXML()しても、xmlは厳格に扱われるのでエラーになってしまいます。

Warning: DOMDocument::loadXML() [domdocument.loadxml]: Opening and ending tag mismatch: dd line 4 and root in Entity,
Warning: DOMDocument::loadXML() [domdocument.loadxml]: Premature end of data in tag root line 2 in Entity,

DOMDocument->loadHTML()だったらタグの閉じ忘れの様なのだったら、ある程度読み込んでくれるみたいです。前述の通りhtmlタグやbodyタグが付いちゃうので、その辺を上手い事取り除かないとですが、未整形だったら「metaタグ付きHTML断片をloadHTML()してsaveHTML()」のタイプが良いかもしれません。

環境

Mac Mac OS X 10.5.8(Leopard
MAMP 1.7.2
php 5.2.6

この記事と、上記サンプルはこんな環境で確認してます。

次のPHP Advent Calendar jp 2010

Exploring php.net - m-takagiの日記
1日遅れになってしまったので、次の記事がもう上がってます。お待たせしちゃってすいません。

*1:echo 23++;はSyntax errorですが