work.log

元エンジニアの備忘録的ブログ

Coroを作ったクローラの試作品で内部リンクを辿る

投稿:2014-04-21 18:34  更新:

Coro のメモ書き。

これまでに Coro で色々と試してきましたが、そろそろクロール機能をつけてサイトを辿らせてみようと思います。

ただし、試作品だしエラー処理とか負荷とか色々考慮していない部分があるので、下記条件で自分で管理するサーバに対してのみやってみます。

  • クロールする時間は 60 秒
  • 内部リンクのみを辿る
  • クロールしたページからタイトルを抜き出す

これで良いかはわかりませんが、上記の条件を元に書いたのがコレ。

#!/usr/bin/perl

use strict;
use warnings;
use Coro;
use Coro::Timer;
use Coro::Semaphore;
use Coro::LWP;
use LWP::UserAgent;
use URI;
use HTML::TreeBuilder;
use Digest::MD5 qw/md5_hex/;

    my $lock  = {};
    my $queue = {};
    my $crawl = {};
    my @url   = ();
    my @done  = ();
    my @fail  = ();

    ## fetcher の timeout (秒)
    my $timeout = 15;

    ## クロールし続ける時間 (秒)
    my $crawl_timeout = 60;
    my $main = Coro::Signal->new;

    my $url = 'http://example.jp/';
    my $dom = URI->new($url)->host;

    worker( fetcher => \&fetcher, 5 );
    worker( parser => \&parser, 1 );
    worker( manager => \&manager, 1 );

    ## fetcher に 1 つだけ url を投げる
    queue("fetch")->put($url);

    ## スレッド切り替え
    $main->wait;

    ## 取得できたタイトルを表示
    foreach (@done) {
        print "$_\n";
    }

sub done {
    push(@done, $_[0]);
}

sub fail {
    push(@fail, $_[0]);
}

sub queue {
    my $name = shift;
    $queue->{$name} ||= Coro::Channel->new;
}

sub worker {

    my ($name, $code, $num) = @_;

    for (1 .. $num) {

        my $desc = $name . "_" . rand_md5();

        async {

            Coro::current->desc($desc);

            while ( 1 ) {
                $code->();
            }

        };

    }

}

sub manager {

    Coro::Timer::sleep 1;

    my $done = scalar(@done);
    my $fail = scalar(@fail);

    print "Timeout: $crawl_timeout Done: $done Fail: $fail\n";

    my @coro = Coro::State::list;

    foreach my $coro (@coro) {

        next if (!$coro->desc);
        next if ($coro->desc !~ /fetcher/);
        next if (! defined($coro->{alive}));

        if ($coro->{alive} <= 0 && ! $coro->{idle}) {

            print "timeout: " . $coro->desc . "\n";
            $coro->cancel();
            fail("timeout: " . $coro->desc);

            $lock->{$coro->{host}}->up;
            worker( fetcher => \&fetcher, 1 );

        } else {

            print $coro->desc . " alive = $coro->{alive} $coro->{status} $coro->{url}\n";

            if (! $coro->{idle}) { $coro->{alive} -= 1; }

        }
    }

    ## クロール用のタイマーを減算
    $crawl_timeout--;

    if ( $crawl_timeout <= 0) {

        print "completed: Done => $done Fail => $fail\n";
        Coro::Timer::sleep 5;

        foreach my $coro (@coro) {
            next if (!$coro->desc);
            next if ($coro->desc !~ /parser/);
            $coro->cancel();
        }

        $main->send;

    }
}

sub fetcher  {

    my $url = queue("fetch")->get;
    my $cur = Coro::current;
    my $host = URI->new($url)->host;
    my $sem = $lock->{$host} ||= Coro::Semaphore->new(2);
    my $lwp = '';
    my $res = 0;

    $cur->{host} = $host;
    $cur->{url} = $url;
    $cur->{status} = '[lock]';

    $lock->{$host}->down;

    $cur->{status} = '[run]'; 
    $cur->{idle} = 0;
    $cur->{alive} = $timeout;

    $lwp = LWP::UserAgent->new();
    $res = $lwp->get($url);

    if ($res->is_success) {
        queue("parser")->put($res->content);
    } else {
        fail("fail:$url");
    }

    $cur->{status} = '[wait]'; 
    $cur->{idle} = 1;

    Coro::Timer::sleep 3;

    undef $cur->{host};
    undef $cur->{url};
    $cur->{status} = '[idle]'; 
    $cur->{alive} = $timeout;

    $lock->{$host}->up;

}

sub parser {

    my $content = queue("parser")->get;

    my $tree = HTML::TreeBuilder->new;
    $tree->parse($content);
    $tree->eof();

    ## スクレイピングしてデータを抜き出し
    if ($tree->find('title')) {

        foreach ($tree->find('title')) {

            done($_->as_text);
            last;

        }

    } else {

        fail("notitle");

    }

    ## スクレイピングしたページのリンクを抜き出し
    ## fetcher に送りつけて内部リンクを辿る
    if ($tree->find('a')) {

        foreach ($tree->find('a')) {

            next if ( !$_->attr('href') );
            next if ( $_->attr('href') !~ /$dom/ );
            next if ( $_->attr('href') !~ /^http/ );
            my $url = $_->attr('href');

            ## url はハッシュに格納して重複しないようにする
            if (! $crawl->{$url}) {
                $crawl->{$url} = 1;
                queue("fetch")->put($url);
            }

        }

    }

}

sub rand_md5 {

    my $max = 9999;
    my $min = 1000;

    my $rand = time() . int( rand($max - $min + 1) ) + $min;
    my $md5 = md5_hex($rand);

    return($md5);

}

これを走らせるとこんな感じに。

Timeout: 25 Done: 22 Fail: 0
fetcher_afd8a9f322b11c08823642a489a1846a alive = 15 [wait] https://worklog.be/archives/2995
fetcher_fef5b40e43245f64e36dbff8c3936c46 alive = 15 [wait] https://worklog.be/archives/2996

Timeout: 24 Done: 23 Fail: 0
fetcher_afd8a9f322b11c08823642a489a1846a alive = 15 [run] https://worklog.be/page/2
fetcher_fef5b40e43245f64e36dbff8c3936c46 alive = 15 [wait] https://worklog.be/archives/2993
・
・ ? 制限時間いっぱいまでクロール ?
・
completed: Done => 37 Fail => 0

? 以下取得できたタイトル ?

意外とあっさりできちゃいました。

今回はアンカーテキストのリンクのみを見て内部リンクを辿っていますが、link タグ何かもみたりした方が良さげ。

暫くは遊びながら改良してみようと思います。

おすすめのVPSサーバ

  • OSが選べる
  • VPS同士でLANが組める
  • 複数台構成向き

このブログで使っています。

  • 転送量が多いサービスに
  • 借りてるのは3年間一度もdown無し!

よく見られている記事

  • 本日
  • 週間
  • 月間