Featured image of post 第2回-天気予報サイトも取得したい!コピペの限界 - PerlとMooでWebスクレイパーを作ってみよう

第2回-天気予報サイトも取得したい!コピペの限界 - PerlとMooでWebスクレイパーを作ってみよう

天気予報サイトもスクレイピングしたくなった!前回のコードをコピペして対応するが、同じ処理が散在して保守性が低下。コード重複の問題に直面し、リファクタリングの必要性を認識します。

@nqounetです。

前回は、Mojo::UserAgentを使ってニュースサイトから見出しを取得するスクレイパーを作りました。今回は、別のサイトからも情報を取得したくなったときに何が起こるかを体験していきましょう。

このシリーズについて

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

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

天気予報も取得したい!

ニュースサイトのスクレイパーがうまく動いたので、調子に乗って天気予報サイトからも情報を取得したくなりました。天気予報サイトのHTMLは、ニュースサイトとは構造が異なります。

まず、sample_weather.htmlというサンプルHTMLを作成します。

 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
<!DOCTYPE html>
<html>
<head>
    <title>お天気サイト</title>
</head>
<body>
    <h1>週間天気予報</h1>
    <table class="forecast">
        <tr>
            <th>日付</th>
            <th>天気</th>
            <th>気温</th>
        </tr>
        <tr class="day-forecast">
            <td class="date">1月20日</td>
            <td class="weather">晴れ</td>
            <td class="temp">12℃/3℃</td>
        </tr>
        <tr class="day-forecast">
            <td class="date">1月21日</td>
            <td class="weather">曇り</td>
            <td class="temp">10℃/2℃</td>
        </tr>
        <tr class="day-forecast">
            <td class="date">1月22日</td>
            <td class="weather"></td>
            <td class="temp">8℃/5℃</td>
        </tr>
    </table>
</body>
</html>

ニュースサイトとは全く異なる構造ですね。こちらはテーブル形式で天気情報が格納されています。

コピペで対応してみる

さて、このHTMLからデータを抽出するスクレイパーを作りましょう。前回作ったニューススクレイパーのコードをコピペして、必要な部分だけ修正します。

 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
#!/usr/bin/env perl
# 言語: perl
# バージョン: 5.36以上
# 依存: Mojo::UserAgent(Mojoliciousに含まれる)

use v5.36;
use Mojo::UserAgent;

my $ua = Mojo::UserAgent->new;

# 天気予報サイトから取得
my $res = $ua->get('file://./sample_weather.html')->result;

if ($res->is_success) {
    my $dom = $res->dom;

    say "=== 週間天気予報 ===";
    # 天気予報の各行をループ
    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;
        say "$date: $weather ($temp)";
    }

    # 結果をファイルに保存
    open my $fh, '>', 'weather_result.txt' or die "Cannot open: $!";
    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;
        print $fh "$date: $weather ($temp)\n";
    }
    close $fh;
    say "結果を weather_result.txt に保存しました";
} else {
    say "取得失敗: " . $res->message;
}

これをweather_scraper.plとして保存し、実行します。

1
2
3
4
5
6
$ perl weather_scraper.pl
=== 週間天気予報 ===
1月20日: 晴れ (12℃/3℃)
1月21日: 曇り (10℃/2℃)
1月22日: 雨 (8℃/5℃)
結果を weather_result.txt に保存しました

動きました!問題なさそうに見えますね。

問題点に気づく

ここで、2つのスクリプト(news_scraper_v1.plweather_scraper.pl)を並べて見てみましょう。

1
2
3
4
5
6
7
8
9
# news_scraper_v1.pl
my $ua = Mojo::UserAgent->new;
my $res = $ua->get('file://./sample_news.html')->result;

if ($res->is_success) {
    my $dom = $res->dom;
    # ... ニュース特有の抽出処理 ...
    # 結果を表示
}
1
2
3
4
5
6
7
8
9
# weather_scraper.pl
my $ua = Mojo::UserAgent->new;
my $res = $ua->get('file://./sample_weather.html')->result;

