2007-12-29 22:42  第13号

use Web::Scraper;

    年の瀬にさりげなく再開。

    久しぶりということでネタはたくさんあるのですが、ぱっとすぐ思いつくおススメ Web::Scraper を紹介。これはその名のとおり、ウェブのスクレイピング(HTML のある部分を抽出)用のモジュールです。半年くらい前に生まれた新しいモジュールでありながら、すでにこの分野でメジャー感がある miyagawa プロダクトです。

    API が用意されているサイトの情報は普通に API で取ればよいですが、世の中そうばかりでもないわけで、HTML を文字列処理したりするわけです。昔からこの辺は Perl が得意としてきたところですが、Web::Scraper がどう新しくて便利なのかは、作者によるプレゼン資料にわかりやすく説明されているとおりです。

    • 正規表現ではなく CSS のシンタックス(や XPath)で切り出し場所を特定できる
    • トライ&エラーに便利なコマンドラインインターフェースが付属
    • 文字コードや URL の正規化など裏側処理を Web::Scraper がワンモジュールでめんどうみてくれるので、コードがすっきり

    素敵、というわけでさっそく

    使い方

    Amazon の商品ページから価格部分を取るサンプルで説明してみます。

    use Web::Scraper;
    use URI;
    
    # 価格部分を yen という名前で取るスクレイパーを作成
    my $scraper = scraper {
        process '#buyboxTable b.price', 'yen' => 'TEXT';
    };
    
    # 悪魔の箱ページのURLオブジェクトを、
    my $uri = new URI('http://www.amazon.co.jp/o/ASIN/B000WQKBE2/');
    
    # 先ほどのスクレイパーに渡す。(スクレイピングされる)
    my $res = $scraper->scrape($uri);
    
    print $res->{yen}; # ¥ 4,401
    

    Web::Scraper は、use すると scraper という関数をエクスポートします。scraper は、サンプルのようにスクレイピングの指示をブロック({ })の形で取り、スクレイパーオブジェクトを返します。そのスクレイパーは scrape() という関数を持ち、それに URL とか渡すとスクレイピングして結果を返してくれます。

    解説

    scraper に渡すブロックの中の構文は以下のようになります。

    process どこから, 何というキー名で => どう取得(値の形式);
    

    先ほどのサンプルだと、'#buyboxTable .price' という箇所から、yen という名前でその 'TEXT' を取る、みたいに読めます。

    1. どこから

    ここには HTML から切り出す場所を CSS セレクタもしくは XPath で記述します。Amazon の価格部分は id="buyboxTable" の中にある <b class="price"> だったので '#buyboxTable b.price' と指定しています。

    もちろん XPath で '//table[ @id="buyboxTable" ]//b[ @class="price"]' みたいな感じでも OK ですが、この場合は CSS セレクタの方がわかりやすい。XPath はひどいマークアップの HTML から何かを取りたい時とかに本領発揮します。

    2. キー名

    scraper が取るブロックには、そのページで取りたい箇所の分 process 行を並べることができますが、それぞれの結果に付けるキー名を指定します。

    my $scraper = scraper {
        process '#buyboxTable .price', 'yen' => 'TEXT';
        process '#primaryUsedAndNew .price', 'used' => 'TEXT';
        process '//*[ @id="ftMessage" ]//b', 'delivery' => 'TEXT';
    };
    

    これで、$scraper->scrape($uri) の結果は以下のようになります(utf8 フラグ付き)。

    {
        yen => "¥ 4,401",
        used => "¥ 3,639",
        delivery => "2007/12/28 金曜日 にお届けします!",
    }
    

    ちなみに process の仲間に result というのがあって、これでキー名を指定しておくとハッシュじゃなくてそのものが返ってきます。詳しくは後述の実例のリンク先で見てください。

    セレクタの指定によっては合致する部分が複数ある場合がありますが、キー名を 'item[]' のように各カッコ付きで指定することで、リストで取得することもできます。

    # 全 <a> タグの href 属性を url というキーにリストで
    my $res = scraper { process 'a', 'url[]' => '@href'; }->scrape($uri);
    
    use YAML;
    print Dump $res;
    
    # 結果
    ---
    url:
      - !!perl/scalar:URI::http http://www.amazon.co.jp/ref=bd_vg/250-9239974-3346614
      - !!perl/scalar:URI::http http://www.amazon.co.jp/gp/cart/view.html/ref=topnav__vg/250-9239974-3346614
      - !!perl/scalar:URI::http http://www.amazon.co.jp/gp/registry/wishlist/ref=topnav__vg/250-9239974-3346614
    ...
    

    URI モジュールはそのうち取り上げます。

    3. 値の形式

    'TEXT''@href' が出てきましたが、値の取り方をいろいろな方法で指定できます。ここが柔軟で、いろいろ工夫のしがいがあるところです。

    まずは基本。http://example.com/ が

    <div>
        <a href="/2008.html" target="_blank">Happy New Year</a>!
    </div>
    

    という HTML だったとすると以下のようになります。

    • 'HTML' はその部分の内側の HTMLです。
      process 'div', 'test' => 'HTML';
      の結果は '<a href="/2008.html" target="_blank">Happy New Year</a>!'
    • 'TEXT' は HTML タグを取りはらったもの。
      process 'div', 'test' => 'TEXT';
      の結果は 'Happy New Year!'
    • '@属性名' で属性の値。
      process 'div a', 'test' => '@target';
      の結果は '_blank'

    これ以外の方法で取りたい時用として、

    • scraper {} をさらに渡してネストさせる
    • フィルタ という 'TEXT' などを手軽に加工する方法が用意されています
    • さらに サブルーチンを渡す$_ としてその HTML::Element が来るので好きにできます。

    これらについては後述の実例リンク先を参照。

    一つの場所から上記を組み合わせて取りたい時は、scraper を渡してネストさせるか、もしくは

    • ハッシュ でまとめます。

      process 'div a', 'link' => {
          text => 'TEXT',
          host => [ '@href', sub { $_->host } ],
      };
      

      で以下のように取れます。

      link => {
          text => 'Happy New Year',
          host => 'example.com',
      }
      

    コマンドラインインターフェース

    Web::Scraper をインストールすると scraper というコマンドが入ります。これが超絶便利です。HTML はサイトによってまちまちなのでトライアンドエラーを繰り返しがちですが、いつのまにかスクレイピングに成功するのが目的になってしまってたり。。みたいな事態を防げます。

    $ scraper <取るURLまたはhtmlファイルのパス>
    

    で始めます。
    さっそくミニミニさまーずの放送時間を Yahooテレビのこのページから取ってみることにします。

    $ scraper "http://tv.yahoo.co.jp/bin/sea...(長いので省略)"
    
    scraper>
    

    と、そのページについての scraper シェルが立ち上がります。ここで、おもむろに process 文を作っていくのですが、実際の対象の HTML はいつでも

    scraper> s
    

    と、s コマンドでソースを見ることができます。ただし、scraper + Firebug で鬼に金棒です。

    Firebug からコピーした XPath を使って process 文を作っていきます。scraper シェルの中だけ 「何というキー名で => どう取得(値の形式)」の代わりに WARN というのが使えます。これはその場ですぐ結果を見ることができるものです。

    scraper> process '/html/body/center/table[6]/tbody/tr[2]/td[2]/small', WARN;
    

    何も起きません。実は Firebug で取った table タグ内の XPath は Firefox がレンダリング時に加えた実際の HTML にはない tbody タグが入っているので、それを取ります。

    scraper> process '/html/body/center/table[6]/tr[2]/td[2]/small', WARN;
    <small>20:54〜21:00</small>
    

    これでうまく出ました。他のを全部取ってみるよう tr の指定を取ります。こういった微調整&確認がすぐできるのが scraper シェルのすばらしいところ。

    scraper> process '/html/body/center/table[6]/tr/td[2]/small', WARN;
    <small>20:54〜21:00</small>
    <small>26:25〜26:30</small>
    <small>17:54〜18:00</small>
    ...
    

    うまくいったので、WARN を 'list[]' => 'TEXT'; に変えます。変えた後は、y コマンドで、YAML::Dump した結果を表示できます。

    scraper> process '/html/body/center/table[6]/tr/td[2]/small', 'list[]' => 'TEXT';
    scraper> y
    ---
    list:
      - 20:54〜21:00
      - 26:25〜26:30
      - 17:54〜18:00
    ...
    

    最後に、c でコードを吐き出してくれます。試行錯誤した process 文を全部出したい場合は c all です。

    scraper> c
    #!/usr/bin/perl
    use strict;
    use Web::Scraper;
    use URI;
    
    my $uri = URI->new("http://tv.yahoo.co.jp/bin/search?p=%A5%DF%A5%CB%A5%DF%A5%CB%A4%B5%A4%DE%A4%A1%A1%C1%A4%BA&cate=0&search222=%B8%A1%BA%F7&area=tokyo");
    my $scraper = scraper {
        process '/html/body/center/table[6]/tr/td[2]/small', 'list[]' => 'TEXT';
    };
    my $result = $scraper->scrape($uri);
    

    まあこれこの年末しか使えないし、ブラウザで表示している時点で目的は達成されている気がしますが、まあサンプルということで(笑)

    TIPS

    UserAgent を変える

    use LWP::UserAgent;
    my $ua = new LWP::UserAgent( agent => 'Mozilla/5.0 ...' );
    
    # セット
    $scraper->user_agent($ua);
    
    # その UA でスクレイピング
    print Dumper $scraper->scrape($url);
    

    Basic 認証

    # 普通にこの方法が楽
    my $url = new URI('http://user:pass@example.com/private/');
    

    ログイン認証

    $scraper->scrape() には URI オブジェクト以外に、HTML そのものを文字列で渡すこともできます。なので、普通に Mech とかで進んで、コンテンツを Web::Scraper に渡すとよいです。

    use WWW::Mechanize;
    my $mech = new WWW::Mechanize;
    
    # mech で進んで...(ニコニコ動画の)
    $mech->get('http://www.nicovideo.jp/ranking/view/daily/all');
    $mech->submit_form(
        fields => {
          mail => 'me@example.jp',
          password => 'p4ssw0rd',
        },
    );
    
    # ランキングを取るスクレイパーに
    my $scraper = scraper {
        process 'a.video', 'ranking[]' => '@href';
    };
    
    # mech から HTMLを渡してスクレイプさせる
    my $res = $scraper->scrape($mech->content);
    

    HTML 渡しの場合スクレイパーはそれが何という URL のものなのかわかりませんが、 Web::Scraper にある URL の正規化機能を生かすには、scrape() の第二引数で渡せばよいので、mech からの場合は以下のようにすると良いです。

    my $res = $scraper->scrape($mech->content, $mech->uri);
    

    実例

    Happy holidays!

    まぐまぐ、半年発行しないでいると廃刊扱いになってバックナンバーごと消えてしまうという仕様らしいのであわてて配送。そういう仕組みだったとは(笑) とはいえリクエストもあったので来年は定期的に書こうと思います。

    ではみなさん、よいお年を

    SEE ALSO

    Web::Scraper

    内村プロデュース~発酵紀

    WRITTEN BY

    冨田 尚樹 <tomita@cpan.org>

    Feedback

    はてなブックマーク livedoorクリップ Yahoo!ブックマーク del.ico.us use Web::Scraper;

    Trackback

    url: http://e8y.net/mt/mt-tb.cgi/211

    Comments