2023/12/16

Web::Scraper の実装がカッコいいぞという話

この記事は、"Perl Advent Calendar 2023" 16日目の記事です。


Web::Scraper というPerlモジュールがあります。名前の通り、Webページをスクレイピングするためのモジュールです。2007年にリリースされたモジュールです。このモジュールで育った人も多いのではないかと思います。

このモジュールの scrape メソッドは以下のような引数を受け付けます。

$res = $scraper->scrape(URI->new($uri));
$res = $scraper->scrape($html_content);
$res = $scraper->scrape(\$html_content);
$res = $scraper->scrape($http_response);
$res = $scraper->scrape($html_element);

URIオブジェクト、HTML文字列かそのリファレンス、HTTP::Responseオブジェクト、HTML::Elementオブジェクト、という風に、いろいろな引数を受け入れてくれて便利です。

scrape の中身はどうなっているのでしょうか。

sub scrape {
    my $self  = shift;
    my($stuff, $current) = @_;

    my($html, $tree);

    if (blessed($stuff) && $stuff->isa('URI')) {
        my $ua  = $self->user_agent;
        my $res = $ua->get($stuff);
        return $self->scrape($res, $stuff->as_string);
    } elsif (blessed($stuff) && $stuff->isa('HTTP::Response')) {
        if ($stuff->is_success) {
            $html = $stuff->decoded_content;
        } else {
            croak "GET " . $stuff->request->uri . " failed: ", $stuff->status_line;
        }
        $current ||= $stuff->request->uri;
    } elsif (blessed($stuff) && $stuff->isa('HTML::Element')) {
        $tree = $stuff->clone;
    } elsif (ref($stuff) && ref($stuff) eq 'SCALAR') {
        $html = $$stuff;
    } else {
        $html = $stuff;
    }

    $tree ||= $self->build_tree($html);

URIオブジェクトがきたら HTTP::Response を取得して scrape に再帰します。

HTTP::ResponseがきたらHTMLを取得します。

HTML::Elementがきたらオブジェクトをクローンしておきます。

そして、HTTP::Response由来のHTMLか、引数で渡されたHTMLから、HTML::TreeBuilder をベースとする HTML::TreeBuilder::XPath で HTMLをパースします。

最終的に $tree に収れんしていく感じがしびれますね。特に再帰してるところがヤバい(語彙)

Web::Scraperscrape メソッドの多様な引数を受けるところだけでなく、コードリファレンスを受け付ける scraper メソッド本体の実装も Perlらしさ全開のカッコよさなので是非読んでみてください。

サイト内検索