Featured image of post 第6回-デフォルトの保存処理を用意しよう - PerlとMooでWebスクレイパーを作ってみよう

第6回-デフォルトの保存処理を用意しよう - PerlとMooでWebスクレイパーを作ってみよう

save_data()メソッドにデフォルト実装(ファイル保存)を用意。必要に応じて各サブクラスでカスタマイズできるHook Methodsの活用方法を学びます。柔軟な設計の実現を体験し、拡張性の高いコードの書き方を習得します。

@nqounetです。

前回は、NewsScraperWeatherScraperというサブクラスを作成し、それぞれでextract_datasave_dataをオーバーライドしました。今回は、「デフォルトの保存処理」を基底クラスに用意し、サブクラスでは必要なときだけカスタマイズする方法を学びます。

このシリーズについて

このシリーズは「Mooで覚えるオブジェクト指向プログラミング」シリーズを読了した方を対象に、実践的なWebスクレイパーを作りながらオブジェクト指向設計を深く学ぶシリーズです。

シリーズ全体の目次は以下をご覧ください。

抽象メソッドとフックメソッドの違い

前回までに、2種類のメソッドを見てきました。

  1. 抽象メソッド: 基底クラスではdieになり、サブクラスで必ず実装が必要
  2. 共通メソッド: 基底クラスで完全に実装され、サブクラスでは触らない

今回学ぶのは、この中間にある「フックメソッド(Hook Methods)」です。

種類基底クラスでの状態サブクラスでの対応
共通メソッド完全に実装オーバーライド不要
フックメソッドデフォルト実装必要ならオーバーライド
抽象メソッドdie(未実装)必ずオーバーライド

フックメソッドは「デフォルト実装があるけど、必要ならカスタマイズできる」というものです。

save_dataをフックメソッドにする

現在のWebScraper基底クラスでは、save_dataは抽象メソッド(die)でした。これをデフォルト実装のあるフックメソッドに変更しましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package WebScraper;
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, Mojo::UserAgent(Mojoliciousに含まれる)

use Moo;
use experimental qw(signatures);
use Mojo::UserAgent;

has url => (
    is       => 'ro',
    required => 1,
);

# 保存先ファイル名(デフォルトは結果を画面出力)
has output_file => (
    is      => 'ro',
    default => sub { undef },
);

sub scrape ($self) {
    my $dom = $self->_fetch_html();
    my @data = $self->extract_data($dom);
    $self->save_data(@data);
    return @data;
}

sub _fetch_html ($self) {
    my $ua = Mojo::UserAgent->new;
    my $res = $ua->get($self->url)->result;
    
    if ($res->is_success) {
        return $res->dom;
    }
    die "取得失敗: " . $res->message;
}

# 抽象メソッド: サブクラスで必ず実装
sub extract_data ($self, $dom) {
    die "extract_data must be implemented by subclass";
}

# フックメソッド: デフォルト実装を用意
sub save_data ($self, @data) {
    if ($self->output_file) {
        # ファイルに保存
        open my $fh, '>', $self->output_file
            or die "Cannot open file: $!";
        for my $item (@data) {
            if (ref $item eq 'HASH') {
                # ハッシュの場合はJSON風に出力
                print $fh join(", ", map { "$_: $item->{$_}" } keys %$item) . "\n";
            } else {
                print $fh "$item\n";
            }
        }
        close $fh;
        say "結果を " . $self->output_file . " に保存しました";
    } else {
        # 画面に出力
        for my $item (@data) {
            if (ref $item eq 'HASH') {
                say join(", ", map { "$_: $item->{$_}" } keys %$item);
            } else {
                say $item;
            }
        }
    }
}

1;

