Featured image of post 第4回-基底クラスで処理の流れを定義しよう - PerlとMooでWebスクレイパーを作ってみよう

第4回-基底クラスで処理の流れを定義しよう - PerlとMooでWebスクレイパーを作ってみよう

WebScraperクラスを作成し、scrape()メソッドで処理の流れを定義。各ステップを抽象メソッドとして宣言し、Template Methodパターンの基礎を構築します。継承ベースの設計手法を実践的に学びます。

@nqounetです。

前回は、スクレイピング処理には「取得→抽出→保存」という共通の骨格があることを発見しました。今回は、Mooを使ってこの骨格を「基底クラス」として定義していきます。

このシリーズについて

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

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

処理の骨格をクラスで表現する

前回整理した「処理の骨格」をおさらいしましょう。

	flowchart LR
    A[1. 取得<br/>fetch_html] --> B[2. 抽出<br/>extract_data] --> C[3. 保存<br/>save_data]

この流れを、Mooを使ってクラスとして表現していきます。

WebScraper基底クラスを作成する

まず、スクレイパーの「親クラス(基底クラス)」となる WebScraper クラスを作成します。

 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
package WebScraper;
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, Mojo::UserAgent(Mojoliciousに含まれる)

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

# スクレイピング対象のURL
has url => (
    is       => 'ro',
    required => 1,
);

# メインの処理メソッド:処理の「骨格」を定義
sub scrape ($self) {
    # 1. HTMLを取得
    my $dom = $self->_fetch_html();
    
    # 2. データを抽出(サブクラスで実装)
    my @data = $self->extract_data($dom);
    
    # 3. データを保存(サブクラスで実装)
    $self->save_data(@data);
    
    return @data;
}

# HTMLを取得する(共通処理)
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) {
    die "save_data must be implemented by subclass";
}

1;

このクラスには3つの重要なポイントがあります。

ポイント1: 処理の「骨格」を定義するscrapeメソッド

1
2
3
4
5
6
sub scrape ($self) {
    my $dom = $self->_fetch_html();      # 1. 取得
    my @data = $self->extract_data($dom); # 2. 抽出
    $self->save_data(@data);              # 3. 保存
    return @data;
}

このメソッドは、「取得→抽出→保存」という処理の順番(骨格)を定義しています。この順番は全てのスクレイパーで共通です。

ポイント2: 共通処理を実装する_fetch_htmlメソッド

1
2
3
4
5
sub _fetch_html ($self) {
    my $ua = Mojo::UserAgent->new;
    my $res = $ua->get($self->url)->result;
    # ...
}

HTMLの取得処理は全てのスクレイパーで同じなので、基底クラスで実装しています。メソッド名の先頭にアンダースコア(_)をつけているのは、「このメソッドはクラス内部でのみ使用する」という慣習的な表記です。

ポイント3: サブクラスに実装を強制する「抽象メソッド」

1
2
3
4
5
6
7
sub extract_data ($self, $dom) {
    die "extract_data must be implemented by subclass";
}

sub save_data ($self, @data) {
    die "save_data must be implemented by subclass";
}

これらのメソッドは、基底クラスでは「実装がない」状態です。呼び出すとdieでエラーになります。

これは「抽象メソッド」と呼ばれるパターンで、「このメソッドはサブクラスで必ず実装してね」という意味を持ちます。Perlには他の言語のようなabstractキーワードはありませんが、dieを使うことで同じ効果を実現しています。

なぜこの設計が良いのか

この設計には以下のメリットがあります。

  1. 処理の順番を保証: scrapeメソッドで処理の順番が固定されているので、「取得→抽出→保存」の順番が必ず守られる
  2. 共通処理を1箇所に: _fetch_htmlの実装は基底クラスに1つだけなので、修正時も1箇所だけで済む
  3. 拡張ポイントが明確: extract_datasave_dataをオーバーライドすれば、新しいスクレイパーを簡単に追加できる
  4. コードの意図が明確: クラスの構造を見れば、どこが共通でどこがカスタマイズポイントかがすぐわかる

クラスの構造を図解する

この設計を図にすると、以下のようになります。

	classDiagram
    class WebScraper {
        +url
        +scrape()
        -_fetch_html()
        +extract_data(dom)*
        +save_data(data)*
    }
    note for WebScraper "* = 抽象メソッド<br/>(サブクラスで実装)"

*がついたメソッドは「抽象メソッド」であり、サブクラスでオーバーライドする必要があります。

この基底クラスを使ってみる(失敗例)

まだサブクラスを作っていないので、WebScraperを直接使おうとするとどうなるでしょうか?

1
2
3
4
5
6
7
#!/usr/bin/env perl
use v5.36;
use lib '.';
use WebScraper;

my $scraper = WebScraper->new(url => 'file://./sample_news.html');
$scraper->scrape();  # ここでエラー!

実行結果:

1
extract_data must be implemented by subclass at WebScraper.pm line XX.

extract_dataが実装されていないので、dieでエラーになります。これは意図した動作です。基底クラスはあくまで「骨格」を定義するものであり、具体的な処理はサブクラスで実装する必要があります。

今回のまとめ

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

  • 処理の「骨格」を基底クラスのscrapeメソッドで定義する
  • 共通処理(_fetch_html)は基底クラスで実装する
  • サブクラスで実装すべきメソッドは「抽象メソッド」としてdieで宣言する

次回予告

次回は、このWebScraper基底クラスを継承して、NewsScraperWeatherScraperというサブクラスを作成します。extendsを使って継承し、extract_datasave_dataメソッドをオーバーライドして、それぞれのサイトに特化した処理を実装していきます。

「Mooで覚えるオブジェクト指向プログラミング」で学んだ継承とオーバーライドの知識が、ここで活きてきますよ!

お楽しみに!

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