Friction River Software

  • お問い合わせ

CakePHP5入門【WebAPI編⑩】データ自動更新①

A子

ナンバーズの当選番号データを毎回『手動で更新する』のがかなり面倒くさい

B美が前に言ってたよね?
自動で更新する方法がある」…って

B美

ふむ、仕方ないかぁ
(本当はやりたくないんだけど…)

cd html/numapp[Enter]
composer require chrome-php/chrome[Enter]

これを「MATE端末」上で実行してね


C菜

なんかエラーみたいなのが出てますよ~

Skipped installation ・・・」って~

B美

それは気にしなくても大丈夫…
んで、次にrootになってからaptコマンドでパッケージをインストールします

su -[Enter]
apt update[Enter]
apt install chromium libnss3-tools[Enter]

必ず事前に「apt update」を実行してね
(じゃないと、エラーが出るかも…)









A子

えらくたくさんのパッケージがインストールされたね(苦笑)

B美

甘いわよ
まだまだ足りないわ

次は以下のパッケージをインストールしてね

apt install fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc-s1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 xdg-utils[Enter]
apt install fonts-noto fonts-noto-cjk fonts-ipafont fonts-ipaexfont[Enter]
exit[Enter]

あ、どちらも途中で「Y」を押すこと
(二つ目については、フォントのインストールに時間がかかるから分けてます)


A子

うわぁ、大変だなー(棒)

まぁ、それはともかく、クーロンで自動実行するわけだからコマンドクラスを用意すれば良いんだよね?

B美

もちろんよ

bin/cake bake command lottery[Enter]

これで「src/Command/LotteryCommand.php」が作られるわね

C菜

そのクラスの中に以下の記述を追加しますね~
テーブルのフェッチです~)

private $Numbers;

public function initialize(): void
{
    parent::initialize();

    $this->Numbers = $this->fetchTable('Numbers');
    $this->loadComponent('SixSevenWeek');
}

データベース登録時に「曜日」や「六曜」計算が必要なので、「SixSevenWeek」コンポーネントのロードも記述しておきます~

B美

ちょっと待って!
コンポーネントって、コントローラーから共有できるライブラリなんだけど、コマンドクラスから使うには少し工夫が必要なの

いくつかの方法があるんだけど、私はいつもこうしてるわ

use Cake\Controller\ComponentRegistry;
use App\Controller\Component\SixSevenWeekComponent;

を先頭に記述してから、「initialize」メソッドには以下の一文を追加

$this->SixSevenWeek = new SixSevenWeekComponent(new ComponentRegistry());

あ、クラスフィールド($SixSevenWeek)の宣言もしておいたほうが良いかもね
(無くても動くけど…)

あとは従来通り

$six = $this->SixSevenWeek::getSix('2026-03-31');

…って感じで使えるのよ

C菜

ということは~

private $Numbers;
private $SixSevenWeek;

public function initialize(): void
{
    parent::initialize();

    $this->Numbers = $this->fetchTable('Numbers');
    $this->SixSevenWeek = new SixSevenWeekComponent(new ComponentRegistry());
}

これでいかがでしょうか~?

B美

OKよ
それじゃ、次はWebページへのアクセスについて実装していきましょう

use HeadlessChromium\BrowserFactory;
use HeadlessChromium\Exception\BrowserException;

この二行を先頭に記述したあと、「execute」メソッド内にはこう書くの

//時間がかかるかもしれないので、タイムアウトを無効にする
set_time_limit(0);

//ナンバーズ3サイトのURL
$url3 = 'https://www.mizuhobank.co.jp/takarakuji/check/numbers/numbers3/index.html';

//ナンバーズ4サイトのURL
$url4 = 'https://www.mizuhobank.co.jp/takarakuji/check/numbers/numbers4/index.html';

//エラー発生の有無
$error_flag = false;

//引数として「which chromium」の結果ではダメ(本当の場所を指定)
$browserFactory = new BrowserFactory('/usr/lib/chromium/chromium');

//仮ホームディレクトリ
$userDataDir = sys_get_temp_dir().'/chrome-profile';

//ブラウザ生成
$browser = $browserFactory->createBrowser([
    'headless' => true,
    'noSandbox' => true,
    'startupTimeout' => 60,
    'connectionDelay' => 30,
    'args' => [
        '--user-data-dir='.$userDataDir,
        '--disable-blink-features=AutomationControlled',
        '--disable-gpu',
        '--disable-dev-shm-usage',
        '--disable-features=VizDisplayCompositor',
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--single-process',
        '--no-zygote',
        '--disable-software-rasterizer',
        '--disable-extensions',
        '--disable-background-networking',
        '--disable-sync',
        '--metrics-recording-only',
        '--mute-audio',
    ]
]);

//0.5秒待機
usleep(500000);

