netkeiba のデータをスクレイピングして LOD 化する(2)
python でデータを 扱うにあたり,Notebook を使わない選択肢はないだろう. Google が提供する Colaboratory を使って,「下書き」的にコードを書いていく.
https://colab.research.google.com/
まずは,木曜の夕方には確定する出走馬情報を得るアプローチを考える.
netkeiba.com においては,開催レースの一覧が /top/race_list.html
で提供されている.
ただし,これをそのまま cURL 等でページ取得しても,各日程ごとの情報は得られない.
なぜなら,jQuery で Ajax を頑張っているからだ(DevTool で調べてみるとわかるだろう).
サーバクライアントモデルよろしく,API エンドポイントにリクエストを投げてデータだけを得てクライアント側で出力しているのかと思いきや,HTML コードの断片を貰って埋め込んでいるような方式のように見える.
同じく DevTool でネットワークを監視してみると,/top/race_list_sub.html
に対して ?kaisai_date=YYYYMMDD
でリクエストを投げていることがわかった.
https://race.netkeiba.com/top/race_list_sub.html?kaisai_date=YYYYMMDD
留意すべきは,開催済み・開催前のレースの両方とも同一の形式で取得できてしまうことである.
すなわち,ベースとなる HTML が /race/shutuba.html
と /race/result.html
というように異なっている.
となると,beautifulSoup に喰わせるのではなく正規表現で race_id
を引っこ抜いてくるのが賢いだろう.
レース ID については,例えば凱旋門賞や香港スプリントといった中央競馬以外のレースの場合には,例外処理が必要かもしれない.
…………と思ったのもつかの間,ご丁寧に ?race_id=YYYYPPNNDDRR&rf=race_list
といった具合に &rf=race_list
が手がかりとなってくれている.
ありがたくこれを活用し,正規表現でレース ID のみをぶっこ抜く.
スクレイピングの負荷分散
で,実際にスクレイピングしていくにあたって,負荷分散を考えねばならないという問題がある. 例えば手元の Python プログラムで秒間 150 回のリクエストをサーバに送ったとしよう. それによって自分は素早く大量のデータを得ることができるが,そんな大量のリクエストを捌かねばならない(しかも無償で!)サーバ側はたまったものではない. 多くの場合,ロードバランサーなり監視システムなりが同一 IP からの異常リクエストを検知し遮断する措置が取られる.
データを集めるためには「お行儀よく」スクレイピングのコードを書く必要がある……とこれまでは思っていたが,あまりにも時間がかかりすぎる. 待っている時間にもサーバ利用料金は発生してしまうし,資源の無駄である. どうにか回避するためには,複数箇所から同時並行にリクエストを投げれば良いことに気づくだろう. 各プロセスが 2 秒ずつ待機せねばならなかったとしても,それが 10 プロセス同時であれば効率は十倍になる(進次郎構文).
最近になって,これを手軽に実装できるのは,Google API Gateway + Google Cloud Function の組み合わせであろうことに気がついた.
単にリクエストのプロキシになってもらうというだけなので,特段難しいコードは存在しない(Fetch のみ).
ちょうど先月にプレビュー版が出た Cloud Function Gen 2 を試しつつ,API Gateway で wrap して,Python からでも並列処理がしやすい方法を検討する.
(ただし,現時点ではエントリポイントがパブリックにオープンになってて悪用の恐れがあるので,認証かなんかをきちんと検証する必要がある)
API Gateway に対するリクエストが膨大になっても問題ないっぽいし,認証に API キー使えば不特定多数にオープンになるリスクも減らせる.
このドキュメントによると,「100秒あたり1000万リクエストまで」がレート制限っぽいので,個人が100並列でスクレイピングかける程度だと全然問題ないっぽい,やったぜhttps://t.co/uAiCr1YTo8 https://t.co/gFPWwErK6k
— 気合の鬼🌗 (@Ningensei848) 2022年3月12日
API Gateway と Cloud Function
色々こねこねして,使いたい名前をファイルに列挙するだけで API Gateway が生えるようにした.
$ npm run exec:all --name=NAME --project=projectName
$ npm run gateway:describe --name=NAME --project=projectName
$ npm run gateway:describe:api --name=NAME --project=projectName
$ gcloud services enable my-api-123abc456def1.apigateway.my-project.cloud.goog
gateway:describe
でエンドポイントが得られ,gateway:describe:api
でマネージドサービスプロパティが表示される.
gcloud services enable ${managedServiceUri}
で API を有効化することで,エンドポイントに対して API キーによる認証付きでリクエストができるようになる.
また,その API キーについては,コンソール上のAPI とサービスで取得する.
〈呼び出せるキーの制限〉は,openapi.yaml
で info.title
で指定したもの1をドロップダウンから選べば良い.
これでエンドポイントには,クエリパラメータとして key=${API_KEY}
を渡さないと拒否されるようになった.
リクエストのプロキシ
いよいよ python コードを書くことになるが,リクエストを投げるのは対象とする URL ではなく,API Gateway のエンドポイントであり,URL はクエリパラメータとして url=${TARGET_URL}
という形で渡す.
返り値は特に加工してないため,単純にリクエストを投げたのと同様の HTML が返る.
一箇所から規定時間内に規定回数以上のリクエストを投げると,DoS 攻撃と勘違いされてアクセス制限の憂き目に合うが,Cloud Functions で負荷分散+ Gateway でプロキシしてやれば,問題は生じないかも?
まだ検証していない段階だが,ひとまず API Gateway の自動生成までは出来たので一区切り.