The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Gungho::Manual::Tutorial.ja - Gunghoチュートリアル

初めてのGungho

クローラーというものは実際に様々な因子が関係するので簡単なクローラーを作る、 というのはなかなか難しいのですが、ここでは以下の環境があると仮定して Gunghoでクローラーを実装するまでを追って行きたいと思います。

なお、ただ単純にリスト等からページを取得するだけであればexamples/ ディレクトリ 内のsimple等の例をお手本とすれば良いでしょう。こちらではもう少し手間は かかりますが、実際にクローラーを作成する時に近い形での例を示します。

まずこれから取得したいURLのデータベースが存在すると仮定します。今回のクローラー では、このデータベースに存在する全てのURLを一回ずつ取得し、その結果得られる HTMLページからリンクを抜き出し、さらに他のデータベースに格納する、というところ までを作成します。

必要なもの

  Gungho 0.09005
  sqlite 3
  DBI

はじめに

まずはGunghoの基本を理解するためにGungho::Manual::Basicsを読んでください。

それが終わったらまず作業用のディレクトリを作成します。とり急ぎSimpleCrawler という名前で作業をすることにします。以下のディレクトリを作成してください:

  SimpleCrawler
  SimpleCrawler/lib
  SimpleCrawler/lib/SimpleCrawler
  SimpleCrawler/data

これから先は全てSimpleCrawlerディレクトリの中から作業するものとします。 "lib"という表記があったら実際にはSimpleCrawler/libの事を指しています。

Provider

まずはURLを格納するためのデータベースをsqliteで作成します。

  shell> sqlite3 data/crawler.db   
  SQLite version 3.4.0
  Enter ".help" for instructions
  sqlite> create table urls (url text primary key, fetched_on integer);

これで"url"というカラムと"fetched_on"というカラムが存在するテーブルが 作成されました。

このテーブルに好きなURLを挿入してください。クローラーを向ける先ですので なるべく迷惑のかからなそうなURLを使用してください。ここでは架空の simplecrawler.comというサイトを指定してみます。

  sqlite> insert into urls(url) values('http://simplecrawler.com');
  sqlite> .quit

以上でとりあえずデータベースは完了です。

今度はこのデータベースからURLを取り出すProviderを作成します。 lib/SimpleCrawler/Provider.pmをエディタで開いてください。

まずは名前空間の定義と、継承の定義を行います。

  package SimpleCrawler::Provider;
  use strict;
  use warnings;
  use base qw(Gungho::Provider::Simple);

次はコンストラクタを作成します。今回は特に設定を変更する予定はありませんので、 データベースへの接続情報もそのままnew()に書き込んでしまいます。実際には このような設定は設定ファイルから読み込むようにすると良いでしょう。

  __PACKAGE__->mk_accessors($_) for qw(dbh);

  sub new
  {
    my $class = shift;
    my $dbh = DBI->connect(
      'dbi:SQLite:dbname=data/crawler.db',
      undef,
      undef,
      { RaiseError => 1, AutoCommit => 1 }
    );
    $class->next::method(dbh => $dbh, has_requests => 1);
  }

ここではmk_accessors()でdbhというアクセッサーを作成し、new()でそれを 親クラスのnew()に渡します。

次にエンジンにリクエストを渡すdispatch()メソッドを作成します。

  sub dispatch
  {
    my ($self, $c) = @_;

    if (! $self->has_requests) {
      return;
    }

    $self->next::method($c);

    my $dbh = $self->dbh;
    my $sth = $dbh->prepare("SELECT url FROM urls");
    $sth->execute();
    my $url;
    $sth->bind_columns(\$url);
    while ($sth->fetchrow_arrayref) {
      my $r = Gungho::Request->new(GET => $url);
      $c->send_request($r);
    }
  }

ここでは1回しかデータベースからの読み込みを行いたくないので、まず has_requests()フラグですでにリクエスト済みかどうかテストします。

その後データベースからURLを読み込みます。それぞれのURLに対してGungho::Request を作成し、それをsend_request()に投げるとその後でHTTP通信が始まり、取得が 始まります。

dispatch()はエンジンモジュールにより定期的に呼ばれますので、一斉にURLを リクエストするのではなく、少しずつリクエストしたい場合はこのメソッドの中で コントロールしてください。

Handler

Handlerは取得されてきたレスポンスを処理する場所です。URLが取得されてくる たびにHandler内で定義されているhandle_response()メソッドが呼び出されます。

  package SimpleCrawler::Handler;
  use strict;
  use warnings;
  use base qw(Gungho::Handler);
  use HTML::LinkExtor;

