FirefoxのhtmlparserをXPCOM経由で呼び出して壊れたHTMLを修復する

dapper のように、ブラウザでクリックをしたところをスクレイプする、というときにはXPathが向いています。ブラウザでクリックした部分のXPathをサーバに保存しておけば、あとで保存したXPathに従ってドキュメントからエレメントを取り出すことができます。
しかし実際にHTMLからXPathを使ってエレメントを取り出すときに大きな問題になるのが、ウェブページの大半(体感で70%くらい)は記述されているHTMLが構造的に壊れているという事実です。タグを開いたまま閉じていなかったり、開いていないものを閉じていたり、ドキュメントの最後に</body></html>が二重に入っていたり、壊れかたは様々ですが、とにかくウェブ上のHTMLは大半が壊れています。壊れているHTMLが大半だからなのか、壊れているHTMLでもブラウザが適当に解釈して表示してくれるからなのか、とにかく世の中のヘージの大半は構造的におかしい状態になっています。
HTMLが壊れていることはなぜ問題になるかというと、XPathを利用するためには前段階でドキュメントをXMLパーサにかけてDOMを構築する必要があります。そしてXMLパーサは構造的に正しい(well-formedな)XMLドキュメントしかパースしてくれません。そうでないものはすぐにエラーを出して処理が止まってしまうのでXPathでエレメントを取り出したりはできません。たとえば、perlのXML::XPathの場合、内部でexpatというXMLパーサを使っています。このパーサにwell-formedでないドキュメントを与えるとドキュメントが壊れている位置を教えてくれるだけでなにも処理してくれません。

XPathでスクレイピングをするなら、XMLパーサにドキュメントが構造的に壊れていないかを調べて、壊れていたときにはXMLパーサが処理できる構造にまで修復する必要があります。まずはそんなことをやってくれるライブラリをいくつか調べました。

libtidy
古くからHTMLバリデータとして知られているHTML-lintのコアになっているのがlibtidyです。ただ、少し試してみた感じでは修正のしかたが人間の直感ともFirefoxとも違うという意味で、ちょっとくせがありました。PHPバインディングがあります。 余談ながらwikipediaによると、もともとlintというのはC言語で書かれたソースコードを静的に解析して問題のありそうな部分を見つけてくれるツールの名前だそうで、ほかにもいろんな言語用のtidyがあります。
htree
進化する“Webスクレイピング”技術の世界 − @IT の中で紹介されているhtreeはrubyのモジュールで、HTMLをパースして pure ruby なXMLパーサであるREXMLのドキュメントとして返してくれるものです。htreeはHTMLの壊れている部分に遭遇すると、内部でhtmlinfo.rbに定義されているルールに従って適当な構造に修復してREXMLのドキュメントを構築してます。いくつかのページを入れて試したところ、非常に良い結果が得られました。
HTML::TreeBuilder::XPath
perlにはHTMLをパースしてノードのツリーを作ってくれるHTML::TreeBuilder::XPathがあります。C言語で書かれたHTML::Parse.pmがトークンに分解し、HTMLの壊れた部分をHTML::TreeBuilderが修復しながらツリーを作ります。修復ルールはツリーを作るperlのコード中にインラインで記述されているため網羅的ではなく、あくまで実用上問題ないレベルに修復するのが目的で、完全にHTMLとして論理的に正しい状態にまでは修復しないようです。

ふつうにXPathを利用したいときにはこれらのツールで十分に目的を達成できます。
ですが、ブラウザでクリックしたエレメントのXPathを保存して、後からサーバで保存したXPathを使ってエレメントを抜き出したい、というときには、ブラウザがHTMLを修復するロジックと、サーバがHTMLを修復するロジックが厳密に同じである必要があります。XPathの書き方で少しは差異を吸収できるものの、基本的には少しでも違っていると違うエレメントが参照されてしまうので、ブラウザとサーバ間で修復の仕方が違うのは致命的なのです。

そんな状況だったところに、先日FirefoxのhtmlparserをXPCOM経由でjavaから利用するためのラッパーライブラリ Mozilla Java Html Parser というものがリリースされました。それもいま最もかっこいいスクレイピングツールのdapperのデベロッパーが開発したというものです。同じ問題をdapperも抱えていたのかと感慨深かったと同時に、その問題を直球で解決しちゃうところにうたれました。このライブラリは、Firefoxの持っているhtmlparserをXPCOMというCOMに似たインターフェイスを使って呼び出して、壊れたHTMLを当然Firefoxと全く同じルールで復元してくれます。

Firefoxのビルド

そこでさっそく試してみようというわけで試してみました。ただ、自分はjavaをまともに使えないので、javaのラッパー部分をはがして直接XPCOMを呼び出してた、という感じです。バイナリが用意されているようなのでjavaが使えればすぐに試すことができるのだと思います。(もしこれをご覧の方でjavaが使える方がいらしたら試してみていただけないでしょうか) Mozilla Java Html Parser を自分でソースをビルドする場合はFirefox自体のビルドも必要になってきます。現在FirefoxのCVSからソースを取得するとFirefox3系のソースがダウンロードされてきますが、この Mozilla Java Html Parser はFirefox2系のために書かれたもののようなので、FTPからFirefox2系のソースをダウンロードしてくる必要があります。 今回ビルドに使った環境は、前にFUSEでソーシャルブックマークのブックマークをコピーするのに使ったVPC上の Fedora Core 6 です。 はじめにpangoを新しくした以外は、やや古い記事ながら IBM XPCOM: 第3回: XPCOMのセットアップ - Japan に書かれている通りではまったりすることはなくすんなりとビルドすることができました。 Firefoxのビルドには非常に時間がかかりますが、エラーが出て止まることもありませんでした。Linux上でのビルドはよくメンテナンスされているのかもしれません。不必要なモジュールを外せば早くなるのかもしれませんが、Firefoxのビルドははじめてで何も知らなかったので.mozconfigにはなにも記述していません。
sudo yum update pango
wget 'ftp://ftp.mozilla.org/pub/mozilla.org/firefox/releases/2.0/source/firefox-2.0-source.tar.bz2'
tar xvfj firefox-2.0-source.tar.bz2
cd mozilla
./configure --enable-application=browser
gmake
gmake -f client.mk build

