PHPでHTMLを簡単に解析できるDOMDocument
単純作業って嫌ですよね。
たまには良いんですよ、たまには。頭の中をからっぽにして音楽聴きながらExcelにデータを打ち込むとか、まぁまず二度は入力しないだろうって作業なら我慢して出来るんですけど、毎日どこそこのページをチェックして、データをコピーして貼り付け、とか考えただけで鬱になりそうです。
その昔、ネットゲームの攻略情報サイトを制作/運営していまして、公式サイトからのサーバーメンテナンスのお知らせとか、自分のサイトにもリンク貼って告知していたわけです。
当時、これを何とか自動化できないかなと思って、PHPのfile_get_contents関数を使ってHTMLを読み込んで、お知らせのあるULタグを見つけて、日付っぽい文字列とリンクっぽい文字列を見つけて、アレやコレやいじくってと、だいたい100行くらいのPHPスクリプトで実現できました。
一緒にそのWebサイトを作っていた人たちもいるのですが、ウェブスクレイピングなんて言葉も浸透していない時代の話ですから「君はホントに単純作業が嫌いだねーw」「コピペするだけで済むのに何時間もかけてプログラムを組むなんて」と苦笑されたものです。
泥臭いコーディングですし、公式サイトのページが変わったらエラーになるだろうし、そうなったら修正する手間が増えて余計コピペのほうが良かったと後悔するかもなぁーとはぼく自身も思っていました。
ところがどっこい。
12年間動き続けた自動更新システム
ええ、書き間違いじゃないですよ。12年です12年。PHPスクリプトの最終更新日を見たら2006年でしたもん。
自動更新に失敗したらぼくのメールアドレスに通知が届くように組んであるはずなのですが、この12年間1度もエラーメールが来ないんです。逆に不安になって、月に1回くらいは手動でも見ていたのですが、まったく問題なし。
情報取得元の公式サイトも微妙にデザインは変わっているのですが、多少タグが変わっても動くような作りにはしておいたので、奇跡的にその範囲内で済んでいたようです。
とはいえ、この12年の間に公式サイトのお知らせページが複数に増えており、新着情報とメンテナンス情報的なものが別れてしまっていたため、こちらで告知している情報が少なめになっておりました。
せっかくだからフルスクラッチで書き直すことに
いくら最初のプログラムに時間をかけたといっても(あまり覚えていませんが)1日もかけてはいないはず。それが12年も動いたのだから、もう十分に元は取ったでしょう。
というわけで、最近のPHPではHTML解析にどんな手段を用いるのがトレンドなのかなぁーと調べてみました。
特に新しい関数が増えていなければ、正規表現とmb_strpos関数で泥臭くタグ解析していたコードはそのままにするつもりだったのですが、すげーのがありましたよ。
PHP5で追加されたDOMDocumentクラスです。
PHP7じゃなくて良かったー。
最近作ったサイトは順次PHP7に移行しているのですが、さすがに12年前に作ったサイトとなると、移行作業に苦労しそうなので、もうしばらくPHP5系で動かしたかったんですよね。
(でも、ホントはPHP5も2018年末でサポート終了なので、そろそろ重い腰を上げないと、とは思っている)
DOMDocumentの使い方
例えばこんな感じ。
<?php
$target_html = file_get_contents('https://blog.ver001.com/');
$target_html = mb_convert_encoding($target_html, 'HTML-ENTITIES', 'auto');
$dom = new DOMDocument;
@$dom->loadHTML($target_html);
$ul_node = $dom->getElementsByTagName('ul')->item(1);
$li_nodes = $ul_node->getElementsByTagName('li');
foreach ($li_nodes as $node) {
echo $node->nodeValue.'<br>';
}
?>
まるでJavaScriptだ!
そう、まさにこういうことがやりたかったんですよ。
JavaScriptのDOMなら、getElementsByTagNameやgetElementByIdでHTMLの解析は簡単ですもんね。
同じことがPHPで出来るようになっているとはなぁ。
前にPEARで似たようなことをやるクラスは見かけたことあったのですが、わざわざPEAR入れるのも面倒だなぁーとスルーしてたんですよね。
でもDOMDocumentはPHP標準で使えるので、こりゃ使わなきゃ、とさっそくDOMDocumentを使って組むことにしました。
コーディング例の補足説明
わかりづらい部分かもなので2箇所だけ補足説明しておきます。
$target_html = mb_convert_encoding($target_html, 'HTML-ENTITIES', 'auto');
loadHTMLがHTML5記法のcharsetに対応していないため、事前にHTMLエンティティに変換しておいたほうが良いようです。
@$dom->loadHTML($target_html);
HTMLの文法的にわずかでも間違いがあるとloadHTMLがWarningを出してしまうため@マークを付けてエラーを無視しています。
getElementsByClassNameがない
意気揚々とPHPスクリプトを組み始めたのは良いのですが、先述の例のように「2番目に現れたULタグ」とかいう指定はスマートじゃないじゃないですか。
その2番目にあるULタグに<ul class="menu">とかのわかりやすいクラス名が付いていたら、なおさらクラス名を指定して、データを取得したいですよね。
でもPHPのDOMDocumentにはgetElementsByClassNameがないのです。
軽く調べたところ、有志が自前でコーディングしたgetElementsByClassName関数とか公開されていて、お!これ使うかな、とも思ったのですが………
function getElementsByClassName($dom, $ClassName, $tagName=null) {
if($tagName){
$Elements = $dom->getElementsByTagName($tagName);
}else {
$Elements = $dom->getElementsByTagName("*");
}
$Matched = array();
for($i=0;$i<$Elements->length;$i++) {
if($Elements->item($i)->attributes->getNamedItem('class')){
if($Elements->item($i)->attributes->getNamedItem('class')->nodeValue == $ClassName) {
$Matched[]=$Elements->item($i);
}
}
}
return $Matched;
}
あー……なるほど……なるほどね!
ループしてひとつひとつattributesのクラス名を比較してるのね。そりゃそうか。
悪くない。悪くないんだけど、もうちょいスマートな方法ないのかなぁ。まぁなければないで、この関数を別ファイルに移動してライブラリ化して見えなくすれば良いけれども…。
DOMXPathがあった
こちらもPHP5以降で使える標準クラスで、DOMXPathというのがありました。
これはヤバイですね。
XPathの簡単な記法でHTMLタグの要素にアクセスできるってわけです。
具体的にはこんな感じ。
<?php
$target_html = file_get_contents('https://blog.ver001.com/');
$target_html = mb_convert_encoding($target_html, 'HTML-ENTITIES', 'auto');
$dom = new DOMDocument;
@$dom->loadHTML($target_html);
$xpath = new DOMXPath($dom);
$ul_nodes = $xpath->query('//ul[@class="menu"]');
$li_nodes = $ul_nodes->item(0)->childNodes;
foreach ($li_nodes as $node) {
echo $node->nodeValue.'<br>';
}
?>
コーディング量はほとんど変わっていませんが、UL要素の2番目と決め打ちしてノードを取得していた最初の例からすると汎用性がグンと上がっています。
まとめ
- PHPでHTML解析するならDOMDocument
- loadHTMLはちょっとの構文ミスでもWarning出すので@マーク忘れずに。
(@が気持ち悪ければ libxml_use_internal_errors(true);でも可) - 更にクラス名などの属性値を取りたいならDOMXPath
正規表現などの文字列解析に比べるとかなーり速度が落ちるようですが、そんなに速度が重要な場面ってあるのかな? 検索エンジンみたいなのを作る予定で、大量に処理するのなら別の方法も考えたほうが良いのかも知れません。
ぼくのようなちょっとお知らせを取得したい、しかもCronジョブで1日に1回だけ、とかそんな感じなら特に解析速度が気になることはありませんでした。
何より、今まで100行以上使って書いていたコーディングが30行以下にまでダイエットできて、たいへん見やすくなりました。
また更に12年続くかどうかは……さすがに微妙ですが、きっと今後作るWebサイトでも大活躍してくれることでしょう。すごいぜ、DOMDocument!