Fork me on GitHub

2009/02/14

はてな
Luaも動的にメンバが追加出来る言語ですので、データ構造を動的に作り上げる事が出来ます。今日はなんとなく、「LuaでWebScraper作ったら、どんなんになるんだろう...」と思いつきで...
用意するのは
  • LuaからXPathを操作出来るltxml
  • LuaからSocketを操作出来るLuaSocket
あくまでサンプルですので、CSSセレクタも使えなければ最新のWebScraperの様に相対/絶対URL展開や、フィルタ等はサポートしていません。
またresultも動作させていない為、結果が全て戻ります。
さらにltxmlが内部で使っているTinyXML/TinyXPathの仕様からか、XPathの途中に「//」をめり込ます事が出来ませんでした。
まずLua版WebScraperのソース。

luascraper.lua
local http = require("socket.http")
local xml = require("xml")

-- return process structure
function process(t)
  return {name="process",process={xpath=t[1], name=t[2], scraper=t[3]}}
end
-- not supported
function result(p)
  return p
end
-- return scraper structure
function scraper(self)
  self.name = "scraper"
  -- scrape method
  function self.scrape(url, ctx)
    -- create http session and parse HTML
    if ctx == nil then
      local chunk = {}
      local b, c = http.request {
        method = "GET",
        url = url,
        sink = ltn12.sink.table(chunk)
      }
      if not c == 200 then
        return nil
      end
      local html = table.concat(chunk)
      ctx = {op = self, doc = xml.parse(html)}
    end
    -- scraping...
    self.res = {}
    if not ctx.doc then
      return nil
    end
    for k,v in pairs(self) do
      if (type(v) == "table" and v.name == "process") then
        if (type(v.process.scraper) == "table") then
          for k1,v1 in pairs(ctx.doc:select(v.process.xpath)) do
            local newctx = {top=ctx.top, doc=v1}
            self.res[#(self.res)+1] = v.process.scraper.scrape(url, newctx)
          end
        else
          local node = ctx.doc:select(v.process.xpath)
          if node then
            local attr = v.process.scraper
            if attr == "TEXT" then
              self.res[v.process.name] = node[1]:text()
            elseif string.sub(attr, 1, 1) == "@" then
              attr = string.sub(attr, 2)
              self.res[v.process.name] = node[1]:attribute(attr)
            end
          end
        end
      end
    end
    return self.res
  end
  return self
end
案外ボリューム無く書けました。
このモジュールを使って私のtwitter/with_friendsの発言リストをスクレイピングするLuaスクリプトがコレ

twitter_scraper.lua
require "luascraper"

local s = scraper {
  process {'//tr[@class="hentry"]',
    'status', scraper {
      process {'//td[@class="content"]/strong/a', 'nick', 'TEXT'},
      process {'//td[contains(@class,"author")]/a/img', 'name', '@alt'},
      process {'//td[contains(@class,"author")]/a/img', 'image', '@src'},
      process {'//span[contains(@class,"entry-title")]', 'description', 'TEXT'},
      process {'//a[@rel="bookmark"]', 'url', '@href'},
    }},
  result = 'friends';
}

function dump(r)
  print('-')
  for k,v in pairs(r) do
    if type(v) == "table" then
      dump(v)
    else
      print(k,v)
    end
  end
end
r = s.scrape('http://twitter.com/mattn_jp/with_friends/')
dump(r)
そして結果がコレ
image   http://s3.amazonaws.com/twitter_production/profile_images/24072332/negipo_normal.png
name    Yoshiteru Negishi
url http://twitter.com/negipo/statuses/371889282
description fooo.nameのおかげで自分が入り忘れてるウェブサービスがわかるようになった
nick    negipo
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34076402/crestock-221345-1024x768_normal.jpg
name    ハラヘ(´・ω・`)
url http://twitter.com/mobcov/statuses/371888702
description 今見るとシローアマダはかっこ悪いけどアイナサハリンは(ry
nick    mobcov
-
image   http://s3.amazonaws.com/twitter_production/profile_images/33598712/wt_normal.png
name     暴君
url http://twitter.com/VoQn/statuses/371888452
description 授業終わった。やっとご飯食べられる
nick    VoQn
-
image   http://s3.amazonaws.com/twitter_production/profile_images/24072332/negipo_normal.png
name    Yoshiteru Negishi
url http://twitter.com/negipo/statuses/371887582
description はてなperlグループに入った
nick    negipo
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34078402/cat_888_normal.gif
name    (c)yabkoji
url http://twitter.com/yabkoji/statuses/371887022
description 激スイマー襲来中。
nick    yabkoji
-
image   http://s3.amazonaws.com/twitter_production/profile_images/22560082/nobita_normal.jpg
name    kimidora
url http://twitter.com/kimidora/statuses/371886072
description ねむいねむいねむいねむいねむいねむいねむいねむいねむいねむい。。。。。。。。。。ぐぅ *Tw*
nick    kimidora
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371885732
description うぅ、ムラサメで部長とカブった。
nick    smokeymonkey
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371885022
description はてなグループって排他制御あるのかしら。
nick    smokeymonkey
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34076402/crestock-221345-1024x768_normal.jpg
name    ハラヘ(´・ω・`)
url http://twitter.com/mobcov/statuses/371884382
description TextMateアップデートキターーーーー!!
nick    mobcov
-
image   http://s3.amazonaws.com/twitter_production/profile_images/32257122/basara_normal.jpg
name    japo
url http://twitter.com/japo/statuses/371884052
description ハマーン様万歳だろJK
nick    japo
-
image   http://s3.amazonaws.com/twitter_production/profile_images/19383642/ore_normal.jpg
name    Sugano Yoshihisa(E)
url http://twitter.com/koshian/statuses/371883862
description 「あのPerlモジュールなんてったっけ?」とぐぐったら、自分のはてなブックマークがひっかかって見事に発見
nick    koshian
-
image   http://s3.amazonaws.com/twitter_production/profile_images/22022432/plagger_logo_purple_normal.png
name    plagger.org
url http://twitter.com/plagger/statuses/371883772
description Safariの『Webクリップ』を試してみる。 - sta la sta
nick    plagger
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371882682
description うわ、 GFF でペーネロペー出てるのか。超欲しい。
nick    smokeymonkey
-
image   http://s3.amazonaws.com/twitter_production/profile_images/33078022/icon_normal.gif
name    Miki@7500
url http://twitter.com/7500/statuses/371881092
description 午後の部にとりかかりまっしゅ!夕方までしゃいなら?ノシ
nick    7500
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371879692
description 初代からν→ F91 まで + アレックスと 08 小隊とデンドロビウムは把握してる。その後はさっぱりだな。
nick    smokeymonkey
-
image   http://s3.amazonaws.com/twitter_production/profile_images/15059862/al3x_normal.jpg
name    Alex Payne
url http://twitter.com/al3x/statuses/371879502
description Oh, how I wanted to be rid of MacPorts. Curse you, Image/Rmagick.
nick    al3x
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34297902/kamon_normal.png
name    wrongbee
url http://twitter.com/wrongbee/statuses/371878872
description ペーネロペーの画像検索結果
nick    wrongbee
-
image   http://s3.amazonaws.com/twitter_production/profile_images/33078022/icon_normal.gif
name    Miki@7500
url http://twitter.com/7500/statuses/371878362
description @
nick    7500
-
image   http://s3.amazonaws.com/twitter_production/profile_images/15927972/otsune_hanaji_purple_normal.jpg
name    ??uns?o ??n??s??
url http://twitter.com/otsune/statuses/371877452
description Pukkaのすばらしい所はdel.icio.usのアカウント切り替えが一瞬でプルダウンメニューで選べる所だ。bookmarkletなんかでソーシャルブックマークしてたらログアウト・ログインの時間分だけ人生を無駄にする lang:ja
nick    otsune
-
image   http://s3.amazonaws.com/twitter_production/profile_images/34253552/twfoot_normal.gif
name    highness
url http://twitter.com/smokeymonkey/statuses/371876992
description 閃光のハサウェイはガンダムサイドストーリーでは一番好きだ。
nick    smokeymonkey
-
-
Luaでもやれない事はない!

追記
result処理、相対/絶対URL展開処理を加えて、CodeReposに入れておきました。
Posted at 03:01 in ソフトウェア::lang::lua | WriteBacks (0)
Tagged as: lua, webscraper
Bookmarks: このエントリーのtweets add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip | add to buzzurl add to buzzurl | add to fc2bookmark add to fc2bookmark | add to Yahoo Bookmark add to Yahoo Bookmark | add to Pookmark add to Pookmark

2007/12/05

はてな
iTunesのライブラリ情報XMLファイルをアップロードする事で自分に合ったアーティスト情報を教えてくれるサービス、「veena!」の検索ボックスを使って、指定のアーティストに関連する
  • YouTube動画
  • Yahooオークション情報
をWeb::Scraperでスクレイピングしてみようと思います。
ソースはそれ程難しくもなく
#!/usr/bin/perl

use strict;
use warnings;

use URI;
use URI::Escape qw(uri_escape_utf8 uri_unescape);
use Web::Scraper;
use YAML::Syck;

my $artist = shift || 'Ozzy Ozbourne';
my $uri = URI->new('http://www.veena.jp/srch_artist.php?artist_name='
    . uri_escape_utf8($artist));

my $youtube_list = scraper {
    process '//table[@class="info_tbl"]/tr/td',
        'video[]' => scraper {
            process '//a[1]', url => sub {
                my $url = shift->attr('href');
                $url =~ s/^.*\?url=(.*)$/$1/;
                uri_unescape($url);
            };
            process '//a[2]', title => 'TEXT';
            process '//img', image => '@src';
        };
    result 'video';
};

my $auction_list = scraper {
    process '//table[@class="info_tbl"]/tr/td',
        'auction[]' => scraper {
            process '//a[1]', url => '@href';
            process '//a[2]', title => 'TEXT';
            process '//img', image => '@src';
        };
    result 'auction';
};

my $artist_list = scraper {
    process '//a[contains(@href, "artist.php")]',
        'artists[]' => scraper {
            process 'a', id => sub {
                my $url = shift->attr('href');
                $url =~ s/^.*id=(.*)$/$1/;
                $url;
            };
            process 'a', 'youtube' => sub {
                my $url = shift->attr('href');
                $url =~ s/artist\.php/http:\/\/veena.jp\/list_youtube\.php/;
                my $list = $youtube_list->scrape(URI->new($url));
                \@$list;
            };
            process 'a', 'auction' => sub {
                my $url = shift->attr('href');
                $url =~ s/artist\.php/list_auction\.php/;
                my $list = $auction_list->scrape(URI->new_abs($url, $uri));
                \@$list;
            };
            process 'a', name => 'TEXT';
        }
};
my $result = $artist_list->scrape($uri);
warn Dump $result;
って感じ。YouTube動画情報一覧とYahooオークション情報はアーティスト情報にぶら下がる形で出力したかったので検索結果一覧用のscraperとその結果を取得するscraperを親子関係にしてあります。
結構一覧としてはキレイに出力されているかと思います。
---
artists: 
  - 
    auction: 
      - 
        image: !!perl/scalar:URI::http http://ac.c.yimg.jp/7/1026/1783/000/img305.auctions.yahoo.co.jp/users/6/4/6/7/rosiertrueblue-thumb-119657918759294.jpg
        title: Ozzy Osbourne
        url: !!perl/scalar:URI::http http://page.auctions.yahoo.co.jp/jp/auction/108393777
      - 
        image: !!perl/scalar:URI::http http://ac.c.yimg.jp/7/1022/1783/000/img245.auctions.yahoo.co.jp/users/6/4/6/7/rosiertrueblue-thumb-119657997018368.jpg
        title: Ozzy Osbourne
        url: !!perl/scalar:URI::http http://page11.auctions.yahoo.co.jp/jp/auction/n61267094
      - 
        image: !!perl/scalar:URI::http http://a1017.lm.a.yimg.com/7/1017/1783/000/img257.auctions.yahoo.co.jp/users/8/2/8/3/kokita74-thumb-119486785113507.jpg
        title: Ozzy Osbourne
        url: !!perl/scalar:URI::http http://page8.auctions.yahoo.co.jp/jp/auction/h52088580
   ...
    id: 216546
    name: Randy Rhoads (Ozzy Ozbourne)
    youtube:
      -
        image: !!perl/scalar:URI::http http://img.youtube.com/vi/MEUbYkLe_wo/default.jpg
        title: Ozzy Ozbourne's top 10 songs
        url: http://www.youtube.com/watch?v=MEUbYkLe_wo
      - 
        image: !!perl/scalar:URI::http http://img.youtube.com/vi/GLtjWi4qkIY/default.jpg
        title: Goodbye to Romance - Ozzy/Randy Rhoads (solo)
        url: http://www.youtube.com/watch?v=GLtjWi4qkIY
      - 
        image: !!perl/scalar:URI::http http://img.youtube.com/vi/AQqbNHhBWcI/default.jpg
        title: iron man
        url: http://www.youtube.com/watch?v=AQqbNHhBWcI
   ...
Ozzy OzbourneのキーワードでRandy Rhoadsも引っかかってウハウハです。
で、このYAMLをどうするか...
use LWP::UserAgent;
my $ua = LWP::UserAgent->new;
$ua->timeout(10);
$ua->agent('Mozilla');
for my $artist (@{$result->{artists}}) {
  for my $video (@{$artist->{youtube}}) {
    my $url = $video->{url};
    my $req = HTTP::Request->new(GET => $url);
    $req->header('Accept-Encoding', 'identity');
    my $res = $ua->request($req);
    if ($res->is_error) {
      if ((my $verify_url = $res->request->uri) =~ /\/verify_age\?/) {
        my $verify_req = HTTP::Request->new(POST => $verify_url, {action_confirm => 'Confirm'});
        $res = $ua->request($verify_req);
        $res = $ua->request($req) if $res->is_success;
      }
    }
    if ($res->content =~ /video_id=([^&]+)&l=\d+&t=([^&]+)/gms) {
      my $flv = "http://youtube.com/get_video?video_id=$1&t=$2";
      print "Downloading $flv\n";
      my $download_req = HTTP::Request->new(GET => $flv);
      $download_req->referer($url);
      my $res = $ua->request($download_req);
      if ($res->is_success) {
        open FH, ">$2.flv";
        binmode FH;
        print FH $res->content;
        close FH;
        print "Downloaded $2.flv\n";
      } else {
        print "Failed to download $2.flv\n";
      }
    } else {
      print "Not found flv file\n";
    }
  }
}
やっぱこうなりますわね...

mattn the crazy train scraper!

2007/11/28

はてな
タレントスケジュールなんてサイトを見つけたので、さっそくスクレイピング。
ドキュメントに同じid属性が複数あるという、なんともダイナミックなHTMLにもめげず作り上げたのが以下
#!/usr/bin/perl

use encoding 'utf-8';
use strict;
use warnings;
use Encode qw(from_to);
use URI;
use URI::Escape qw(uri_escape_utf8);
use Web::Scraper;
use YAML;

if ($^O eq 'MSWin32') {
    binmode(STDERR, ':encoding(shift_jis)');
    Encode::from_to($ARGV[0], 'cp932', 'utf-8');
}
my $talent = shift || '小島よしお';

my $talent_schedule = scraper {
    process '//div[@class="find_bl"]/following-sibling::*[1]//td', day => 'TEXT';
    process '//div[@class="find_bl"]/following-sibling::*[1]//td/div',
        'schedule[]' => scraper {
            process 'div', media => sub { my $m = $_->attr('class'); $m =~ s/^icon_//g; $m };
            process '/div/a', url => '@href';
            process '/div/a', title => 'TEXT';
            process '/div/node()[1]', timespan => sub {
                my $s = $_->string_value;
                $s =~ s/ //;
                $s =~ s/(^|[^\d])(\d):(\d\d)/0$2:$3/g;
                my @span = split(/[^\d:]/, $s);
                \@span;
            };
        };
    result qw/day schedule/;
};
my $uri = URI->new('http://talent-schedule.jp/'.uri_escape_utf8($talent));
my $oppappi_schedule = $talent_schedule->scrape($uri);
warn Dump $oppappi_schedule;
ちょっと日付まわりで苦労してますが...

小島よしおって、結構番組出てますねぇ。

でもそんなの関係ry)

2007/10/31

はてな
Web::Scraper使うときに、scraperコマンドを使って頑張る人もいれば、FirebugのDOMツリーで「XPathをコピー」とやっている人もいるでしょう。
前者の場合、端末でスクロールアウトするHTMLを見ながらXPathをこさえて間違ったらズラズラズラ…と画面が流れて行ってしまいます。後者の場合は、CLASSやID属性を使わないXPathが出来上がってしまいます。
映画に出てくるHackerの如く一発でXPathを決められればそれは素晴らしい事だと思いますが、いかんせん幾度か失敗しますよね。
で、何回もXPathを確かめられるツールが欲しいなと思い、perl-GTK2で作ってみました。

画面はこんな感じ
WebScraperHelper1
引数に「http://b.hatena.ne.jp/」を付けて起動したらこんな感じ
WebScraperHelper2
URLを変更して「Get」をクリックすれば再読み込みします。
そして、はてなブックマークトップページの「注目の動画」部分にある画像一覧を取得する為に
//a[text()="注目の動画"]/../../..//img
というXPathを書いて「Update」をクリックすれば
WebScraperHelper3
こんな感じのHTMLが出来上がります。
あとはこれをWeb::Scraperのprocess部分に貼っつけるだけ。

ちなみにXPathでの属性値参照も出来ますので、はてなブックマークトップページで
//meta[@http-equiv="Content-Type"]/@content
というXPathを書けば
content="text/html; charset=UTF-8"
という結果が返ります。
起動にはCPANからGtk2モジュールをインストールする必要があります。HTMLのパース方法やノードの取得方法等は大体Web::Scraperと合わせていますので、Web::Scraperが動く環境にGtk2をインストールすれば動くかと思います。
また画面はLinux上で起動した物ですが、UN*Xらしい事は一切やってませんのでWindowsでも動作するかと思います。
ダウンロード:WebScraperHelper.pl

もう少し機能を足そうかと思いましたが、今日はもうギブアップ。寝ます。