これでビルドしてできあがったfirefoxが正しく動くかどうかは確認していませんが、今回の目的であるhtmlparserの動作には問題がありませんでした。

ビルド

今回はとりあえずCで動かすだけなので Java Mozilla Html Parser を展開して中から MozillaHtmlParser/native/src の JavaContentSink.cpp, JavaContentSink.h, JavaContentSinkHack.h をビルドディレクトリにコピーしてきます。 Makefileは mozilla/config/autoconf.mk あたりを利用すれば簡単にできたりするのかもしれませんがわからないので、同様に MozillaHtmlParser/native/linuxMake.sh をもとに適当に削ったりして作りました。main関数の入っているhtmlparser.cppは MozillaHtmlParser/native/src/MozillaParser.cpp をひとつにまとめただけです。 このまま実行すると何も出力されないため(それだけでなく自分の環境ではセグりました)、JavaContentSink.cppの JavaContentSink::callback と JavaContentSink::setJavaEnviroment の中身をカラにして、デバッグ出力をオンにします。
72c72
< bool JavaContentSink::doDebug=false;
---
> bool JavaContentSink::doDebug=true;
728,730d727
< jstring string1 = env->NewStringUTF(arg1);
< jstring string2 = env->NewStringUTF(arg2);
< env->CallVoidMethod(mozillaParserObject , callbackMid , string1 , string2 );
734,740d730
< env = aEnv;
< if (doDebug) printf("Setting java enviroment...");
< mozillaParserObject = aMozillaParserObject;
<
< jclass cls = env->GetObjectClass(mozillaParserObject);
< callbackMid = env->GetMethodID(cls , "callback" , "(Ljava/lang/String;Ljava/lang/String;)V");
<

これでmakeするとhtmlparserという名前の実行ファイルができあがります。

実行

実行するときにはXPCOMのファイルがある場所にLD_LIBRARY_PATHが通っている必要があります。
env "LD_LIBRARY_PATH=$MOZDIR/dist/lib:$MOZDIR/dist/bin/components" ./htmlaprser hello.html
<ul>
  <li><a href=&qout;./&qout;>listA
  <li><a href=&qout;/>listB
</div>
こんな壊れたHTMLを入れると、下のように解釈されてでてきました。(読みやすいように整形してあります)
<begin>
 <open container="html">
  <open container="body"></leaf>
   <open container="ul"></leaf>
    <open container="li">
     <open container="a"><attr key="href" value="./"/></open>
      <text value="listA"/></leaf>
     </open container="a">
    </open container="li">
    <open container="li">
     <open container="a"><attr key="href" value="/"/></open>
      <text value="listB"/></leaf>
     </open container="a">
    </open container="li">
   </open container="ul">
  </open container="body">
 </open container="html">
</begin>

感想

というわけでFirefoxの持っているhtmlparserを使ってHTMLを修復してみました。 ビルドが面倒、というのが致命的なのですが、このXPCOMを使ってFirefoxの機能を部分的に呼び出して使う、というのはFirefoxの持っているパワーを部分的に持ってくることができるため非常に便利そうです。

tags

  • Firefox
  • HTML
  • XPCOM
  • tool
  • 「FirefoxのhtmlparserをXPCOM経由で呼び出して壊れたHTMLを修復する」のはてなブックマーク数
  • 「FirefoxのhtmlparserをXPCOM経由で呼び出して壊れたHTMLを修復する」deliciousブックマーク数
  • 「FirefoxのhtmlparserをXPCOM経由で呼び出して壊れたHTMLを修復する」をはてなブックマークに追加
  • save "FirefoxのhtmlparserをXPCOM経由で呼び出して壊れたHTMLを修復する" to del.icio.us
  • 「FirefoxのhtmlparserをXPCOM経由で呼び出して壊れたHTMLを修復する」をリアルタイムブログ検索
  • permalink
  • スクレイピングはもっと簡単にならなければいけない
  • cookieのサイズはパフォーマンスに影響を与えるか

comments

TypeKey Enabled
スタイル用のHTMLタグが使えます。
*required

trackbacks

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

http://labs.gmo.jp/mt/mt-tb.cgi/104
©2010 Kentaro Kumagai, GMO Internet Labs., GMO Internet, inc.
bits and bytes
2007 .03. 01 0:14

tagcloud

  • API1
  • C/C++2
  • E4X1
  • FUSE2
  • Firefox18
  • HTML4
  • IE1
  • MySQL1
  • OSX4
  • Opera2
  • PHP4
  • XML1
  • XPCOM4
  • XPath3
  • apache2
  • binary2
  • book1
  • data11
  • debug4
  • design1
  • experiments3
  • extension10
  • google gears1
  • google maps API1
  • greasemonkey3
  • httpd5
  • javascript17
  • linux1
  • logging2
  • mobile3
  • perl4
  • tips4
  • tool11
  • vim2
  • visualization2
  • widget1
  • wii1
  • windows7
  • サービス6
  • 和訳1

Archives

  • 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