PhantomJS + Selenium::Remote::Driver でスクレイピングをこころみる
PhantomJS といえば、WebKit を headless でうごかせて便利なやつですが、PhantomJS 1.8 から Ghost Driver がくみこまれるようになりました。
わかる人むけにかくと「JSONWire Protocol をサポートする httpd が phantomjs にくみこまれた」ということです。
GhostDriver は WebDriver Wire Protocol の1実装です。で、そのクライアントライブラリとして Selenium::Remote::Driver が CPAN にあがっていますから、これをつかって簡単に phantomjs とやりとりができます。
Selenium::Remote::Driver という名前のとおり、Selenium もつかえますんで、selenium をつかって、対応しているブラウザでのテストもできるのがいいかんじです。
というわけでためしてみましょう。
準備
brew install phantomjs
cpanm Selenium::Remote::Driver
でOK。
自分でコンパイルするのはすごい時間かかるので、バイナリを利用するのが吉です。
参考:
phantomjs、P4のcentosで2時間近くがんばってbuildしている横でmacにbrew installしたらバイナリが3秒で入ったでござる — hidek (@hidek) January 9, 2013
phantomjs ghost driverをたちあげる
phantomjs ghost driver をたちあげるには、以下のようにするかんじです。
phantomjs --webdriver=9999
ここで 9999 はポート番号です。
ここではシェルからたちあげることを想定していますが、実際は Test::TCP とかでたちあげちゃう方が楽だとおもいます。
Selenium::Remote::Driver でアクセスしてみる
use Selenium::Remote::Driver;
my $driver = Selenium::Remote::Driver->new(
    remote_server_addr => '127.0.0.1',
    port => 9999,
);
my $res = $driver->get('http://mixi.jp');
say $driver->get_title();
こんなかんじで OK です。簡単ですね。
簡単なんだけど、問題があります。なんか文字化けするんです。
????????롦?ͥåȥ????? ?????ӥ? [mixi(?ߥ?????)]
とか。。
なんでかな、っておもいます。
Selenium::Remote::Driver がわるいんじゃないかとうたがってみる。
ちょっと Selenium::Remote::Driver があやしいんじゃないかとうたがってしまいますね。
なんかあやしいので、シンプルな実装をつくってためしてみましょう。
#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use 5.010000;
use autodie;
use LWP::UserAgent;
use JSON::XS;
{
    package JSONWire::Client;
    use LWP::UserAgent;
    our $VERSION = '1.0.0';
    use Moo;
    use JSON;
    has port => (
        is => 'ro',
        required => 1,
    );
    has json => (
        is => 'ro',
        default => sub {
            JSON->new
        },
    );
    has host => (
        is => 'ro',
        required => 1,
    );
    has agent => (
        is => 'ro',
        default => sub {
            LWP::UserAgent->new(
                agent => __PACKAGE__ . "/" . $VERSION,
            );
        },
    );
    sub create_session {
        my $self = shift;
        my $res = $self->agent->post(
            "http://$self->{host}:$self->{port}/session",
            Content => $self->json->encode( { desiredCapabilities => {} } )
        );
        if ($res->code ne 303) {
            JSONWire::Client::Exception::HTTP->throw($res);
        }
        my $base = $res->header('Location') // die "Missing location";
        return JSONWire::Client::Session->new(
            base => $base,
            agent => $self->agent,
            json => $self->json,
        );
    }
    package JSONWire::Client::Session;
    use Moo;
    has base => (
        is => 'ro',
        required => 1,
    );
    has agent => (
        is => 'ro',
        required => 1,
    );
    has json => (
        is => 'ro',
        required => 1,
    );
    has last_response => (
        is => 'rw',
    );
    sub get {
        my ($self, $path) = @_;
        $path =~ s!^/+!!;
        my $res = $self->agent->get($self->base . "/" . $path);
        $self->last_response($res);
        unless ($res->is_success) {
            JSONWire::Client::Exception::HTTP->throw(
                $res
            )
        }
        return $self->json->decode($res->content);
    }
    sub post {
        my ($self, $path, $data) = @_;
        $path =~ s!^/+!!;
        my $res = $self->agent->post($self->base . "/" . $path, Content => $self->json->encode($data));
        $self->last_response($res);
        unless ($res->is_success) {
            JSONWire::Client::Exception::HTTP->throw(
                $res
            )
        }
        return $self->json->decode($res->content);
    }
    package JSONWire::Client::Exception::HTTP;
    use Moo;
    has response => (
        is => 'ro',
    );
    use overload q{""} => \&stringify;
    sub throw {
        my ($class, $response) = @_;
        die $class->new(response => $response);
    }
    sub stringify {
        my $self = shift;
        $self->response->status_line;
    }
}
my $driver = JSONWire::Client->new(
    host => '127.0.0.1',
    port => 9999,
);
my $session = $driver->create_session;
$session->post('/url', {url => 'http://mixi.jp'});
my $data = $session->get('/title');
say $data->{value};
で、実行結果は。。
¥½¡¼¥·¥ã¥ë¡¦¥Í¥Ã¥È¥ï¡¼¥¥ó¥° ¥µ¡¼¥Ó¥¹ [mixi(¥ß¥¯¥·¥£)]
なんてこった!!
Selenium::Remote::Driver もだめぽなことにきづく
+NOTE: Currently, I don't use Perl for my day job & support for this module is falling behind. If you want to take over 
 	 2	
+      maintenance of this module, please contact me.
とかかいてある!
https://github.com/aivaturi/Selenium-Remote-Driver/commit/82ac94841a7fe1a8d8c0ff59ab5b061c47d20eda
だれかメンテナンスしないのかな?
JSONWire::Client を github にあげてみた
https://github.com/tokuhirom/JSONWire-Client
需要ありそうなら CPAN にあげます。
結論
Selenium::Remote::Driver は日本語のscrapingができない。
あと、Wight 的なのよりこの路線が今後主流かなーとおもったり。
ref. http://blog.64p.org/entry/2012/10/13/144648
あと、phantomjs 1.8 では Ghost Driver では日本語がとおりません! だれか bug report とかしてあげてください。
【追記】
どうも OSX 用のバイナリが腐ってるっぽくて、ひできさん方式で、自前コンパイルすれば問題ないので、みなさん3時間かけてコンパイルしましょう。
Published: 2013-01-09(Wed) 04:48