今回のHandlerは返ってきたレスポンス内のHTMLを解析し、リンクを抜き出します。 HTML::LinkExtorはそのために先に読み込んでおきます。

handle_response()はまず返ってきたレスポンスが正常かどうか、また Content-Typeはtext/htmlかどうかを確認します。

  sub handle_response
  {
    my($self, $c, $request, $response) = @_;

    return unless $response->is_success;
    return unless $response->content_type eq 'text/html';

次にせっかくなのでレスポンスの返ってきた時間を記録します。

    my $dbh = DBI->connect(
      'dbi:SQLite:dbname=data/crawler.db',
      undef,
      undef,
      { RaiseError => 1, AutoCommit => 1 }
    );

    my $sth;
    $sth = $dbh->prepare_cached("UPDATE urls SET fetched_on = ?");
    eval {
      $sth->execute( time() );
    };
    if ($@) {
      print $@, "\n";
    }

ここからリンクの取得をします。HTML::LinkExtorはコードリファレンスを要求する のでそれを用意し、パースします。

    my @links;
    my $code = sub {
      my($tag, %attrs) = @_;
      return unless $tag eq 'a';

      push @links, $attrs{href};
    };

    my $p = HTML::LinkExtor->new($code);
    $p->parse( $response->content );
    $p->eof;

そうして最後に今抜き出してきたリンクをデータベースに挿入します。

    my $sth = $dbh->prepare("INSERT INTO urls (url) VALUES (?)");
    foreach my $link (@links) {
      eval {
        $sth->execute($link);
      };
      if ($@) {
        print $@, "\n";
      }
    }
  }

本来ならもっとエラー処理を行ったり、 リンクを解決して無効なリンク等がないかを 確認したりする必要がありますが、今回はこちらは割愛しました。

設定ファイル

ようやくProviderとHandlerができました。今度このProviderとHandlerを使用して gunghoを走らせるために設定ファイルを作成します。Gunghoではほとんどの設定を YAML等の設定ファイルから行えるように設計されています。

まずはUserAgentを設定します。この値は必ずつけるようにしてください。

  user_agent: 'SimpleCrawler Demo 0.01'

次にProviderとHandlerの設定を行います。今回は特に設定が必要な形には しなかったので、どのモジュールを使うのかだけ指定します:

  provider:
    module: '+SimpleCrawler::Provider'
  handler:
    module: '+SimpleCrawler::Handler'

providerhandlerそれぞれでmodule項目を指定します。この際モジュール名の先頭に'+'をつけるのを忘れないでください。'+'をつけないとGungho::Provider等の 文字列が先頭に追加され評価されますのでご注意ください。

さらにエンジンを指定します。エンジンはPOEエンジンを使います。POEエンジンは 様々なオプションが存在しますので細やかな設定が可能なのですが、今回はデフォルト 設定で運用しますのでモジュール名のみ指定します。

  engine:
    module: POE

あとはログモジュールを指定します。今回はdata/crawler.logというファイルと、 標準エラーにwarning以上のログを送るようにします。

  log:
    module: Dispatch
    config:
      logs:
        - module: Screen
          min_level: warning
          name: stderr
          stderr: 1
        - module: File
          min_level: warning
          name: logfile
          filename: data/crawler.log

基本設定はここまでで、これだけでもGunghoは稼働しますがここまでの行程だけでは サイト側が取得してほしくないデータまで取得しに行ってしまう行儀のよくない クローラーになってしまいます。これを改善するためにGunghoにデフォルトで ついてくるコンポーネントを読み込みます。

  components:
    - RobotRules
    - RobotsMETA
    - Throttle::Simple
    - BlockPrivateIP

これらのコンポーネントは一般的なクローラーが必要とする主な機能のうちの 数個です。RobotRulesはrobots.txtを適用し、リクエストが取得できるかどうかを 調節します。 RobotsMETAはページ内のMETAタグ内のディレクティブを解析します。 Throttle::Simpleは最大リクエスト数の絞り込み(スロットリング)を行い、 BlockPrivateIPはプライベートアドレスに解決されるURLへの接続を拒否するように します。

これらを書いた設定ファイルをdata/crawler.ymlに保存します。

実行

いよいよ実行です。

  env PERL5LIB=lib gungho -c data/crawler.yml

今回はlibディレクトリーにいくつかのモジュールを置いてあるのでPERL5LIBを 設定してgunghoスクリプトを実行します。全てうまくいっていればこれでもう クローラーが動き始めるはずです!