2010年2月12日金曜日

WebViewを使ったスクレイピングの使い道

Android MLで WebViewとJavaScriptを使ったスクレイピング ってゆートピックを見つけたんだけど、現在進行形で正しくこのアーキテクチャを使ってプログラムを書いているところだったので、フォロー入れます。つーか入れずに居られない。

なんのこっちゃという方は、こちらを見てください。

WebViewを使ったスクレイピング / scraping a page using the WebView in a Service

スクレイピングにあたっての問題点

まず、自分の場合は特定ページのHTMLを定点観測するためにパーサーを書く必要があったんだけど、Javaに慣れてないので以下のような問題をどう超えようか悩んでいました。

  1. PullParserなどよりXPathを使いたい(使用されるリソース量よりコーディング手間の方が優先。とりあえず形にするにはコレ重要)
  2. 対象のページで使用される文字コードがなんだか分からない場合でも、なんとかしたい。(特にHTTPヘッダーで示される文字コードと、HTMLヘッダーで示される文字コードと、実際に使われている文字コードが違うような変態ページにも対処したい)
  3. XHTMLに準拠してないページでも、なんとかしたい。準拠してなくてもXMLパーサがなんとか使える程度ならいいんだけど。
  4. 壊れているHTML(閉じ忘れ)などでも、なんとかしたい。

ぶっちゃけ、対象ページが固定されていればあまり問題にはならないのですが、今後のため汎用的に作れないかなぁと思っていました。

解決策

んで、まぁいろいろ悩んだのですが、Android向けのアプリを書いていたのでWebView(WebKitをJNI経由でコントロールするためのGUI部品)を使えば、ほぼクリア出来るということがわかりました。

思い起こせば、昔Rubyでスクレイパーを書いていたとき、もじらのHTMLパーサ使おうかなぁと悩んだことがありまして、 理由は全く同じでした。特にHTMLが壊れていても、なんとかなるのが大きい!

特に上記2・3・4なんかはJavaでやってたら死ねたと思います。(単なる知識不足ですが)

補足

一応補足しておくと、上記1ですが、結局XPathは見送りになりました。上記2・3・4のメリットとの天秤でしたが、XPathを使うためには、Javaの外部ライブラリを使うか、JavaScriptの外部ライブラリを使うか、HTML5対応ブラウザの標準関数を使う必要があり、Androidに載っているブラウザはHTML5にはまだ対応出来ていないため、どれもちょっと微妙でした。自分の場合はDOMを使ってパースすることにしました。(DOMを使う場合でも、他の手段と比べてかなりコード量は減るはずです)

2についてはWebKit側で自動的に推定してくれます。精度はレガシーなページ(EUCやShiftJISなページ)をブラウザで開いてもまぁまぁきちんと表示されることから、まぁまぁ良いぐらいでしょうか。

3についてはブラウザ内のJavaScriptを使うことで解決しています。DOMでもXPathでも使えるはずです。個人的にはJavaでパーサを書くより好きです。

4については、WebKitが対象のHTMLを取得し、レンダリングのためWebKitのHTMLパーサーに渡した時点で、文法的に明らかにおかしい(trを閉じてないのにtableを閉じたなど)は、自動的に修正してくれます。ブラウザ内のJavaScriptからアクセス可能なのは、このパース後(修正後)のHTMLなので、スクレイピングに支障をきたすほど壊れたHTMLに出会う確率はぐっと減ります。

使いどころ

ざっと思いつくのはこんな感じでしょうか。

  • レガシーなページを含む場合のスクレイピング。最近のHTMLはXHTMLに対応しているこが多いけど、昔からある個人運営のページや、携帯向けページは未だにスクレイピングしづらいものがある。
  • 上記の発展として、「(単純にタグを除くだけの全文検索タイプではなく)タグ構造を考慮に入れたサーチエンジン」のためのクローラ&パーサ。まともにHTML解析機を作ってると死ねると思うので。
  • オートパイロット。例えばヤフオクなどで、ログインしてなかったら(特定文字を検出したら)オートパイロットモードになってログインし、指定されたページに入札を行うなど。同様のことはHTTPリクエストだけでも行えるが、今何が起こっているのか見えるのでユーザは安心かと。
  • Ajax前提のサイトなど、JavaScriptが動いてくれないと必要なデータを取得できないような場合。これが今まで出来なかった! Androidエライ!!
  • HTTP的なお作法に厳しいページへのスクレイパー。Railsなどはリダイレクトを多用するけど、自分でリダイレクト対応のコードを書くのは面倒だよね。Cookieも自動的に処理してくれるから、セッション管理が楽。Basic認証とかSSLとか数え上げれば切りが無いほど、「フツウ」にHTMLに触れるのはメリット。

で、デメリット

