PR
個人開発 cronPHPスクレイピング個人開発共有サーバー

共有ホスティングで Akamai を抜けるまで

青い光に照らされたサーバールームの通路 個人開発

ahha には抽選結果のデータが要ります。公式サイトから定期的に取得しなければなりません。

簡単な仕事だと思っていました。リクエストを送り、パースし、CSV に保存し、cron に載せれば終わり。

403 が返ってきました。

スポンサーリンク

相手は Akamai Bot Manager

いくつかの方法を試しましたが、全部 403 でした。

curl https://<対象>
# → 403

php -r 'echo file_get_contents("https://<対象>");'
# → 403

User-Agent をブラウザのものに変えても、ヘッダを全部真似ても同じでした。ブラウザで開けば普通に開きます。

Akamai Bot Manager が TLS ハンドシェイクの段階で弾いていました。

HTTP ヘッダではなく、その下の層の話です。TLS 接続を確立するときにクライアントが送る情報 — 対応する暗号スイートの一覧、拡張フィールドの順序、楕円曲線パラメータなど — の組み合わせが、クライアントごとに固有の指紋(JA3 / JA4 フィンガープリント)を作ります。

curl の指紋と Chrome の指紋は違います。Akamai はそこを見ます。ヘッダをどれだけブラウザらしく飾っても意味がありません。

制約条件

このプロジェクトのサーバーは日本の共有ホスティング(エックスサーバー)でした。

  • Python 3.6.8(標準インストール版)
  • PHP 8.3 CLI が使える
  • cron が使える
  • root 権限なし
  • コンパイルツール・開発ヘッダはほぼ入っていない

そしてプロジェクトの方針として「エックスサーバー + PHP」が明記されていました。インフラを勝手に変えられない状況です。

五回、方向を変えた

試行 1 — Python + curl_cffi

curl_cffi は curl-impersonate を Python から使えるようにしたライブラリです。ローカルではこれで通りました。

サーバーに上げるとPython 3.6.8 では動きません。最低 3.8 以上が必要です。

試行 2 — PHP で書き直す

PHP にも curl があるから通るだろうと思って移植しました。

即 403。当然でした。PHP の curl も結局システムの libcurl を使うので、指紋は同じです。言語を変えても TLS の指紋は変わりません。

この段階で「PHP では根本的に無理だ」と結論づけるところでした。後から見ると、これが早すぎる断定でした。

試行 3 — pyenv で Python 3.11 を入れる

サーバーに新しい Python を自前でビルドして置こうとしました。pyenv install 3.11 には OpenSSL・libffi の開発ヘッダが必要です。共有ホスティングには入っておらず、root がないので入れられません。調査段階で詰みました。

試行 4 — GitHub Actions の cron

「サーバーで無理なら外で回そう」という発想です。技術的には成立したはずです。

ただ、無料枠には使用量の上限があり、プロジェクトが育てば結局コストになります。すでに料金を払って使っているサーバーがあるのに外に出るのはどうか、という指摘が入りました。妥当だったので廃案。

試行 5 — PHP + curl-impersonate バイナリ → 成功

発想を変えて「PHP から curl ライブラリを使うのではなく、curl-impersonate の実行ファイルを直接呼べばいいのでは」を調べました。

日本語のブログに実際の使用例がありました。PHP の exec() 系で curl-impersonate のバイナリを呼ぶ方式です。

スポンサーリンク

PythonによるWebスクレイピング(第3版)

PythonによるWebスクレイピング(第3版)

楽天で見る

スポンサーリンク

PythonによるWebスクレイピング (第3版)

PythonによるWebスクレイピング (第3版)

Yahoo!ショッピングで見る

curl-impersonate とは

curl をフォークして特定ブラウザの TLS 指紋をそのまま再現するようにしたプロジェクトです。Chrome・Firefox・Safari などの指紋を模した実行ファイルが提供され、事前コンパイル済みバイナリを取ってきて実行するだけで動きます。コンパイルも root 権限も要りません。

curl_chrome116     Chrome 116 の指紋
curl_chrome110     Chrome 110 の指紋
curl_ff109         Firefox 109 の指紋

構成

