Laravel9でWebスクレイピングしてみた

## はじめに
今回､聞いたことあるけど実際触ったことのなかったWebスクレイピングをやってみようということで､LaravelでWebスクレイピングを試してみることにしました｡

内容は何でも良かったのですが､
Pubmedという論文サイトがありまして､それをWebスクレイピングすることにしました｡

### Webスクレイピングとは
> Webスクレイピングとは、Webサイトから特定の情報を自動的に抽出するコンピュータソフトウェア技術のことです。Webスクレイピングを使えば、インターネット上に存在するWebサイトやデータベースを探り、大量のデータの中から特定のデータのみを抽出させることができます。
> Webサイト上のデータを移す際は、手作業でコピー＆ペーストを行う方も多いでしょう。しかし、Webスクレイピングの活用によって、面倒な手作業を自動化できるため、作業時間の短縮や転記ミス防止が可能です。 抽出したデータはExcelやCSVファイルなどにエクスポートできるため、データを活用した分析が行えるようになります。

とのことです｡
実際にWebに転がっている大量のデータから､必要なデータのみを取ってくることができ､とても便利な機能だそうですね｡

https://www.octoparse.jp/blog/web-scraping

### Laravelを使ってWebスクレイピングをやってみる
Webスクレイピングといえば､Pythonが最も有名な気もしますが､
普段Laravelを使って仕事をしているので､勉強も兼ねてLaravelでやってみようと思います｡

少し調べてみると､どうやらLaravel9からやり方が少々変わったようです｡

もともと､Laravel8でGoutteというライブラリを使ってスクレイピングしていたそうですが､
2022年10月時点でGithubの更新がストップしているようで､
今回は更新が頻繁にされている､`Guzzle`を使って､実装していきます｡

https://readouble.com/laravel/9.x/ja/http-client.html

### Laravel Guzzleとは
GuzzleはPHPで書かれたHTTPクライアントライブラリで、簡単にHTTPリクエストを送信し、レスポンスを受け取ることができます。

Laravelフレームワークと組み合わせて使用することで、外部APIとの通信や、
RESTfulサービスの消費など、ウェブベースのアプリケーション開発における多くのHTTP関連タスクを簡単に行うことができます。


### ステップ 1: Guzzle のインストール
まず、Guzzle HTTP クライアントを Laravel プロジェクトにインストールします。

```terminal
composer require guzzlehttp/guzzle
```

Laravel9以降の場合､GuzzleがLaravelのパッケージに含まれているため、
実行すると更新される形になります｡

### ステップ 2: 必要なパッケージのインストール
このコードは、symfony/dom-crawler パッケージを使用しています。このパッケージをインストールしていない場合は、以下のコマンドでインストールしてください。
スクレイピングした結果をdom形式で扱いやすくするためです。

Dockerコンテナ内で実行をします｡

```terminal
composer require symfony/dom-crawler
```

これで準備ができました｡
あとは､好みにトリミングしてデータをスクレイピングしていきます｡

### Controller を作っていく

まず全文を添付します｡
```php
class PubmedScrapingController extends Controller
{
    public function scrape(Request $request)
    {
        $client = new Client();
        $base_url = 'https://pubmed.ncbi.nlm.nih.gov/';

        // ユーザー入力を取得
        $keywords = $request->input('keywords', '');
        $filter = $request->input('filter', '');
        $max_pages = 10;

        $articles = [];

        if (!empty($keywords)) {
            for ($page = 1; $page <= $max_pages; $page++) {
                $url = "{$base_url}?term={$keywords}&filter={$filter}&page={$page}";
                $response = $client->request('GET', $url);
                $html = $response->getBody()->getContents();

                $crawler = new Crawler($html);
                $page_results = $crawler->filter('.docsum-content')->each(function (Crawler $node) {

                    return [
                        'title' => $node->filter('.docsum-title')->text(),
                        'authors' => $node->filter('.docsum-authors')->text(),
                        'pmid' => $node->filter('.docsum-pmid')->text(),
                        'snippet' => $node->filter('.full-view-snippet')->text(),
                        'journalCitation' => $node->filter('.docsum-journal-citation.full-journal-citation')->text()
                    ];
                });

                $articles = array_merge($articles, $page_results);
                sleep(1); // API制限やサーバーへの負荷を考慮して1秒待機
            }
        }

        // ページネーションの設定
        $articles = collect($articles);
        $currentPage = $request->input('page', 1);
        $perPage = 10;
        $currentItems = $articles->slice(($currentPage - 1) * $perPage, $perPage)->all();
        $paginatedArticles = new LengthAwarePaginator($currentItems, count($articles), $perPage, $currentPage, [
            'path' => $request->url(),
            'query' => $request->query(),
        ]);

        return view('pubmedScraping.index', compact('paginatedArticles', 'keywords'));
    }
```

1つずつ解説をしていきます｡

#### 1, Guzzleクライアントの初期化
`$client = new Client();` この行で、Guzzle HTTPクライアントのインスタンスを作成しています。これを使って、後でHTTPリクエストを送信します。

