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パーサが処理できる構造にまで修復する必要があります。まずはそんなことをやってくれるライブラリをいくつか調べました。
ふつうにXPathを利用したいときにはこれらのツールで十分に目的を達成できます。
ですが、ブラウザでクリックしたエレメントのXPathを保存して、後からサーバで保存したXPathを使ってエレメントを抜き出したい、というときには、ブラウザがHTMLを修復するロジックと、サーバがHTMLを修復するロジックが厳密に同じである必要があります。XPathの書き方で少しは差異を吸収できるものの、基本的には少しでも違っていると違うエレメントが参照されてしまうので、ブラウザとサーバ間で修復の仕方が違うのは致命的なのです。
そんな状況だったところに、先日FirefoxのhtmlparserをXPCOM経由でjavaから利用するためのラッパーライブラリ Mozilla Java Html Parser というものがリリースされました。それもいま最もかっこいいスクレイピングツールのdapperのデベロッパーが開発したというものです。同じ問題をdapperも抱えていたのかと感慨深かったと同時に、その問題を直球で解決しちゃうところにうたれました。このライブラリは、Firefoxの持っているhtmlparserをXPCOMというCOMに似たインターフェイスを使って呼び出して、壊れたHTMLを当然Firefoxと全く同じルールで復元してくれます。
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の動作には問題がありませんでした。
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という名前の実行ファイルができあがります。
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>
トラックバック元エントリにこのエントリへのリンクがない場合はトラックバックを受け付けません。
http://labs.gmo.jp/mt/mt-tb.cgi/104
comments