if ($res->is_success) {
    my $dom = $res->dom;
    # ... 天気特有の抽出処理 ...
    # 結果をファイルに保存
}

気づきましたか?ほとんど同じ構造のコードが2つのスクリプトに重複しています。

両方のスクリプトで共通しているのは以下の部分です。

  • Mojo::UserAgent->new でHTTPクライアントを生成
  • $ua->get(URL)->result でHTTPリクエストを送信
  • $res->is_success でレスポンスの成功を確認
  • $res->dom でDOMオブジェクトを取得

一方、スクリプトごとに異なるのは以下の部分です。

  • 取得先のURL
  • DOMから抽出する要素(CSSセレクタ)
  • 結果の表示形式や保存方法

3つ目のスクレイパーを追加したくなったら?

もし、ECサイトの商品情報も取得したくなったらどうなるでしょうか?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/usr/bin/env perl
# product_scraper.pl

use v5.36;
use Mojo::UserAgent;

my $ua = Mojo::UserAgent->new;
my $res = $ua->get('file://./sample_products.html')->result;  # URLが違うだけ

if ($res->is_success) {  # ここは全く同じ
    my $dom = $res->dom;  # ここも同じ
    # ... 商品特有の抽出処理 ...  # ここだけ違う
    # 結果を保存  # これも似たような処理
}

また同じコードをコピペすることになります。

コピペの何が問題なのか?

「動けばいいじゃん」と思うかもしれませんが、コピペには以下の問題があります。

  1. 修正が大変: エラーハンドリングを改善したいとき、全てのスクリプトを修正する必要がある
  2. バグが伝播する: 1つのスクリプトのバグを直しても、コピー先には残ったまま
  3. 一貫性がなくなる: 時間が経つと各スクリプトが微妙に異なる進化を遂げ、保守が困難に
  4. テストが困難: 同じ処理なのに、スクリプトごとにテストが必要

これは「DRY原則(Don’t Repeat Yourself)」に違反している状態です。

現状のコードの構造を図解する

2つのスクリプトの現状を図にすると、以下のようになります。

	flowchart TD
    subgraph news_scraper.pl
        N1[UserAgent生成] --> N2[HTTPリクエスト]
        N2 --> N3[成功確認]
        N3 --> N4[DOM取得]
        N4 --> N5[ニュース抽出]
        N5 --> N6[結果表示]
    end
    style N1 fill:#ffcccc
    style N2 fill:#ffcccc
    style N3 fill:#ffcccc
    style N4 fill:#ffcccc
	flowchart TD
    subgraph weather_scraper.pl
        W1[UserAgent生成] --> W2[HTTPリクエスト]
        W2 --> W3[成功確認]
        W3 --> W4[DOM取得]
        W4 --> W5[天気抽出]
        W5 --> W6[結果保存]
    end

    style W1 fill:#ffcccc
    style W2 fill:#ffcccc
    style W3 fill:#ffcccc
    style W4 fill:#ffcccc

赤く塗られた部分が重複しているコードです。かなりの量が重複していることがわかりますね。

このままでは破綻する

スクレイパーが2つ、3つ…と増えていくと、重複コードも2倍、3倍…と増えていきます。ある日「全てのスクレイパーにタイムアウト設定を追加したい」となったとき、全てのファイルを開いて同じ修正を繰り返さなければなりません。

修正漏れがあればバグになります。修正内容が微妙に違えば、それが新たなバグを生みます。

次回予告

このコピペ地獄から抜け出す方法はないのでしょうか?次回は、2つのスクリプトの「共通部分」と「異なる部分」を明確に整理し、関数化によってコードの重複を減らしていきます。そして、さらにその先には、オブジェクト指向の力を借りた「より良い解決策」が待っています。

お楽しみに!

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