といいつつAndroidのWebViewも万能ではない。

  • WebViewはViewクラスを継承しているので、いわゆるUIスレッドでなければ正常に動かないかも。(下記Tips参照)
  • Javaのメリットって、比較的簡単にマルチスレッドを使えるってのがあるだろうけど、上記理由によりマルチスレッドでは動かない(可能性が高い。やり方によるのかも)
  • 上記理由により、意味不明に落ちることがある。そんときのバックトレースから、Looperの仕組みなどをきちんと理解する必要があるのかもしれない。ハマると抜け出すのに苦労する。
  • リソース大食い。ServiceからWebViewを使うことにした場合、これは頻繁に虐殺されることを指す。殺されないための手当と、殺された場合にシームレスに復活するための手当を要す。これはこれで結構面倒(かつノウハウがあまりWeb上に蓄積されていない)。また、このせい(リソース食い過ぎ)で他のServiceがまともに動かない可能性も。
  • (PCを含めた)スクレイパーとしては致命的に重い。Android内に限っても、、、やっぱり重いかも。特にHT03AのCPUではそう感じる。1GHzぐらいあればあまり気にならないかも。
  • 2つのWebViewを同時に動かすと落ちる? 不安定になる?(調査中)  JNI使う場合は、行儀良くしないとアプリではなくOSが落ちます。
  • Androidに載ってるJavaScriptエンジンって、V8じゃなかったような。。。感覚的にはJavaでXMLパーサを使うより、ブラウザ内でJavaScriptを使ってパースする方が、2〜3倍遅い気がする。(あくまで感覚値)
  • UTF8でXMLやXHTMLを返すとわかっている場合は、JavaでHTTP叩いてXMLパーサを通した方がきっと早くてリソース消費も少なくてマルチスレッドで動く。なんでもばっちこいな場合のみ、WebViewが使えるのかも。特にAndroid上で動かす必要が無いならば、他の方法とよく比較検討した方がいいかも。

Tipsというかバッドノウハウ

試行錯誤から得たノウハウをちょっとだけ。(出し惜しみではなく、もうハマリポイントを忘れたため)

  • 自分の場合、Serviceクラス(の派生)からWebViewクラス(の派生)を呼んでも、正常に動きました。WebViewを使ったスクレイピングのようにレンダリングやパラメータは調整しましたけど。この状態でActivityにバインドして毎分リクエストして24時間ぐらいは平気で連続取得に耐えていますので、お作法的にはよろしく無いかもしれないけど、実際には使えると思っています。まぁ、少なくともアプリが自分のコントロール下にある(ソースをいじれる)うちは、ServiceでWebView使ってもいいかなぁとか。
  • JavaからWebView.loadUrl("javascript:ごにょごにょ;");で、今開いているページに対して追加でJavaScriptを動かせることができる。ブックマークレットみたいな感じ。ちなみに、ここで渡すJavaScriptは相当長くても動いてくれる。
  • WebKitのHTMLパーサーにかけた後の、修正済みHTMLをJavaScript経由でJavaに取り込むこともできる・・・ハズ。JavaScriptでHTMLタグのinnerHTMLを取得するなど。ハズっつーのは、実際にやってみたけど尻切れになっちゃうから。HTMLタグの子供をループしてのinnerHTML取得だと、もう少し取れるけど、完全に取れたか確認出来ないし。。。うまくいった方はやり方教えてください。
  • ってか、NDK使って、WebKitの文字コード推定&コンバートとHTMLパーサ部分だけを使うJNI作っちゃえばいいのかも。ってか、コレAndroidの標準ライブラリに入れてくれないかなぁ。

まとめ

長くなったので、まとめてみる。

  • やんちゃなページを相手にスクレイピングを仕掛けたいならWebViewに限る
  • とくにAjaxなヤツを相手にするなら現状WebView以外の選択肢は無いように思える
  • でもこいつはクセが強いぞ。心して掛かれ

3 件のコメント:

keigoi さんのコメント...

とても参考になります!
フォローありがとうございます。

> Javaのメリットって、比較的簡単にマルチスレッドを使えるってのがあるだろうけど、上記理由によりマルチスレッドでは動かない(可能性が高い。やり方によるのかも)

シングルスレッドで動いているケースなら、マルチスレッドでも動かないことはないと思いますです。
UIスレッド以外からUIを叩くのはよろしくないので(Looperが居ないのでたぶん落ちる)、
別スレッドからUIを叩きたい場合は Handler#post でUIスレッドにやりたい処理をポストするのがよいようです。
ご存知かもしれませんが、ご参考まで、

まいむぞう さんのコメント...

はい。Handler#post()を使う方法は知っていたのですが、この場合肝心の並行動作させたい処理(WebKitのコントロール)が、UIスレッドに集約しまうので、マルチスレッドの恩恵は受けられないかもしれないなぁと思ってました。
良く考えてみれば、JNI経由でWebKitを叩いても、処理終了を待たずにすぐ帰ってくるようであれば、この方法でも良いのかも!?
ってかそもそも、WebKit側が(UIスレッドに集約されていたとしても)マルチスレッドから操作されることに対応出来ているのかな?とかとか。UIスレッドに集約することでスレッド間の同期は問題なくても、スレッドAがあるページのロードを依頼したときに、スレッドBが別ページにポストしたら、やっぱり全体としてダメなんじゃないかな、という話でした。
この場合は、やっぱりJavaだけでパースする必要があるのかも。

keigoi さんのコメント...

マルチスレッドの恩恵はとくに無いんじゃないかなと思います。
まずHT-03Aのようにシングルコアであれば並列化の恩恵は無いですよね。
加えて、WebViewではメインスレッドとレンダリングに1本(Looper)とhttp接続に4本スレッドが走っているようなので、どこかでIOがブロックしても全体が止まるということはないと思います。
でわでわ