1. バイナリの配置

事前コンパイル済みの Linux x86_64 版を取得してサーバーに置き、実行権限を与えます。

/home/<user>/scraper/bin/
  curl_chrome116
  curl-impersonate-chrome
  (関連する .so ファイル)

chmod +x /home/<user>/scraper/bin/curl_chrome116

2. PHP から呼ぶ

<?php
/**
 * curl-impersonate のバイナリを呼び出して HTML を取得する。
 *
 * PHP の curl 拡張はシステム libcurl を使うため TLS 指紋が変わらない。
 * 外部バイナリを呼ぶことでブラウザの指紋を再現する。
 */
$bin = '/home/<user>/scraper/bin/curl_chrome116';
$url = 'https://<対象>';

$cmd  = escapeshellcmd($bin) . ' ' . escapeshellarg($url);
$html = shell_exec($cmd);

if ($html === null || $html === '') {
    // 失敗処理
    exit(1);
}

escapeshellarg() を忘れないこと。URL を外部入力から受け取るなら必須です。

3. cron に登録

PHP 8.3 CLI のパスを明示して登録します。共有ホスティングは既定の php が旧バージョンのことが多いためです。

0 20 * * 1,4 /opt/php-8.3/bin/php /home/<user>/scraper/fetch.php >> /home/<user>/scraper/cron.log 2>&1

このプロジェクトでは公式サイトの更新が遅れることがあるので、20:00 と 20:30 の二回を仕掛けています。一度目が失敗しても 30 分後に再試行する構えです。

4. 結果の配布

パースした CSV をドキュメントルート下に置き、アプリがそれを取りに行きます。アプリ側は取得した CSV をローカルキャッシュ(TTL 1 時間)に保存し、オフラインならバンドル同梱の初期データにフォールバックします。

学んだこと

「根本的に無理」はたいてい調査不足

試行 2 で「PHP では無理」と結論づけるところでした。実際には PHP で通りました。方法が curl ライブラリではなく外部バイナリの呼び出しだっただけです。

技術的な不可能を宣言する前に、実際の使用例を検索したほうがいい。先に同じ壁にぶつかった人がいて、たいてい解法もあります。

方向を決める前に環境を調べる

試行 1 はサーバーの Python バージョンを先に確認していればやらずに済みました。試行 3 も開発ヘッダの有無を先に見ていれば同じです。

ローカルで動くものをサーバーに上げるのではなく、サーバーで動くものをローカルで作る順序であるべきでした。

すでに持っているインフラの中でまず解く

試行 4 は技術的にはいちばん楽な道でした。個人開発では「とりあえずクラウドサービスを足そう」の誘惑が大きい。ひとつずつは無料枠でも、積み重なると固定費になります。

文書に明記された方針を勝手に変えない

「エックスサーバー + PHP」はプロジェクト文書に書かれた方針でした。それを無視して Python に固執したり外部サービスに出ようとしたのが試行 1〜4 です。制約の中で探したら答えが出ました。制約を取り払おうとするより、制約の中で探すほうが速いことが多い。

スポンサーリンク

これで使える実践Webスクレイピング

これで使える実践Webスクレイピング

楽天で見る

スポンサーリンク

金徳謙 これで使える実践Webスクレイピング Pythonで学ぶWeb情報収集 広島修道大学テキストシリーズ Book

金徳謙 これで使える実践Webスクレイピング Pythonで学ぶWeb情報収集 広島修道大学テキストシリーズ Book

Yahoo!ショッピングで見る

この記事について

自社アプリ ahha のデータ取得基盤を作る過程で踏んだ内容です。ここで紹介した手法は、公開されているデータを常識的な頻度(週 2〜3 回)で取得するためのものであり、対象サイトに負荷をかける使い方は勧めません。対象サイトは特定していません。

Akamai が正確に JA3 なのか JA4 なのか、あるいは他の指標まで見ているのかは確定できていません。「TLS フィンガープリント検査」のレベルまでしか把握しておらず、通過後は追っていません。curl-impersonate がいつまで有効かも分かりません。Chrome のバージョンが上がれば再現対象も変わります。

コメント

タイトルとURLをコピーしました