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 connect_info);
sub new
{
my $class = shift;
return $class->next::method(
connect_info => [
'dbi:SQLite:dbname=data/crawler.db',
undef,
undef,
{ RaiseError => 1, AutoCommit => 1 }
]
);
}
ここではmk_accessors()
でdbhとconnect_infoというアクセッサーを作成し、 new()
でconnect_infoを親クラスのnew()に渡します。
後にsetup()というメソッドが呼び出されますので、このタイミングでDB接続等の 初期化を行います。
sub setup
{
my ($self, $c) = @_;
my $dbh = DBI->connect(@{ $self->connect_info })
or die "Could not connect to database";
$self->dbh($dbh)
}
次にエンジンにリクエストを渡すdispatch()
メソッドを作成します。
sub dispatch
{
my ($self, $c) = @_;
if (! $self->has_requests) {
return;
}
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()
メソッドが呼び出されます。
先ほどのProviderとは別にSimpleCrawler/Handler.pmを作成します。
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'
provider
とhandler
それぞれで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への接続を拒否するように します。
RobotRulesはデフォルトでDB_File.pmを使用しますのでファイルを書き出す事に なります。デフォルトでこのファイルは/tmp/robots.db が指定されますが、 /tmpに書き込み権限がないなどの理由でファイルを作成できないとエラーが 起こります。その場合はファイル名を書き込みができる場所に明示的に指定して ください:
robot_rules:
storage:
module: DB_File
config:
filename: '/path/to/storage.db'
これらを書いた設定ファイルをdata/crawler.ymlに保存します。
実行
いよいよ実行です。
env PERL5LIB=lib gungho -c data/crawler.yml
今回はlibディレクトリーにいくつかのモジュールを置いてあるのでPERL5LIBを 設定してgunghoスクリプトを実行します。全てうまくいっていればこれでもう クローラーが動き始めるはずです!