FirefoxのHTMLパーサを使って壊れたHTMLを修復する PHP extension

ほかのことをやっていて遅くなりましたが FirefoxのhtmlparserをXPCOM経由で呼び出して壊れたHTMLを修復する で Java Mozilla Html Parser をC言語から呼び出せるようになったので、そのあとC言語から呼び出すためのラッパーを作って、さらにそれを使って PHP extension を作り、そのextensionを使ってPHPのDOMオブジェクトを構築するPHPのクラスを作りました。
その過程で Fedora Core 6 であればFirefoxのソースからなにかをビルドしたりすることなく簡単に試してみれるようにすることができたので、その手順をご紹介します。

Cのラッパー libfxpars

まずはFirefoxのXPCOMを呼び出すCのライブラリをビルドしましょう。 libfspars-0.0.1.tgz をダウンロードしてください。
tar xvfz libfspars-0.0.1.tgz
cd libfspars-0.0.1
make
make test
sudo make install
で自動的に /usr/bin/firefox からFirefoxのインクルードファイルがあるディレクトリ(通常は/usr/include/firefox-version)とライブラリのあるディレクトリ(/usr/include/firefox-version)を割り出してビルドします。インストールすると /usr/local/lib に libfspars.so と libfspars.so.1 が作られます。得体の知れないファイルをインストールしたくない、というときはインストールしなくても大丈夫です。最後の make install を飛ばして次に進んでください。 makeでこける場合、Firefoxの開発用パッケージが入っていないのかもしれません。
sudo yum install firefox-devel
でFirefoxの開発用パッケージをインストールしてみてください。 make test を実行すると、Makefileの中で "<table><td>hello, world</tr>" という壊れたHTMLがFirefoxのパーサによって解釈されて
OpenNode html
OpenNode body
OpenNode table
OpenNode tbody
OpenNode tr
OpenNode td
AddText hello, world
CloseLeaf
CloseNode td
CloseNode tr
CloseNode tbody
CloseNode table
CloseNode body
CloseNode html
と出力されます。Firefoxによってbody, html, tbody, tr などが補完されているのがわかります。

PHP Extension fxhtmlparser のビルド

Cのライブラリができたら次は PHP extension をビルドします。fxhtmlparser-0.0.1.tgz をダウンロードしてください。Cのライブラリを /usr/local/lib にインストールしなかった場合は、configureに --with-fxpars-dir= でlibfsparsを展開したディレクトリの名前を指定する必要があります。/home/kuma/libfspars-0.0.1にソースを展開してビルドしたのでしたら --with-fxpars-dir=/home/kuma/libfspars-0.0.1 と指定してください。
tar xvfz fxhtparser-0.0.1.tgz
cd fxhtparser-0.0.1
phpize
./configure --enable-fxhtmlparser [ --with-fxpars-dir= ]
make
でエクステンションがビルドされます。ビルドが完了するとmodulesディレクトリにfxhtmlparser.so という名前の PHP extensionができあがっています。動くかどうか実行してみましょう。
% php run.php
xpcom_dir: /usr/lib/firefox-1.5.0.7
<html><body>
<p>broken unordered list
</p>
<ul> <li><p><a href="http://example.com/">first list item</a> </p></li>
<li>next list item<p></p>
</li>
<p>next paragraph</p>
</ul>
</body></html>
という出力が出てきたら、無事にFirefoxのHTMLパーサを使って壊れたHTMLを修復できています!

PHPのクラス FxHtmlParser

