為さねば成らぬ

retia.verno@gmail.com

Androidでスクレイピング with TagSoup

この記事はクローラー/スクレイピング Advent Calendar 2014 - Qiita 21日目の記事です。

今回はAndroidスクレイピングをする話です。 PxViewerNicoscapeといったアプリを公開していますが、どちらもページからスクレイピングしたものを表示しています。 そこで使っているTagSoupについて紹介しようと思います。ただし辛いです。

対象読者

Androidスクレイピングをしたい、さらに実装が辛くても少しでも軽量高速なものをお求めの方

TagSoup

TagSoupはJavaで書かれているパーサです。実はAndroid Sdkの中でもHtmlクラスで使われています。(https://github.com/android/platform_frameworks_base/blob/59701b9ba5c453e327bc0e6873a9f6ff87a10391/core/java/android/text/Html.java)

特徴としてはSAXが使えることです。

SAX

一般的なスクレイピングライブラリではそのHTMLを操作するAPIとしてDOMが用いられています。DOM(Document Object Model)は、パースの処理で最初にXML(HTML)を読み込みそれを木構造の形でメモリ上に構築します。この木構造の葉がHTMLでの各ノードに対応することになるので、その葉を探索することで目的の情報を取得することになります。 DOMは一度HTMLを木構造として構築してしまえば、各ノードへのアクセスは楽になります。ただその反面データをすべて格納するためにメモリが必要になる、構造構築のために時間がかかると言った問題があります。

それに対して軽量高速なのがSAX(Simple API for XML)です。SAXはXML(HTML)を頭から読んでいきます。その中でパーサは読んだものを次の3つとして認識し処理していきます。

  1. 開始タグ : <a href="hogehoge">のような、開始のタグです。タグ名,属性が取得されます。
  2. 終了タグ : </a>のような、タグを終了するものです。タグ名のみで属性は含まれません。
  3. 文字 : 開始・終了タグ以外の文字です。タグ内部でない文字は1文字ずつ認識されることになります。

SAXはHTMLを頭から読んでいった中でこれらを認識すると、その段階でイベントを発生させます。 このイベントを処理することでHTMLから必要な情報を取得していきます。

例えば以下のようなHTMLを想定してみましょう。

<html>
  <body>
    <a href="http://hogehoge">Hello</a>
  </body>
</html>

これをSAXでパースすると、以下のようにイベントが発生します。

  1. 開始タグ: タグ名html
  2. 開始タグ: タグ名body
  3. 開始タグ: タグ名a, href属性http://hogehoge
  4. 文字: H
  5. 文字: e
  6. 文字: l
  7. 文字: l
  8. 文字: o
  9. 終了タグ: a
  10. 終了タグ: body
  11. 終了タグ: html

このHTMLから"Hello"というデータを取得したい場合には、href属性にhttp://hogehogeを持つaの開始タグ が出現してから aの終了タグ までの間に出現する 文字 を取ってくればよいということになります。

実装

TagSoupを利用する場合にはHPからjarを落としてきて、プロジェクトに外部jarとしてimportする必要があります。 ここ等を参考にしてください。

SAXによるHTMLのパースでは、以上のイベントを処理する部分が必要になります。TaSsoupではDefaultHandlerクラスを実装することにより処理を記述します。実装すべきメソッドは基本的に3つです。 startElement(String uri, String localName, String qName, Attributes attributes) endElement(String uri, String localName, String qName) characters(char ch[], int start, int leng) それぞれ開始タグ/終了タグ/文字を表しています。

早速ですが、先ほどのHelloを取得するコードを見てみましょう。

public class SampleHandler extends DefaultHandler {
    private boolean mInAtag = false;
    private StringBuilder mSb;
    private String mWord;

    @Override
    public void startElement(String uri, String localName, String qName, Attributes attributes) {
        f(qName.equals("a")){
            mInAtag = true;
            mSb = new StringBuilder();
        }
    }

    @Override
    public void endElement(String uri, String localName, String qName){
        if(qName.equals("a")){
            mInAtag = false;
            if(mSb != null){
                mWord = mSb.toString();
            }
        }       
    }

    @Override
    public void characters(char ch[], int start, int leng) {
        if (mInAtag && mSb != null) {
            for (int i = start; i < start + leng; i++) {
                mSb.append(ch[i]);
            }
        }
    }

    public String getWord(){
        return mWord;
    }
}
HttpClient client = new DefaultHttpClient();
HttpGet httpget = new HttpGet("http://hogehoge/index.html");
HttpResponse response = client.execute(httpget);
Parser parser = new Parser();
SampleHandler handler = new SampleHandler();
parser.setContentHandler(handler);
parser.parse(new InputSource(response.getEntity().getContent()));

String word = parser.getWord();

SampleHandlerを実装し、それに対してParserを用いてHTMLのソースを流します。すると各イベントが処理されて最終的にSampleHandler.getWord()でHelloがとれます。 してから終了するまでのフラグを表しています。そしてmInAtagが真のときのみ,characters()にて出てきた文字がmSbに格納されます。最終的にaタグが閉じられた段階でmSbの文字列をmWordに移し、それが目的のものとなります。

まとめ

本記事ではSAXパーサとしてTagSoupを使うことで、Androidスクレイピングを行う方法をご紹介しました。 Androidだけに限らず、もちろんJavaでも利用は可能です。

ただし一言言っておきますと、実装は非常に面倒 です。ソースに示した通り各イベントについて処理を書かなければならず、たとえば1つのHTMLから複数の情報を取ろうとした場合どのタグが出た、どのタグが出てない、でフラグ判定が多数必要になります。そして非常に煩雑になります。

PxViewerをリリースした当時はAndroid2.2の時代で、まだまだ端末側のリソースが少ない状況でした。その中では軽量高速なSAX,TagSoupが合っていたのです。 ただ今ならばメモリが足りないなどということは起こりにくくなりましたし、Jsoupなど普通にDOMパーサで実装することをお勧めします。