変更点は以下の通りです。

  1. output_file属性を追加(デフォルトはundef
  2. save_dataメソッドにデフォルト実装を追加
    • output_fileが指定されていればファイルに保存
    • 指定されていなければ画面に出力

サブクラスの変更

これにより、サブクラスではsave_dataのオーバーライドが任意になります。

シンプルなNewsScraper

デフォルトの保存処理をそのまま使う場合、save_dataは不要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package NewsScraper;
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, WebScraper

use Moo;
use experimental qw(signatures);

extends 'WebScraper';

# extract_dataだけ実装すればOK!
sub extract_data ($self, $dom) {
    my @headlines;
    for my $headline ($dom->find('h2.headline')->each) {
        push @headlines, $headline->text;
    }
    return @headlines;
}

# save_dataはオーバーライドせずデフォルトを使用

1;

カスタマイズしたWeatherScraper

天気予報スクレイパーでは、デフォルトと異なる形式で出力したいので、save_dataをオーバーライドします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package WeatherScraper;
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, WebScraper

use Moo;
use experimental qw(signatures);

extends 'WebScraper';

sub extract_data ($self, $dom) {
    my @forecasts;
    for my $row ($dom->find('tr.day-forecast')->each) {
        my $date = $row->at('td.date')->text;
        my $weather = $row->at('td.weather')->text;
        my $temp = $row->at('td.temp')->text;
        push @forecasts, {
            date    => $date,
            weather => $weather,
            temp    => $temp,
        };
    }
    return @forecasts;
}

# カスタムの保存処理(読みやすい形式で出力)
sub save_data ($self, @data) {
    say "┌─────────────────────────────────┐";
    say "│       週間天気予報              │";
    say "├─────────┬────────┬──────────────┤";
    say "│ 日付    │ 天気   │ 気温         │";
    say "├─────────┼────────┼──────────────┤";
    for my $f (@data) {
        printf "│ %-7s │ %-6s │ %-12s │\n",
            $f->{date}, $f->{weather}, $f->{temp};
    }
    say "└─────────┴────────┴──────────────┘";
}

1;

実行してみる

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/env perl
use v5.36;
use lib '.';
use NewsScraper;
use WeatherScraper;

# ニュースを画面出力
say "--- ニュース(画面出力)---";
my $news1 = NewsScraper->new(url => 'file://./sample_news.html');
$news1->scrape();

say "";

# ニュースをファイル保存
say "--- ニュース(ファイル保存)---";
my $news2 = NewsScraper->new(
    url         => 'file://./sample_news.html',
    output_file => 'news.txt'
);
$news2->scrape();

say "";

# 天気(カスタム形式で画面出力)
say "--- 天気予報 ---";
my $weather = WeatherScraper->new(url => 'file://./sample_weather.html');
$weather->scrape();

実行結果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
--- ニュース(画面出力)---
Perl 5.42.0が2025年7月にリリース
PerlがTIOBEインデックスでトップ10に復帰
Perl Toolchain Summit 2025が開催

--- ニュース(ファイル保存)---
結果を news.txt に保存しました

--- 天気予報 ---
┌─────────────────────────────────┐
│       週間天気予報              │
├─────────┬────────┬──────────────┤
│ 日付    │ 天気   │ 気温         │
├─────────┼────────┼──────────────┤
│ 1月20日  │ 晴れ   │ 12℃/3℃      │
│ 1月21日  │ 曇り   │ 10℃/2℃      │
│ 1月22日  │ 雨     │ 8℃/5℃       │
└─────────┴────────┴──────────────┘

同じNewsScraperでも、output_fileを指定するかどうかで動作が変わります。また、WeatherScraperではカスタムの罫線付きテーブル形式で出力されています。

フックメソッドの利点

フックメソッドを使うことで、以下の利点が得られます。

  1. サブクラスの実装がシンプルに: 必要なもの(extract_data)だけ実装すればよい
  2. カスタマイズの自由度: 必要ならオーバーライドしてカスタマイズできる
  3. デフォルト動作の一貫性: 多くのスクレイパーで同じ保存処理を使える
  4. 属性でのカスタマイズ: output_fileのような属性で動作を切り替えられる

今回のまとめ

今回は以下のことを学びました。

  • フックメソッド: デフォルト実装があり、必要なときだけオーバーライドするメソッド
  • save_dataをフックメソッドにすることで、サブクラスの実装がシンプルになった
  • 属性(output_file)でデフォルト動作をカスタマイズできる

次回予告

デフォルトの保存処理を用意できたので、次は「データ検証」の機能を追加しましょう。抽出したデータが期待通りかどうかをチェックするvalidate_dataフックメソッドを追加します。

お楽しみに!

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。