できあがったモジュール fxhtmlparser.so をphp.iniのextensionにapacheの起動時にロードされるようにするか、dlを使って実行時にロードして、実際にPHPから使ってみましょう。 Firefoxはソースツリーにexpatというディレクトリもあり、内部でexpatを使っているようです。HTMLパーサはイベントごとに対応するハンドラが呼び出される構造になっています。fxhtmlparserはfxhtmlparser_initializeとfxhtmlparser_parseという関数を登録してFirefoxからパース時に発生するイベントをPHPで受け取れるようになっています。パーサからのイベントはlibfxparsのmake testの結果からわかるように、やや低レベルで扱いにくいのでFxHtmlParser.phpというラッパークラスを用意しました。 このクラスは下のようにして使います。
<?php
  require_once "FxHtmlParser.php";
  $xpcom_dir = array_shift( $argv );
  $p = new FxHtmlParser( $xpcom_dir );
  
  $html = file_get_contents("testparser.html");
  $dom = $p->parse( $html );
  print $dom->saveHTML();
?>
newするときに渡すパラメータはFirefoxのXPCOMファイルがおかれているディレクトリです。Fedora Coreの場合 /usr/lib/firefox-version を指定すればOKです。あとはふつうのDOMオブジェクトと同じようにsaveHTMLを使ってHTMLの文字列を取り出すこともできますし、DOMXPathを使って
$xp = new DOMXPath($dom);
$r = $xp->query("//li");
foreach ($r as $k => $v) {
    print( "$k: " . $v->textContent . "\n");
}
と書けば、testparser.htmlの中にあるliタグの子孫のテキストを
0: first list item
1: next list item
として取り出すことができます。

各パーサのHTML解釈の違い

ここで、使用するパーサによって壊れたHTMLの解釈がどう違うかを、実際に壊れたHTMLを与えてみてみましょう。 テストにはfxhtmlparser-0.0.1.tgzに入っているtestparser.htmlを使います。中身はこんな
<html>
  <p>
  broken unordered list
  <ul>
    <li><p><a href="http://example.com/">first list item</a>
    <li>next list item</p></li>
  <p>
  next paragraph
壊れ具合になっています。
htree
pure rubyのhtreeは、はじめにHTMLタグがないとうまく動かないことがありました。このテストコードの一番はじめのHTMLタグはhtreeのために入れてあります。
<html xmlns='http://www.w3.org/1999/xhtml'>
<p>
broken unordered list
</p><ul>
  <li><p><a href='http://example.com/'>first list item</a>
        <li>next list item</li></p></li>
</ul><p>
next paragraph
</p></html>
liタグが外側のpタグでネストされています。htreeには</li>の補完に関するルールが記述されていないのかもしれません。
tidy
html-lintのコアであるlibtidyのコマンドラインバージョンtidyを、結果をインデントする -i オプションをつけて実行した結果です。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">

<html>
<head>
  <meta name="generator" content=
  "HTML Tidy for Solaris (vers 1 September 2005), see www.w3.org">

  <title></title>
</head>

<body>
  <p>broken unordered list</p>

  <ul>
    <li>
      <p><a href="http://example.com/">first list item</a></p>
    </li>

    <li>next list item</li>

    <li style="list-style: none">
      <p>next paragraph</p>
    </li>
  </ul>
</body>
</html>
最後のpタグを前にあるulタグの小要素として解釈して、liを補完しています。ulの小要素はliでなければいけないのでtidyはHTMLとして正しい解釈をしていることがわかります。
PHP: DOMDocument->loadHTML()
PHPのDOMDocumentのメソッドloadHTMLは、読み込むHTMLが壊れている場合、エラーを出しつつも適当に解釈してDOMを構築してくれます。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body>
<p>
broken unordered list
</p>
<ul>
<li><p><a href="http://example.com/">first list item</a>
        </p></li>
<li>next list item</li>
<p>
next paragraph</p>
</ul>
</body></html>
tidy同様pタグがulの中に入っていますがliが補完されてはいません。 HTML4.0の定義ではulは小要素として1個以上のliのみを含むことになっているので、この解釈は正しくありません。
FxHtmlParser
Firefoxの場合はこうなります。
<html><body>
<p>broken unordered list
</p>
<ul> <li><p><a href="http://example.com/">first list item</a> </p></li>
<li>next list item<p></p>
</li>
<p>next paragraph</p>
</ul>
</body></html>
基本的にはloadHTMLと同じですが、</p>を無視せずに、前に<p>を補完しています。FirefoxもHTMLとしては正しくない形で解釈していることがわかります。