try {
    //ブラウザページを作成(これを使ってURLにアクセスする)
    $page3 = $browser->createPage();

    //ヘッドレス検知回避
    $page3->addPreScript("
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });
    ");

    //ユーザエージェント偽装
    $page3->setUserAgent(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' .
        '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    );

    //ナンバーズ3サイトをアクセス
    $page3->navigate($url3)->waitForNavigation();
    $this->waitForSelector($page3, '.js-lottery-number-pc');
    $text3 = $page3->evaluate('document.documentElement.outerHTML')->getReturnValue();

} catch (BrowserException $e) {
    $error_flag = true;
} finally {
    $page3->close();
}

try {
    //ブラウザページを作成(これを使ってURLにアクセスする)
    $page4 = $browser->createPage();

    //ヘッドレス検知回避
    $page4->addPreScript("
        Object.defineProperty(navigator, 'webdriver', {
            get: () => undefined
        });
    ");

    //ユーザエージェント偽装
    $page4->setUserAgent(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' .
        '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    );

    //ナンバーズ4サイトをアクセス
    $page4->navigate($url4)->waitForNavigation();
    $this->waitForSelector($page4, '.js-lottery-number-pc');
    $text4 = $page4->evaluate('document.documentElement.outerHTML')->getReturnValue();

} catch (BrowserException $e) {
    $error_flag = true;
} finally {
    $page4->close();
}

$browser->close();

A子

えっと、あまりにも複雑で、理解が追いつかないんだけど…(苦笑)

まず「BrowserFactory」クラスをインスタンス化してる箇所のコメントって何?

B美

MATE端末」上で以下のコマンドをたたくと…

which chromium[Enter]

/usr/bin/chromium」って出るんだけど、それを引数に渡しても動かないのよ

本当の場所は「/usr/lib/chromium/chromium」なので、そっちを指定しないといけないってわけ
(もちろん「/usr/bin/chromium」も存在するんだけど…)


C菜

仮ホームディレクトリ」って何ですか~?

B美

そのURLに「アクセスしてるのが(コンピュータではなく)人間である」ということをサーバに誤認させるための手段の一つよ

多くのWebサイトがスクレイピングを許可しないよう、アクセス制限をかけてるからね
(みずほ銀行のWebサイトも2026年3月17日から対策済み)

A子

createBrowser」メソッドのめっちゃ細かい引数もそういうこと?

C菜

ヘッドレス検知回避」や「ユーザエージェント偽装」も同じ(人間への偽装)だと思うです~

あ、「tryトライ catchキャッチ finallyファイナリー」って何ですか~?

B美

例外が発生する(かもしれない)処理をtryブロック内に書くの
んで、もしも例外が発生したらcatchで捕捉するってわけ

あと、finallyブロックは例外の有る無しにかかわらず、必ず最後に実行される処理なのよ

A子

「例外」って「エラー」のことだよね?

要するに、エラーが発生してもプログラムが止まらないし、必ず実行しなきゃいけない処理(finally)についても最後に確実に実行できるってことかー
(便利な仕組みじゃん)

B美

そういうこと
ライブラリを使うときには、割とよく記述するから覚えておいてね

あと、「waitForSelector」というメソッドを呼び出している箇所があるわよね
それをprivateメソッドとして記述します

//Webページ内にJavaScriptによる値の埋め込みが完了するまで待つ(デフォルト値は最大30秒)
private function waitForSelector($page, string $selector, int $timeoutMs = 30000)
{
    $start = microtime(true);

    while (true) {
        $exists = $page->evaluate(
            'document.querySelectorAll("' . $selector . '").length'
        )->getReturnValue();

        if ($exists > 0) {
            return;
        }

        if ((microtime(true) - $start) * 1000 > $timeoutMs) {
            throw new \RuntimeException("Timeout waiting for: $selector");
        }

        usleep(500000);
    }
}


A子

よくわかんないけど、そのまま書けば良いんだね?

B美

ええ
それでOKよ

さて、どういうHTMLデータを取得できたのか、webrootの下にファイル出力してみましょうか

file_put_contents(WWW_ROOT.'numbers3.txt', $text3);
file_put_contents(WWW_ROOT.'numbers4.txt', $text4);

この二行を「execute」メソッドの末尾に書きましょう
(ちなみに、コントローラーでは「WWW_ROOT」の記述は不要なんだけど、コマンドクラスでは必要だから注意してね)

C菜

それでは実行してみるです~

bin/cake lottery[Enter]

あ、ファイルができましたよ~

A子

ファイルの中身を見てみよう

うーん、テキスト量が多すぎる…(苦笑)
なんとか当選番号データの箇所を見つけたよ

C菜

この中から必要な情報を探し出すのって、かなり大変そうです~

簡単に見つけられるような方法ってあるのでしょうか~?

B美

当然!
(でないとスクレイピングなんてやってられないわ)

ただ、長くなったので今回はここまでにしておきましょう
次回にスクレイピング方法を解説して、データベース登録までの流れをやっていくからね

C菜

楽しみです~

それにしてもB美部長がやりたくないっておっしゃっていた理由が分かりましたね~

A子

たしかに…(笑)