前にも書きましたが、バイト先でWeb::Scraperを使ってます。
スクレイピング対象数が膨大な場合にちょっと困ったことが起きたので、今日はそれのmemo。
何が起こったかというと、スクレイピング先のサーバの調子が悪くて、500エラーなどを返したときに例外処理を書いていないとコケてしまうのだ。
例えば当ブログの最近の各エントリーページからのタイトル部分を引っ張ってくるとしよう。
#!/usr/local/bin/perl
use strict;
use Web::Scraper;
use URI;
use YAML;
binmode STDOUT => ":utf8";
my @uri = qw(https://hoge.sub.jp/blog/archives/2007/11/post_303.html
https://hoge.sub.jp/blog/archives/2007/11/post_3000.html #桁違い
https://hoge.sub.jp/blog/archives/2007/11/post_302.html);
my $scraper = scraper {
process "h3", "title" => "TEXT";
};
for (@uri){
my $result = $scraper->scrape(URI->new($_));
print Dump($result);
}
実行すると
> perl test.pl
---
title: てくのー
GET https://hoge.sub.jp/blog/archives/2007/11/post_3000.html failed: 404 Not Found at test.pl line 17
2番目のURLは現在では存在しないのでWeb::Scraperがcroak を呼び出す。この為、素のloopで回している上記のコードはループの途中でこけてしまう。
・対策方法その1
一番ラクなのはevalブロックで問題の箇所をくくって例外処理だろう。
evalブロック内でdieやcroak等でコケた場合、特殊変数$@(それにしても$@って、google先生キラーな名前だよなぁ)にエラー内容が入る。下記の場合はWeb::Scraper内のcroakに渡された文字列が入る。
#!/usr/local/bin/perl
use strict;
use Web::Scraper;
use URI;
use YAML;
binmode STDOUT => ":utf8";
my @uri = qw(https://hoge.sub.jp/blog/archives/2007/11/post_303.html
https://hoge.sub.jp/blog/archives/2007/11/post_3000.html
https://hoge.sub.jp/blog/archives/2007/11/post_302.html);
my $scraper = scraper {
process "h3", "title" => "TEXT";
};
for (@uri){
my $result;
eval {$result = $scraper->scrape(URI->new($_))};
#例外処理
if($@){
print "exception! $@\n"
} else {
print Dump($result);
}
}
実行してみる
> perl test2.pl
---
title: てくのー
exception! GET https://hoge.sub.jp/blog/archives/2007/11/post_3000.html failed: 404 Not Found at test2.pl line 18
---
title: 時間つぶしはニコニコ動画に限るよ。
ok。最後まで走った。
・対策方法2
plaggerプラグインのplugins/CustomFeed-Script/muhyojo.plを見て初めて知ったのだが、scrapeに渡すのってURIオブジェクトに限らないのね。
確かにWeb::Scraperのソースを見てみると
sub scrape {
my $self = shift;
my($stuff, $current) = @_;
my($html, $tree);
if (blessed($stuff) && $stuff->isa('URI')) {
require Encode;
require HTTP::Response::Encoding;
my $ua = $self->user_agent;
my $res = $ua->get($stuff);
if ($res->is_success) {
my @encoding = (
$res->encoding,
# could be multiple because HTTP response and META might be different
($res->header('Content-Type') =~ /charset=([\w\-]+)/g),
"latin-1",
);
my $encoding = first { defined $_ && Encode::find_encoding($_) } @encoding;
$html = Encode::decode($encoding, $res->content);
} else {
croak "GET $stuff failed: ", $res->status_line;
}
$current = $stuff->as_string;
} elsif (blessed($stuff) && $stuff->isa('HTML::Element')) {
$tree = $stuff->clone;
} elsif (ref($stuff) && ref($stuff) eq 'SCALAR') {
$html = $$stuff;
} else {
$html = $stuff;
}
…(略)
と、いった感じでURIオブジェクト以外も受け付けてくれるようだ。ただ、代入しているだけなのでその場合は自分でdecode等の処理をかけないとまずい。
んで、できたのが、↓
#!/usr/local/bin/perl
use strict;
use Web::Scraper;
use URI;
use YAML;
binmode STDOUT => ":utf8";
use Encode;
use DateTime;
my @uri = qw(https://hoge.sub.jp/blog/archives/2007/11/post_303.html
https://hoge.sub.jp/blog/archives/2007/11/post_3000.html
https://hoge.sub.jp/blog/archives/2007/11/post_302.html);
my $scraper = scraper {
process "h3", "title" => "TEXT";
};
for my $url (@uri){
my $response = $scraper->user_agent->get($url);
unless ($response->is_success) {
#ここで例外処理
print "GET $url failed: " . $response->status_line . ' : ' . DateTime->now . "\n";
next;
}
my ($encoding) = $response->header('Content-Type') =~ /charset=([\w\-]+)/g;
my $result = $scraper->scrape( Encode::decode($encoding, $response->content) );
print Dump($result);
}
encodingの取得はWeb::Scraperを参考にした。よくよく考えたら、日付情報加えただけなのであんまlogは変化してない...。これでは$@とカワランのでダメな例ですね(´Д`;)
とりあえず実行してみる。
> perl test3.pl
---
title: てくのー
GET https://hoge.sub.jp/blog/archives/2007/11/post_3000.html failed: 404 Not Found : 2007-11-19T14:24:06
---
title: 時間つぶしはニコニコ動画に限るよ。
個人的にはやっぱ$@の方が楽かなーという印象。URIオブジェクトではなく、GRTした内容をscrapeに投げる場合は色々とごにょごにょしなきゃいけないのが最大のネックかなー。