@nqounetです。
前回は、NewsScraperとWeatherScraperというサブクラスを作成し、それぞれでextract_dataとsave_dataをオーバーライドしました。今回は、「デフォルトの保存処理」を基底クラスに用意し、サブクラスでは必要なときだけカスタマイズする方法を学びます。
このシリーズについて
このシリーズは「Mooで覚えるオブジェクト指向プログラミング」シリーズを読了した方を対象に、実践的なWebスクレイパーを作りながらオブジェクト指向設計を深く学ぶシリーズです。
シリーズ全体の目次は以下をご覧ください。
抽象メソッドとフックメソッドの違い
前回までに、2種類のメソッドを見てきました。
- 抽象メソッド: 基底クラスでは
dieになり、サブクラスで必ず実装が必要 - 共通メソッド: 基底クラスで完全に実装され、サブクラスでは触らない
今回学ぶのは、この中間にある「フックメソッド(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;
|
変更点は以下の通りです。
output_file属性を追加(デフォルトはundef)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ではカスタムの罫線付きテーブル形式で出力されています。
フックメソッドの利点
フックメソッドを使うことで、以下の利点が得られます。
- サブクラスの実装がシンプルに: 必要なもの(
extract_data)だけ実装すればよい - カスタマイズの自由度: 必要ならオーバーライドしてカスタマイズできる
- デフォルト動作の一貫性: 多くのスクレイパーで同じ保存処理を使える
- 属性でのカスタマイズ:
output_fileのような属性で動作を切り替えられる
今回のまとめ
今回は以下のことを学びました。
- フックメソッド: デフォルト実装があり、必要なときだけオーバーライドするメソッド
save_dataをフックメソッドにすることで、サブクラスの実装がシンプルになった- 属性(
output_file)でデフォルト動作をカスタマイズできる
次回予告
デフォルトの保存処理を用意できたので、次は「データ検証」の機能を追加しましょう。抽出したデータが期待通りかどうかをチェックするvalidate_dataフックメソッドを追加します。
お楽しみに!