Skip to main content

netkeiba のデータをスクレイピングして LOD 化する(3)

Kiai
@Ningensei848

前回はスクレイピング効率を高めるためにプロキシサーバを作ろうという試みを行なって終わった. 今回は具体的にどのようにデータを集めるか検討する.

race_id

前回でも少し触れたように,netkeiba では /top/race_list_sub.html に対して ?kaisai_date=YYYYMMDD を投げて HTML の断片を受け取っている. この URL に対してスクレイピングをかければ,うまいことレースの一覧が得られるということである.

この URL の嬉しい点として,起点が YYYYMMDD になっているところである. 例えば本日の日付 20220315 を投げれば今日開催されるレースが返ってくる………それはつまり,毎日繰り返し定期的にデータを集められるようになるということだ.

また嬉しいことに,JRA だけでなく NAR(地方競馬)についても同様の方法でレース一覧情報が提供されていた. こちらは JRA と異なり平日もバンバン開催されているので,試行回数を増やせるという点ではアドバンテージとなりうるだろう.

netkeiba の文字コード

「競馬の予測」に使えるのは「レース結果」ではなくレース直前の情報だが,それは https://{race|nar}.netkeiba.com/race/shutuba.html?race_id=YYYYPPNNDDRR で得ることができる. ただし,気をつけるべきことがあった. それは,netkeiba のページにおける文字コードが EUC-JP に指定されているということである.

たかが文字コード,されど文字コード…… 主に Python でのスクレイピングかつ対象が日本語ページであるとを考えると,これは死活問題である. おそらくは requests + BeautifulSoup の組み合わせでスクリプトを書くだろうが,これらの基本文字コードは utf-8 である. すなわち,なんにも意識せずリクエストを投げるとすぐ文字化けしたソースを見ることになる.

もし id だけを必要とする場合には,この問題は気にならない. 必要な要素が全て英数字記号のみで構成されているため文字化けしないからだ. ネックとなるのは日本語情報をデータとして取得したいときだろう.

当日にならないと得られない情報

shutuba.html では,枠・馬番・馬体重(+オッズ)も得られるが,これらは直前になってから出ないと判明しないデータである. すなわち,(1)前日までにスクレイピングした情報(2)レースの30分前にスクレイピングした情報では情報量が異なる

最も重要と思われる馬体重の発表は,おおよそ発走時刻の一時間前であるようだ. 十分に余裕があるが,一方でこれは何をトリガーにスクレイピングすればよいだろうか……? あくまで目安の時間であるため,きっかり一時間前にリクエストを投げても結果が帰らないかもしれない. 一旦これは保留とする.

(賞味期限が極めて短いデータであるため,GitHub へ保存するよりは,一旦 Firestore 等を経由しておくのが賢いかも……?)

前日までに得られる情報

出走登録している馬の一覧を得ることができる. ここからさらに前日あたりで本登録となり,出走できない馬も出てくる. まぁそれはそれで致し方ないので,出走候補について全て情報を集めておくのが良いだろう.

コードを書く方針としては,すでに開催されたレースについても shutuba.html は見られるのでそれをもとに処理を書き,適宜例外処理を追加していく感じにすれば良いと思う.

horse_id

すべての馬に /, /result, /ped があり,繁殖入りできた場合には牡馬であれば /sire, 牝馬であれば /mare が存在する. 繁殖実績については現状の優先度は高くないので一旦置いておくとして,各馬のプロファイルは逐次集める必要がある.

//result を見比べてみたところ,どちらにも同じ戦績テーブルが配置されていたため,/result へのスクレイピングは省略できそうだ.

/ (トップページ)では,各馬のプロファイルと戦績を取得できる. 後々ページコンテンツを充実させることを考えると,写真 URL も引っこ抜いてくるのがよさそうだ.

/ped については,テーブルの形がだいぶイレギュラーなもので自前ではうまくパースできなかった. 代わりに,pandasread_html を使ってテーブルを引っこ抜き,その構造を利用してうまいこと「世代ごと」にイテレーションできるようにした. つまり,行で回すのではなく,列でループ処理できるようにした. こうすれば,父母,祖父母……の処理が比較的簡単に実装できる.

データ構造

各馬のプロファイル,戦績,血統なんかも GET できたのだからそれをファイルデータとして保存しておきたい. これまではうまく一行に押し込んたうえで,それを生年ごとに連ねて一つの TSV ファイルで管理していた. しかし今後同時並列に複数箇所でデータ更新が走りうることを考えると,ACID 特性を備えていないのは手痛い. (まぁ素直に DB 使えってツッコミはあるが,あくまで GitHub に残すことを中心に考える)

ファイル数が膨大になってしまうのを覚悟で,各馬ごとに一ファイルを割り当てる方針とする. こうすれば,ファイル更新は最小限で済む.

次に問題になるのがフォーマットだが,サイズを考えると TSV が挙げられる. しかし,たかが一行のデータのために TSV を使うのはもったいない(そうするならまとめろという話にもなる). ここはもっと柔軟な表現を持つ構造化データを用いるべきだと考えた. サイズを考慮すると YAML が候補に上がるが,どの言語でも標準的に扱えるかと言うとそうではない. じゃあやっぱり JSON か……とも思ったが,単に JSON を扱うだけでは芸がない,もとい LOD を謳うのに JSON-LD を無視するのはいただけない.

各 ID ごとにファイルを作って管理,かつフォーマットとしては JSON-LD を用いるものとする というのが,今後の基本方針となった.

もちろん Virtuoso ではこのままでは使えないので,ローカルで .ttl へ変換するスクリプトも用意することになる.