まとめ

壊れたHTMLを解釈してDOMを構築してくれるものはいろいろありますが、もともと情報が失われているところから構造を修復するため、それぞれの実装ごとに修復後の結果が異なります。そして、ulの中にpが入っていたりすることからもわかるように、修復後のDOMがHTMLとして正しい形になっているというわけでもありません。

通常は修復後のHTMLの構造が多少異なっていても問題になりませんが、ブラウザと密接な連携をとりつつDOMの要素を参照したい場合にはブラウザとHTMLの修復ルールが異なっていると大きな問題になります。
そんなときにはFirefoxのHTMLパーサを呼び出して使うと、ブラウザの解釈と厳密に同じ解釈をサーバですることができます。

tags

  • Firefox
  • HTML
  • PHP
  • XPCOM
  • 「FirefoxのHTMLパーサを使って壊れたHTMLを修復する PHP extension」のはてなブックマーク数
  • 「FirefoxのHTMLパーサを使って壊れたHTMLを修復する PHP extension」deliciousブックマーク数
  • 「FirefoxのHTMLパーサを使って壊れたHTMLを修復する PHP extension」をはてなブックマークに追加
  • save "FirefoxのHTMLパーサを使って壊れたHTMLを修復する PHP extension" to del.icio.us
  • 「FirefoxのHTMLパーサを使って壊れたHTMLを修復する PHP extension」をリアルタイムブログ検索
  • permalink
  • cookieのサイズはパフォーマンスに影響を与えるか
  • IEでjavascriptのエラーをデバッグする方法

comments

TypeKey Enabled
スタイル用のHTMLタグが使えます。
*required
すがわら
2008.04.23 11:09

FIREFOX_PREFIX=$(shell "grep" -oP 'firefox-\d(\.\d)+' /usr/bin/firefox | uniq)
が
firefox-2.0.0.13だとうまく取得しません
firefox-2.0.0.1と取得してしまうようです

trackbacks

トラックバック元エントリにこのエントリへのリンクがない場合はトラックバックを受け付けません。

http://labs.gmo.jp/mt/mt-tb.cgi/107
©2010 Kentaro Kumagai, GMO Internet Labs., GMO Internet, inc.
bits and bytes
2007 .03. 12 15:05

tagcloud

  • API1
  • C/C++2
  • E4X1
  • FUSE2
  • Firefox24
  • HTML4
  • IE1
  • MySQL1
  • OSX4
  • Opera2
  • PHP4
  • UI2
  • XML1
  • XPCOM4
  • XPath4
  • apache2
  • binary2
  • book1
  • data13
  • debug5
  • design1
  • experiments3
  • extension13
  • google gears1
  • google maps API1
  • greasemonkey4
  • httpd5
  • javascript19
  • linux1
  • logging2
  • mobile3
  • perl4
  • tips5
  • tool11
  • vim2
  • visualization3
  • widget1
  • wii2
  • windows7
  • サービス7
  • 和訳1

Archives

  • 2008.04 (3)
  • 2008.03 (4)
  • 2008.02 (6)
  • 2008.01 (3)
  • 2007.12 (4)
  • 2007.11 (5)
  • 2007.10 (4)
  • 2007.09 (4)
  • 2007.08 (4)
  • 2007.07 (8)
  • 2007.06 (7)
  • 2007.05 (4)
  • 2007.04 (5)
  • 2007.03 (6)
  • 2007.02 (4)
  • 2007.01 (6)

about

  • bits and bytesのXML
  • 「bits and bytes」のCreative Commons
  • Powered by Movable Type
  • イベントと地図 - モグ
  • Use ecto to blog!
  • bits and bytesのはてなブックマーク数
  • bits and bytesをMy Yahoo!に追加
  • Subscribe with Bloglines