Featured image of post 第9回-完成!Webスクレイパー - PerlとMooでWebスクレイパーを作ってみよう

第9回-完成!Webスクレイパー - PerlとMooでWebスクレイパーを作ってみよう

ニュース・天気・商品情報を並行して取得できるWebスクレイパーシステムが完成!各クラスの役割と全体の構造を整理し、拡張性の高い設計を確認します。Template Methodパターンの威力を実感し、オブジェクト指向設計の集大成を体験します。

@nqounetです。

前回は、ProductScraperを追加し、既存コードを修正せずに新機能を追加できることを確認しました。今回は、ここまで作成してきた全てのクラスを統合して、完成したWebスクレイパーシステムの全体像を確認しましょう。

このシリーズについて

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

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

完成したクラス構成

これまでに作成したクラスの全体像を図にすると、以下のようになります。

	classDiagram
    class WebScraper {
        +url
        +output_file
        +scrape()
        -_fetch_html()
        +extract_data(dom)*
        +validate_data(data)
        +save_data(data)
    }
    class NewsScraper {
        +extract_data(dom)
        +validate_data(data)
    }
    class WeatherScraper {
        +extract_data(dom)
        +save_data(data)
    }
    class ProductScraper {
        +extract_data(dom)
        +validate_data(data)
        +save_data(data)
    }
    
    WebScraper <|-- NewsScraper : extends
    WebScraper <|-- WeatherScraper : extends
    WebScraper <|-- ProductScraper : extends
    
    note for WebScraper "処理の骨格を定義\n* = 抽象メソッド"

各クラスの役割

各クラスの役割を整理しましょう。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package WebScraper;
# 言語: perl
# バージョン: 5.36以上
# 依存: Moo, Mojo::UserAgent

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

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

has output_file => (
    is      => 'ro',
    default => sub { undef },
);

# 処理の骨格(Template Method)
sub scrape ($self) {
    my $dom = $self->_fetch_html();       # 1. 取得
    my @data = $self->extract_data($dom); # 2. 抽出
    $self->validate_data(@data);          # 3. 検証
    $self->save_data(@data);              # 4. 保存
    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 validate_data ($self, @data) {
    return 1;  # デフォルトは何もしない
}

# フックメソッド(デフォルト実装あり)
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') {
                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;

役割: 処理の骨格(取得→抽出→検証→保存)を定義し、共通処理を実装します。

NewsScraper, WeatherScraper, ProductScraper(サブクラス)

各サブクラスは、サイト固有の抽出ロジックを実装します。

サブクラスextract_datavalidate_datasave_data
NewsScraper✓ 実装✓ 実装デフォルト使用
WeatherScraper✓ 実装デフォルト使用✓ 実装
ProductScraper✓ 実装✓ 実装✓ 実装

統合スクリプト

全てのスクレイパーを一括で実行するスクリプトを作成しましょう。

 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
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: NewsScraper, WeatherScraper, ProductScraper

use v5.36;
use lib '.';
use NewsScraper;
use WeatherScraper;
use ProductScraper;

say "=" x 50;
say "  Webスクレイパー統合システム";
say "=" x 50;
say "";

# スクレイパーの設定
my @scrapers = (
    {
        class => 'NewsScraper',
        url   => 'file://./sample_news.html',
        name  => 'ニュース',
    },
    {
        class => 'WeatherScraper',
        url   => 'file://./sample_weather.html',
        name  => '天気予報',
    },
    {
        class => 'ProductScraper',
        url   => 'file://./sample_products.html',
        name  => '商品情報',
    },
);

# 各スクレイパーを実行
for my $config (@scrapers) {
    say "【$config->{name}】";
    
    my $class = $config->{class};
    my $scraper = $class->new(url => $config->{url});
    
    eval {
        $scraper->scrape();
    };
    if ($@) {
        warn "エラーが発生しました: $@";
    }
    
    say "";
}

say "=" x 50;
say "  スクレイピング完了!";
say "=" x 50;

実行結果:

 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
==================================================
  Webスクレイパー統合システム
==================================================

【ニュース】
検証OK: 3 件のニュースを取得しました
Perl 5.40がリリースされました
Mojoliciousが10周年を迎える
CPANモジュール数が5万を突破

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

【商品情報】
=== 在庫あり商品一覧 ===
  プログラミングPerl 第4版 ... ¥5,280
  初めてのPerl 第7版 ... ¥3,520
(2 件の商品が在庫あり)

==================================================
  スクレイピング完了!
==================================================

3種類のスクレイパーが、それぞれ独自のフォーマットでデータを出力しています。

設計の振り返り

ここで、シリーズを通して構築した設計を振り返ってみましょう。

最初のアプローチ(コピペ)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
news_scraper.pl ─┬─ HTTP取得コード
                 ├─ ニュース抽出コード
                 └─ 保存コード

weather_scraper.pl ─┬─ HTTP取得コード(コピペ)
                    ├─ 天気抽出コード
                    └─ 保存コード(コピペ)

product_scraper.pl ─┬─ HTTP取得コード(コピペ)
                    ├─ 商品抽出コード
                    └─ 保存コード(コピペ)

問題点: 同じコードが複数箇所に分散。修正時に全ファイルを変更する必要がある。

最終的なアプローチ(継承)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
WebScraper.pm ─┬─ HTTP取得コード(共通)
               ├─ 処理の骨格(scrape)
               ├─ デフォルト保存処理
               └─ 抽象メソッド定義

NewsScraper.pm ─── ニュース抽出コード

WeatherScraper.pm ─┬─ 天気抽出コード
                   └─ カスタム保存処理

ProductScraper.pm ─┬─ 商品抽出コード
                   ├─ 検証処理
                   └─ カスタム保存処理

利点: 共通コードは1箇所。新しいスクレイパーはサブクラス追加のみ。

処理フローの比較

最初のアプローチ

	flowchart TD
    subgraph スクリプト1
        A1[取得] --> B1[抽出] --> C1[保存]
    end
	flowchart TD
    subgraph スクリプト2
        A2[取得] --> B2[抽出] --> C2[保存]
    end
	flowchart TD
    subgraph スクリプト3
        A3[取得] --> B3[抽出] --> C3[保存]
    end

最終的なアプローチ

	flowchart TD
    subgraph WebScraper
        A[取得] --> B[抽出] --> C[検証] --> D[保存]
    end
    
    subgraph サブクラス
        E1[NewsScraper]
        E2[WeatherScraper]
        E3[ProductScraper]
    end
    
    B -.-> E1
    B -.-> E2
    B -.-> E3

処理の流れは基底クラスで一元管理し、具体的な処理だけをサブクラスに委譲しています。

拡張性の検証

この設計は、以下のような変更に簡単に対応できます。

変更内容対応方法
新しいサイトを追加新しいサブクラスを作成
全スクレイパーにログ機能追加WebScraperを修正(1箇所のみ)
特定スクレイパーの出力形式変更そのサブクラスのsave_dataを修正
HTTP取得時のタイムアウト設定WebScraperの_fetch_htmlを修正(1箇所のみ)

今回のまとめ

今回は以下のことを確認しました。

  • 全クラスの役割と構造の整理
  • 統合スクリプトで全スクレイパーを一括実行
  • 最初のアプローチと最終的なアプローチの比較
  • 拡張性の高い設計の価値

次回予告

最終回では、私たちがこのシリーズで作り上げてきたものの正体を明かします。実は、この設計は「Template Methodパターン」という有名なデザインパターンだったのです!

パターンの正式な定義、Strategyパターンとの違い、そして「Hollywood Principle(制御の反転)」について解説します。

お楽しみに!

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