```php
$client = new Client();
```

#### 2, 基本URLの設定
今回は､Pubmedという論文サイトから引っ張ってきたいので､あらかじめ指定してあげます｡
```php
$base_url = 'https://pubmed.ncbi.nlm.nih.gov/';
```

#### 3, ユーザー入力の取得
Pubmed内には記事数が物凄くあるので､検索機能を同時に導入していきますので､
ここで検索に関するコードを書いていきます｡

```php
// ユーザー入力を取得
$keywords = $request->input('keywords', '');
$filter = $request->input('filter', '');
$max_pages = 10;
```

`$keywords = $request->input('keywords', '');` と `$filter = $request->input('filter', '');` で、ユーザーがフォームに入力した検索キーワードとフィルター条件を取得しています。
第二引数の空文字列は、その入力が存在しない場合のデフォルト値です。

あまりスクレイピングしすぎると､重くなりすぎ読み込みに時間がかかってきてしまうため､
10ページをMAXとしております｡

今回はキーワードチェックを入れており､ユーザーが検索キーワードを入力した場合のみスクレイピング処理が実行されるようにしています｡キーワードが空でない場合にのみ､内部の処理が実行されます｡

```
if (!empty($keywords)) { ... }
```

#### 4, スクレイピングのループ
`for ($page = 1; $page <= $max_pages; $page++) { ... } `
このループは、最大10ページまで（$max_pages = 10;で設定）PubMedの検索結果をスクレイピングします。
重くなりすぎるため､10ページまでとしています｡

#### 5, 構築されたURLでのリクエスト
今回Pubmedからデータをスクレイピングしてくるため､サイトURLを`$base_url`に代入しています｡
`$url = "{$base_url}?term={$keywords}&filter={$filter}&page={$page}";` で、検索キーワード、フィルター条件、現在のページ番号を含むURLを構築し、そのURLに対してGuzzleクライアントを使ってGETリクエストを送信しています。

#### 6, HTTPリクエストの送信
`$response = $client->request('GET', $url);`
Guzzle HTTPクライアントを使用して、構築したURLにGETリクエストを送信し、レスポンスを受け取っています。

#### 6, HTMLコンテンツの取得
`$html = $response->getBody()->getContents();` で、リクエストのレスポンスボディからHTMLコンテンツを取得しています。

#### 7, DOMクローラーの使用
ここで序盤に導入したDOMクローラーを使っていきます｡
`$crawler = new Crawler($html);` この行で、取得したHTMLを解析するためにSymfonyのDOMクローラーコンポーネントを使用しています。

#### 8, データの抽出
`$page_results = $crawler->filter('.docsum-content')->each(function (Crawler $node) { ... });` ここで、各論文情報が含まれるDOM要素（.docsum-contentクラスを持つ要素）をループし、その中からタイトル、著者、PMID、スニペット、ジャーナル情報を抽出しています。

一覧画面なので､内容が軽くわかれば良い程度なので､ここでは最低限抽出するようにしています｡

#### 9, 記事データの結合
`$articles = array_merge($articles, $page_results);` 

で、現在のページの結果を以前のページの結果と結合しています。

#### 10, サーバー負荷の考慮
sleep(1); この行で、次のリクエストを送信する前に1秒待機しています。これは、サーバーに過度な負荷をかけないようにするためです。

#### 11, ページネーションの設定
LaravelのLengthAwarePaginatorを使用して、取得した記事データをページネートしています。これにより、ユーザーは結果をページ単位で閲覧できるようになります。

Laravelにおいて、データベースを使用せずにスクレイピングしたデータでページネーションを実装する場合、`LengthAwarePaginator`を使ってカスタムページネーションを行う必要があります｡

#### 12, ビューへのデータの渡し
`return view('pubmedScraping.index', compact('paginatedArticles', 'keywords')); `この行で、ページネーションされた記事データと検索キーワードをビューに渡して、結果をユーザーに表示しています。

### 実際のページ
上記ControllerでデータをViewに送り､Bladeでページ実装しています｡
Bladeは今回は載せませんが､Bootstrapで作っています｡

[検索前]
検索前は0件表示｡
![スクリーンショット 2024-02-14 0.46.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589286/61ae6499-a41a-ce43-db1a-abb9e77f95b4.png)

[検索後]
検索すると､論文がスクレイピングされる｡
![スクリーンショット 2024-02-14 18.13.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/589286/7ca173e7-6193-1878-d13a-3dfef75f3e77.png)

## まとめ
今回Laravelを使ってWebスクレイピングを実装しました｡
WebスクレイピングといえばPythonというイメージが強かったですが､Laravelでもライブラリを使えば簡単に実装することができたので､使いやすいと感じたのが所感です｡

ただスクレイピングにも色々規則があるようで､
サイトによってはスクレイピング自体が禁止となっているケースもあるようなので
そのあたりしっかりと調査しながら進めていきたいと思います｡

以上｡