tokuhirom's Blog

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 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時間かけてコンパイルしましょう。