Friction River Software

  • お問い合わせ

CakePHP5入門【コラム⑯】データ自動更新の改良

A子

平日(月~金)は毎日クーロン実行しているナンバーズの当選番号データの自動更新なんだけどさ

たまに失敗してるみたいなんだよね(苦笑)

C菜

それはまずいです~

一日でも更新しない日があると、それ以降は自動更新できなくなっちゃいますよ~
(そういう更新アルゴリズムなので~)

A子

てなわけで聞いてみたよ、ChatGPT先生に…

どうやら通信速度の関係で、JavaScriptによって動的に埋め込まれるデータを取得できないときがあるらしい

B美

多分、みずほ銀行のサーバ側の問題でしょうね

スクレイピングの時に、たまたまアクセス過多になったとか…じゃないかな?

C菜

それで解決策は~?

A子

ばっちり教えてもらったよ

1.privateメソッドである「waitForSelector」の改良
2.コマンドクラスの戻り値として「成功」「失敗」を返す
3.コマンド実行をシェルスクリプトで繰返し行う

2番については、3番のための変更ね
(「失敗」時に再試行するため)

C菜

waitForSelector」メソッドはこうでしたけど~?

//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子

うん、それをこうするんだってさ

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

    while (true) {
        $exists = $page->evaluate(
            "document.querySelector('$selector') !== null"
        )->getReturnValue();

        if ($exists) {
            return true;
        }

        if ((microtime(true) - $start) * 1000 > $timeoutMs) {
            throw new \Exception("Selector timeout: $selector");
        }

        usleep(200000);
    }
}

C菜

え?
それって、ほとんど一緒じゃないですか~?

書き換える意味はあるのでしょうか~?

A子

私も同じ疑問を抱いたから質問してみたよ

querySelectorAll」ではDOMの中を全件走査するから時間がかかるけど、「querySelector」では見つけた瞬間に終了するらしい

B美

要するに、安定性の問題よ

JavaScriptによる埋め込みデータの分量が多い場合、「querySelectorAll」ではタイムアウト率が悪化する可能性があるってこと

C菜

なるほどですねぇ~

それでは次、2番はどうなりますか~?

A子

エラー発生時にメール送信してる箇所があったよね
あの中にエラーコードを返す記述を書くよ

//エラーが発生した場合、管理者あてにメール送信
if ($error_flag > 0) {
    $mailer = new Mailer('default');
    $mailer->setFrom([FROM_ADDRESS => FROM_NAME])
        ->setTo(TO_ADDRESS)
        ->setSubject(MAIL_SUBJECT)
        ->deliver(MAIL_BODY[$error_flag]);

    //スクレイピング関連のエラーの場合、エラーコードを返す
    if ($error_flag <= 5) {
        return static::CODE_ERROR;
    }

}

//正常終了
return static::CODE_SUCCESS;

C菜

static::CODE_ERROR」や「static::CODE_SUCCESS」って何ですか~?

A子

知らん(笑)

そう教わったからそのまま書いただけ…

B美

CakePHPのCommandクラスの中には、これらの定数が定義されているの
(「class LotteryCommand extends Command」というクラス定義を見ても分かる通り、Commandクラスを「継承」してるから使えるってわけ)

具体的には「CODE_SUCCESS」は「0」で、「CODE_ERROR」は「1」ってだけなんだけどね

C菜

あれ~?
以前出てきた「self」を使って「self::CODE_ERROR」や「self::CODE_SUCCESS」ではダメなんでしょうか~?

B美

良い質問ね
「self」でも良いし、それでもきちんと動くわよ

でも将来的に拡張される可能性があるフレームワークにおいては、「static」にするのがベストかな
(作法と言っても良い)

A子

ん?
どういうこと?

B美

self」は継承元のクラス(親クラス)優先で、「static」は継承先のクラス(子クラス)優先なの

つまり、継承先で書き換えられる可能性のある要素については「static」で指定しておくほうが、将来的な拡張性が高くなるってわけ

A子

あー
(なんだか面倒くさくなってきた…)

よくわかんないけど、そういうもんだって思っておけばOK?

C菜

私も今一つ理解できないですけど、ライブラリやフレームワークなどの誰かが用意してくれたやつだったら「static」を使って、自分で作ったやつの場合は「self」ってことでしょうか~?

B美

そういう認識でOKよ

まぁ、そこまで厳密に使い分ける必要もないんだけどね(苦笑)

A子

よし、よくわからんことは放っておいて…
最後の3番だよ

見て分かるだろうけど、シェルスクリプトね

B美

言うまでもないとは思うけど…

/home/bimi/html/numapp/bin/cake」の箇所は本番環境に合わせて変更するようにね
(特に「bimi」の箇所)

C菜

コマンドクラスの戻り値が「0」と等しければ「成功」で、そうでなければ60秒待って再試行…ってことですね~
(最大試行回数は5回です~)

B美

あとは私のほうで本番サーバへアップして、クーロン設定のほうもしておくから安心してね

それにしてもA子が自分で色々と動いたってのが驚きなんだけど…(苦笑)

A子

しっ、失礼な!

たまにはそういうこともあるよ

B美

「たまに」かよ!