JSON-LD
のコンテキストもどうにか定義し,半信半疑だった API Gateway を活用したサーバ負荷回避 リクエスト制限の回避も実現できた.
これでようやくスクレピングによるデータ収集が始められる.
今回得た知見は,後ほど Zenn にまとめることとする.
全てのコードは Ningensei848/ML4Keiba においてある. シンプルな Node.js プロジェクト内部に Poetry プロジェクトを同居させた構成となっている.
API Gateway による多重リクエスト制限の回避
DoS 攻撃としてよく知られているように,短時間に極めて大量のリクエストをサーバに送ることは,それだけで悪意ある攻撃と見做されても仕方がない. リクエストをさばくロードバランサやプロキシサーバがリクエスト送信者の同一性を検知し,一定量を超えるとそれ以上応答が帰ってこなくなる. このような事態に陥ると,一定時間を待機すれば制限が解除され る場合もあれば,永遠にその IP からはアクセスできなくなるという場合もある.
これがすなわち何らかの刑事罰に直結するというわけではないが,機械的なリクエストはあくまでジェントルに,悪意の有無に関わらず利己的な操作はすぐに排除されるものだと心に刻んでスクリプトを組まねばならない.
が,逆説的に,IP が同一のものでなければシャットダウンする理由はない. 別の IP からのリクエストを見比べて,それが同一のユーザから送られたものであると判断するには材料が足りないし,なによりすべてのリクエストに対してそんな処理を挟む余裕もない.
俗に「IP アドレスローテーション」とかいう work around らしい.
Cloud Functions (Gen2) を生やして Gateway でまとめる
プロキシサーバを用意する……というとなんか難しそうな,フロントエンド人類には大変厳しい世界が待っていそうな気配がする. と思われたのもつかの間,Google Cloud Functions をつかってリクエストを代理させるエンドポイントを用意し,大量のエンドポイント URL に対して API Gateway で一つにまとめてやればよいことに気がついた.
もちろん,コンソール画面でポチポチするのは大変厳しい.それが GUI の限界である. しかし,GCP には Cloud SDK が用意され ており,そのうち gcloud CLI を使えば上記操作が半自動化できる.
というわけで用意したのが こちら である.
index.ts
は実際に Function としてデプロイされるスクリプトだ.
単にクエリパラメータを読んで,そこに含まれた URL にリクエストを投げ,返ってきたデータをそのままこちらへ返してくれる.
deploy.ts
と openapi.ts
は,ローカルで Cloud Functions と API Gateway に関する諸々の処理を全部やってくれるスクリプトだ.
namelist.txt
に書き連ねたエントリポイントを元に Functions を生やし,その情報をもとに openapi.yaml
を作成する.
と,上記の処理はすべて npm run exec:all --name=NAME --project=projectName
で実行できるように NPM スクリプトに書いてある.
これを有効化するために npm run gateway:describe:api --name=NAME --project=projectName
で API インスタンスの URI を入手し,gcloud services enable my-api-123abc456def1.apigateway.my-project.cloud.goog
で有効化すれば完了だ.
この実行が完了したら,npm run gateway:describe --name=NAME --project=projectName
で実際に作成されたエンドポイントの情報を得られる.
namelist.txt
に書き連ねた名前の分だけ Cloud Functions に関数がデプロイされ,エントリポイントが生える.
実際に Python 等でスクレイピングする際には,この namelist.txt
からパスを作成し,エンドポイントを叩く際にそのパスを参照させればよい.
データ構造とディレクトリ構成
集めたデータはすべて /data
以下に置いた.
GitHub のリポジトリ容量制限に引っかかるかもしれないな~~と思いつつ,流石にそこまでは肥大化しないだろうと高を括っている.
現段階では,主に horse
, race
で大分類を行なった.
将来的には,jockey
, trainer
, owner
, breeder
のディレクトリをつくることになるだろう.
その直下には,csv
, json
, list
といったデータの構造に関するディレクトリ構成としたが,これは冗長だしわかりにくいかもしれない.
二度手間ではあるが,この階層は消して,すぐ直下に YYYY
等が置かれるように変更するかもしれない.
前回の記事 で検討したように,各馬ごとに一ファイルを割り当てる方針 でディレクトリをつくる.
馬自身の血統やその他プロフ,戦績等は horse_id
に紐づいているから,これを分割してやるのが都合が良い.
YYYY
で生年,XXXX
で小分類,ZZ.tsv
とすることで,各小分類には最大でも 100 ファイル程度しか格納されないようにした(ただし,ID に数字以外が含まれている場合は,この限りではない).同様にレース情報も race_id
に紐づくようにした.