Laravel11/PHP8.3 環境で Chrome DevTool Protocol(CDP) 実装の Chrome PHP を使用して Web スクレイピングしてみた

# 概要
[前回](https://qiita.com/footit/items/d802512f98d40b82b93a)は Laravel Dusk の ChromeDriver を使用して Web スクレイピングしてみましたが、 ~~ChromeDriver ですと Chrome ブラウザと ChromeDriver のバージョンをある程度合わせておかないと正常に動作しないなど煩雑です。~~
~~Windows などは Chrome ブラウザが自動更新されますのでなおさらです。~~
訂正：はじまり
ChromeDriver で使用する Chrome ブラウザは Windows にインストール済の Chrome は使用せず、[JSON endpoints](https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json) からポータブルな Google Chrome for Testing ブラウザをダウンロードし適当なディレクトリに展開後、以下の設定を行うことによりバージョンを固定することが可能でした。
~~~php
// Google Chrome for Testing
$options->setBinary(storage_path() . '/chrome/chrome-win64/chrome.exe');
~~~
訂正：おわり

今回は Chrome DevTool Protocol(CDP) を使用して Web スクレイピングできないか調べてみました。

CDP で代表的なものは本家 Puppeteer や Microsoft の Playwright ですが、いづれも node ライブラリで PHP となると PHP から Node をブリッジして Puppeteer を叩く [PuPHPeteer(rialto-php/puphpeteer) ](https://github.com/rialto-php/puphpeteer)ですが、Windows では動作しましたが、残念ながら Linux(CentOS7) では動作しませんでした。
そこでピュア PHP 実装でどちらの環境でも動作する [Chrome PHP(chrome-php/chrome) ](https://github.com/chrome-php/chrome)を Laravel11/PHP8.3 環境にて検証してみました。もちろん Laravel でなくても素の PHP でも vendor/autoload.php でロードしてあげれば問題なく動作します。

# Laravel プロジェクトの作成
~~~php:
> composer create-project laravel/laravel chrome-php
> cd chrome-php
chrome-php> php artisan -V
Laravel Framework 11.19.0
~~~

# Chrome PHP のインストール
Composer で chrome-php/chrome をインストールします。
~~~php:
chrome-php> composer require chrome-php/chrome
~~~

# 使い方
Chrome PHP を使い Web スクレイピングしてみます。

Command クラスの雛型を生成します。
~~~php:
chrome-php> php artisan make:command ScrapingCommand
Console command created successfully.
~~~

試しにまた Yahoo! Japan トップページの主要ニュースをスクレイピングしてみましょう！
（利用規約に該当する場合は速やかに訂正いたします。見た感じ大丈夫のようですが）

![yahoo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/237317/786bc0ef-5346-f2e3-2eb1-6e9255689fa6.png)

~~~php:
<?php

namespace App\Console\Commands;

use HeadlessChromium\BrowserFactory;
use Illuminate\Console\Command;

class ScrapingCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:scraping';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $targetUrl = "https://www.yahoo.co.jp/";

        $browserFactory = new BrowserFactory();
        $browserFactory->addOptions([
            //'headless' => false,
            'noSandbox' => true,
            'enableImages' => false,
            'sendSyncDefaultTimeout' => 30000,
            //'debugLogger' => 'php://stdout'
        ]);
        // starts headless Chrome
        $browser = $browserFactory->createBrowser();

        try {
            // creates a new page
            $page = $browser->createPage();

            // navigate to an URL
            $navigation = $page->navigate($targetUrl);
            $navigation->waitForNavigation();

            // dom script in the browser
            $topics = $page->dom()->querySelectorAll("#tabpanelTopics1 div ul a");
            foreach ($topics as $topic) {
                $url = $topic->getAttribute("href");
                $title = $topic->querySelector('h1 > span')->getText();
                var_dump($url . ":" . $title);
            }
        } catch (Exception $e) {
            Log::error($e->getMessage());
        } finally {
            $browser->close();
        }
    }
}
~~~
Laravel でコマンドを実行をします。
~~~php:
chrome-php> php artisan command:scraping
string(86) "https://news.yahoo.co.jp/pickup/6509848:買い注文殺到 市場の動揺収まらず"
string(86) "https://news.yahoo.co.jp/pickup/6509852:財務省・金融庁・日銀が3者会合へ"
string(86) "https://news.yahoo.co.jp/pickup/6509847:株価乱高下 冷静な判断重要と首相"
string(86) "https://news.yahoo.co.jp/pickup/6509845:蒸し暑さと激しい雷雨 大気不安定"
string(86) "https://news.yahoo.co.jp/pickup/6509851:イラン全土への渡航中止勧告 政府"
string(84) "https://news.yahoo.co.jp/pickup/6509849:排水管から女児の声 気づいた5歳"
string(84) "https://news.yahoo.co.jp/pickup/6509850:Aスイミング日本 抗議で順位変更"
string(86) "https://news.yahoo.co.jp/pickup/6509837:バレー高橋健太郎 代表引退を明言"
~~~

# おわりに
ピュア PHP な Chrome PHP であるため Windows/Linux 環境でも問題なく動作しました。ただ Windows環境では ChromeDriver と比べ実行時間が長く起動に時間がかかる（引っかかる）印象がありました。

参考程度にですが、以下の動作環境は同一VM環境にての計測になります。

~~~powershell:Windows
chrome-php> powershell -C Measure-Command { php artisan command:scraping }
Days              : 0
Hours             : 0
Minutes           : 0
Seconds           : 11
Milliseconds      : 215
Ticks             : 112159288
TotalDays         : 0.000129813990740741
TotalHours        : 0.00311553577777778
TotalMinutes      : 0.186932146666667
TotalSeconds      : 11.2159288
TotalMilliseconds : 11215.9288
~~~

~~~php:Linux (CentOS7)
[chrome-php]$ time php artisan command:scraping
real    0m2.338s
user    0m0.415s
sys     0m0.185s
~~~

特段速度を要求される環境でなければ Web スクレイピングの実装は Chrome PHP でよいかと思います。実際実務でも Chrome PHP を採用し、現在 Linux 環境にて問題なく動作しています。

2025/04/03 追記
Chorme PHP はデータ取得処理 (get) に関しては問題ないのですが、ログインし登録処理などブラウザを操作する処理になると Wait が正しく動作しないなど、途端に難易度が上がることがわかりました。ブラウザ操作に関しては ChromeDriver を使用したほうが無難だと思います。