CakePHP4でPHP Simple HTML DOM Parserを使用してスクレイピングをする

CakePHP4でスクレイピングをする方法を紹介します。

PHPでスクレイピングというと様々なライブラリがありますが、本記事では「PHP Simple HTML DOM Parser」を使用します。
使いやすいライブラリなので、オススメだと思います。

PHP Simple HTML DOM Parserをインストールする

https://github.com/Kub-AT/php-simple-html-dom-parser

githubのページの通り、composerを使用してインストールができます。
下記の通り、実行してください。

composer require kub-at/php-simple-html-dom-parser

PHP Simple HTML DOM Parserの使い方

まずは、use文でライブラリを読み込みます。

use KubAT\PhpSimple\HtmlDomParser;

続いて、実際にスクレイピングする方法です。
僕は競馬が好きなので、本記事ではJRAのホームページをスクレイピングしてみます。

例として、以下の通り、「特別レース登録馬」のページから、開催が行われる日にちと競馬場をスクレイピングしてみます。

JRAのホームページをスクレイピング

Webサイトのコンテンツ取得には、CakePHP4の場合、Http Clientを使用します。
https://book.cakephp.org/4/ja/core-libraries/httpclient.html

JRAのページでは、GETではなくPOSTでページを作っているので、以下のようにPOST送信を行います。
また、SJISでエンコーディングされているため、取得したレスポンスはSJISを指定して、エンコードします。

// post送信をして、コンテンツを取得
$http = new Client();
$response = $http->post('https://www.jra.go.jp/JRADB/accessT.html', [
    'cname' => 'pw03trl00/29',
]);
$body = mb_convert_encoding($response->getStringBody(), "utf-8", "sjis");
$dom = HtmlDomParser::str_get_html($body);

ここまでで、指定したページのHTMLがDOM形式で取得できます。

続いて、ページの中から以下の部分を取得します。

JRAのホームページをスクレイピング

この部分は以下のようにdivタグでclassが「date_unit」となっています。
このdivタグが土曜、日曜でそれぞれ1つずつ(計2つ)あるという構造です。

JRAのホームページをスクレイピング

そのため、この部分を取得するためには以下のようにします。
複数あるため、ループするようにしています。
以下のように、DOM形式から要素を指定して取得できるので使いやすいですね。

// 日付のブロックを取得
foreach($dom->find('div[class=date_unit]') as $elem_date_unit) {
    ...
    ...
}

日付のブロックが取得できたので、続いて、その中身を取得してみます。
まず、日付部分は以下のh2タグで構成されています。

JRAのホームページをスクレイピング

このh2タグを取得するには、HTMLのタグ構成をそのまま指定します。
div[class=block_header] div h2
という指定をすることで、classが「block_header」のdivタグの中のdivタグの中のh2タグという感じで、階層を辿って取得できます。

日付部分は、m月d日という形式になっているので、正規表現を指定して、月日の数字を抜き出しています。

$elem_date_h2 = $elem_date_unit->find('div[class=block_header] div h2')[0];
preg_match('/(\w+)月(\w+)日/', $elem_date_h2->innertext, $match_date);

続いて、各競馬場ごとの特別レースの部分です。
この部分は以下のようなdivタグの構成になっています。

JRAのホームページをスクレイピング

これを取得するには以下のようにします。

// 開催場所のブロックを取得し、競馬場ごとに開催情報を取得
$elem_day_line = $elem_date_unit->find('div[class=day_line mt10] div[class*=rc]');
foreach($elem_day_line as $index => $elem_rc) {
    ....
    ....
}

まず、classが「day_line mt10」となっているdivタグ、そしてその中のclassが「rc」となっているdivタグを取得しています。
「rc」としている部分は、「rc rcA」「rc rcB」「rc rcC」とタグごとに指定されているclassが異なるので、部分一致になるようにしています。

さらに、競馬場の開催情報の部分(以下画像で “1回東京7日" となっている部分)は以下のようなタグの構成になっています。

JRAのホームページをスクレイピング

この部分を取得するには、以下のようにします。
divタグの中のh3タグを取得、という指定の仕方です。

$elem_rc_h3 = $elem_rc->find('div h3')[0];

このように、取得したい要素のタグの構成を確認して、DOM形式から指定した要素を取得することができます。
今までの処理を繋げると、以下のようになります。

// 日付のブロックを取得
foreach($dom->find('div[class=date_unit]') as $elem_date_unit) {
    $elem_date_h2 = $elem_date_unit->find('div[class=block_header] div h2')[0];
    preg_match('/(\w+)月(\w+)日/', $elem_date_h2->innertext, $match_date);

    // 開催場所のブロックを取得し、競馬場ごとに開催情報を登録
    $elem_day_line = $elem_date_unit->find('div[class=day_line mt10] div[class*=rc]');
    foreach($elem_day_line as $index => $elem_rc) {
        $elem_rc_h3 = $elem_rc->find('div h3')[0];
        preg_match('/\d回(.+)\d日/', $elem_rc_h3->innertext, $match_race_course);

        $now = new \DateTime();
        $year = date('Y');
        $venue_date = $now->setDate((int) $year, (int) $match_date[1], (int) $match_date[2]);

        $this->log($venue_date->format('Y-m-d').' '.$elem_rc_h3->innertext.' '.$match_race_course[1], 'debug');
    }
}

これでログを出力してみると、以下のような形式で各要素が取得できたのを確認できました。

2021-02-16 05:13:13 Debug: 2021-02-20 1回東京7日 東京
2021-02-16 05:13:13 Debug: 2021-02-20 1回阪神3日 阪神
2021-02-16 05:13:13 Debug: 2021-02-20 2回小倉3日 小倉
2021-02-16 05:13:13 Debug: 2021-02-21 1回東京8日 東京
2021-02-16 05:13:13 Debug: 2021-02-21 1回阪神4日 阪神
2021-02-16 05:13:13 Debug: 2021-02-21 2回小倉4日 小倉

PHP Simple HTML DOM Parserを使用すると、簡単にスクレイピングできて便利ですね。
本記事では、要素の取得方法の一部を紹介しましたが、他にも様々な方法で取得することができます。
詳しい使い方は公式ドキュメント(英語)をご覧ください。

▼公式ドキュメント(英語)
https://simplehtmldom.sourceforge.io/manual.htm