概要
Laravel でヘッドレスブラウザを使用して Web スクレイピングをしたいのですが、 Selenium(php-webdriver) や php-phantomJS などより、Laravel 公式パッケージでブラウザテストの自動化およびテスティング API の Laravel Dusk を使用してWebスクレイピングできないか調べてみました。
現在の環境が PHP7.4 環境ですので、必然的にLaravel のバージョンは 8.x となります。PHP と Laravel のバージョン対応表は こちら
Laravel 8.x プロジェクトの作成
> composer create-project laravel/laravel=8.* scraping
> cd scraping
Laravel Dusk のインストール
プロジェクトに Laravel/Dusk Composer 依存パッケージを追加します。
Laravel が 8.x のため dusk 7.x 系はインストールされず、v6.25.2 がインスールされました。
> composer require --dev laravel/dusk
Cannot use laravel/dusk's latest version v7.12.3 as it requires php ^8.0 which is not satisfied by your platform.
> composer show -i | findstr dusk
You are using the deprecated option "installed". Only installed packages are shown by default now. The --all option can be used to show all packages.
laravel/dusk v6.25.2 Laravel Dusk provides simple end-to-end testing and browser automation.
Laravel dusk をインストールします。
> php artisan dusk:install
ChromeDriver をインストールします。
> php artisan dusk:chrome-driver
ChromeDriver binaries successfully installed for version 114.0.5735.90.
ChromeDriver サイトの更新停止
ChromeDriver のバージョン 114 がインストールされたのですが、残念ながら dusk 6.x のままでは ChromeDriver サイトの更新停止により 115 以降に更新できません。現在のクライアントの Chrome ブラウザのバージョンが 122 ですのでこのままではバージョンの不一致により ChromeDriver エラーとなり正常に動作しません。
ChromeDriver - WebDriver for Chrome - Downloads
この赤文字の情報から新たなダウンロードサイトは JSON endpoints になります。ちなみに dusk7.x はこの JSON endpoints での更新をサポートしています。
< 115 : https://chromedriver.storage.googleapis.com/index.html
>= 115 : https://googlechromelabs.github.io/chrome-for-testing/
dusk7.x → dusk6.x クラスの差し替え
dusk6.x 環境でもなんとか ChromeDriver を最新に上げたいなーとソースを斜め読みしながら dusk 6.x と dusk 7.x のソースと比較してみたところ ChromeDriverCommand クラスが ChromeDriver の更新処理を担っていることがわかりました。
dusk 7.x の ChromeDriverCommand クラスとこのクラスが依存する OperatingSystem クラスを dusk 6.x のクラスと差し替えます。
dusk7.x → dusk6.x
/vendor/laravel/dusk/src/Console/ChromeDriverCommand.php
/vendor/laravel/dusk/src/OperatingSystem.php
throw 式構文のダウングレード
差し替えにあたって、dusk7.x の ChromeDriverCommand クラスは PHP7.4 では記述できなかった PHP8.0 以降の throw 式構文による Null 合体演算子で記述されているため、4 ケ所ほど以下のようにダサーく修正します。
@@ -138,2 +138,4 @@
- return $milestones['milestones'][$version]['version']
- ?? throw new Exception('Could not determine the ChromeDriver version.');
+ if (is_null($milestones['milestones'][$version]['version'])) {
+ throw new Exception('Could not determine the ChromeDriver version.');
+ }
+ return $milestones['milestones'][$version]['version'];
再度 ChromeDriver の更新
再度 ChromeDriver をアップデートしたところ、最新の ChromeDriver 122 にすることができました。オリジナルのソース改変はあまりやりたくないのですが、dusk6.x はもう更新されないだろうと汗。
> php artisan dusk:chrome-driver
ChromeDriver binary successfully installed for version 122.0.6261.57.
使い方
dusk6.x の ChromeDriver を使い Web スクレイピングしてみます。
Command クラスの雛型を生成します。
> php artisan make:command ScrapingCommand
Console command created successfully.
試しに Yahoo! Japan トップページの主要ニュースをスクレイピングしてみましょう!
(利用規約に該当する場合は速やかに訂正いたします。見た感じ大丈夫のようですが)
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\WebDriverBy;
use Laravel\Dusk\Chrome\ChromeProcess;
use Exception;
/**
* ScrapingCommand class
*/
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';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$targetUrl = "https://www.yahoo.co.jp/";
$process = (new ChromeProcess())->toProcess();
try {
if ($process->isStarted()) {
$process->stop();
}
$process->start();
$options = (new ChromeOptions())->addArguments([
'--disable-gpu',
'--headless',
'--window-size=1200,1000',
'--no-sandbox'
]);
$capabilities = DesiredCapabilities::chrome()->setCapability(ChromeOptions::CAPABILITY, $options);
$driver = retry(5, function () use ($capabilities) {
return RemoteWebDriver::create('http://localhost:9515', $capabilities, 50000, 60000);
}, 5000);
// scraping start
$driver->get($targetUrl);
// waiting for 'footer' id load
$driver->wait(10, 1000)->until(
WebDriverExpectedCondition::visibilityOfElementLocated(
WebDriverBy::id('footer')
)
);
// Yahoo! top topics
$topics = $driver->findElements(
WebDriverBy::cssSelector('#tabpanelTopics1 ul a')
);
foreach ($topics as $topic) {
$url = $topic->getAttribute("href");
$title = $topic->findElement(
WebDriverBy::cssSelector('h1 > span')
)->getText();
var_dump($url . " : " . $title);
}
} catch (Exception $e) {
Log::error($e->getMessage() . "\n");
} finally {
$process->stop();
}
}
}
Laravel のコマンド実行をします。
> php artisan command:scraping
string(86) "https://news.yahoo.co.jp/pickup/6492295 : 松野・高木氏ら5人 政倫審出席へ"
string(85) "https://news.yahoo.co.jp/pickup/6492279 : 1月の貿易収支 1兆7583億円の赤字"
string(88) "https://news.yahoo.co.jp/pickup/6492292 : 外務省 元徴用工訴訟巡り厳重抗議"
string(88) "https://news.yahoo.co.jp/pickup/6492294 : 西山ファーム 元副社長の身柄確保"
string(85) "https://news.yahoo.co.jp/pickup/6492290 : 榊容疑者 映画監督の立場悪用か"
string(88) "https://news.yahoo.co.jp/pickup/6492289 : 罰則ないカスハラ防止条例 効果は"
string(88) "https://news.yahoo.co.jp/pickup/6492293 : ペット死体巡り批判 市が対応変更"
string(88) "https://news.yahoo.co.jp/pickup/6492299 : 仲里依紗 激変も「今の私が自分」"
おわりに
最近はHTMLソースを見ただけではなにをやってるかわからない、ヘッドレスブラウザによるスクレイピングじゃないとNGな動的サイトが増えてきていますね。
次は Chrome DevTools Protocol(CDP) でやってみたいですね。