ML競馬育成計画

技術ブログ×競馬予測

【Python3】Webクローリング・スクレイピング@競馬(レースにおける馬名と単勝オッズを取得する)

競馬に限らずですが、Webサイトから定期的に情報を収集したい!と思うときって、少なからずあるかと思います。

例えば、店舗の混雑状況を定期的にチェックしたり、ショッピングサイトの値段を定期的に取得して、値段の移り変わりをチェックしたりとか。今回は、そんなWebサイトからの定期的な情報収集の一例として、netkeiba.comさんのサイト上から、特定のレースの各馬の単勝オッズ一覧を取得するスクリプトを作ってみようと思います。

うまく使えば、レース直前にスクリプトを実行して即座に情報を取得出来るので、巨大な集合知である人気順を、予想ファクターに組み込む事が出来るかもしれません。

Webからのデータ収集の基本的な流れ

基本的な仕組みはグーグル先生に聞くと大量に出てきますが、ここではWebサイトからの情報取得には2つのステップが必要だよーということだけ、改めて確認しておきましょう。

①クローリング

Webページのリンクを辿って、Webページをダウンロードするステップです。

スクレイピング

①で取得したデータを解析し、必要な情報を抜き出すステップです。

①と②を繰り返し、Webページから必要な情報を取得します。とはいっても、いきなり完成品を作り始めるほどPythonやクローリングに精通しているわけではないので、まずは最小構成で開発してみて、勘所を掴んでいこうと思います。(本記事では、Python3を使って開発していきます)

クローリング(Webページの取得)

Requestsというライブラリを使ってWebページの内容を取得します。Requestsは「人間の為のHTTP」というキャッチフレーズを売りにしている、使い心地の良いライブラリとのことで、初心者が簡単な情報を取得する今回のようなケースには適してそうです。

まずは、簡単なテストスクリプトで動作検証してみます。

import requests

r = requests.get('http://race.netkeiba.com/?pid=race_old&id=p201710020612')
print(type(r))
print(r.status_code) # 正しく動けば200
r.encoding = r.apparent_encoding # HTTPヘッダからエンコーディングを取得出来ない為、推定されるエンコーディングを取得
print(r.encoding) # EUCJP
print(r.text) # text属性でstr型にデコードしたレスポンスボディを取得する
print(r.content) #content属性でbytes型のレスポンスボディを取得する

実行結果は下記の通りです。レスポンスがちゃんと返ってきて、Webページを取得出来ているのがわかるかと思います。

$ python tmp/requests_test.py
<class 'requests.models.Response'>
200
text/html
EUC-JP

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja" id="html">
<head>
<title>
2017/08/13 小倉 12R 500万下 / レース結果|レース情報(JRA) - netkeiba.com
...

スクレイピング(HTMLの解析)

次に、取得したWebページの中身を解析し、必要な情報を取得します。実現する為のライブラリは色々ありますが、高速に動作するlxmlを使用して、必要なデータを抜き出してみましょう。 先程のスクリプトを少し修正して、レース名を取得します。ソースを見てみると、レース名はh1タグで囲まれているので、cssselectを使って抽出できそうです。

import requests
import lxml.html

r = requests.get('http://race.netkeiba.com/?pid=race_old&id=p201710020612')
r.encoding = r.apparent_encoding # HTTPヘッダからエンコーディングを取得出来ない為、推定されるエンコーディングを取得

# fromstring()関数で文字列(str型・bytes型)をパースする。r.contentはbytes型。
root = lxml.html.fromstring(r.content)
# cssselect()メソッドでCSSセレクタにマッチする要素のリストを取得出来る。今回は1つだけなので、添字[0]で、リストの0番目を選択し、textメソッドで値を取得する。
print(root.cssselect('h1')[0].text)

実行結果は下記の通り。無事にレース名を取得する事が出来ました。

$ python tmp/requests_test.py
3歳上500万下

特定レースの人気順(馬名と単勝オッズ)を取得する

さて、最小構成で上手く動いたので、いざ上記のスクリプトを改修して、他の情報を取得しようとしても、うまく動きません。netkeiba.com画面描画にJavaScriptが使われていることが原因です。そこで、JavaScriptを解釈するクローラーを作成する必要があります。

具体的には、Seleniumというプログラムからブラウザを操作するツールを使って、PhantomJSという画面を持たないヘッドレスブラウザを自動操作する事で、JavaScriptを解釈したクローリングを実現します。

まず最初に、SeleniumとPhantomJSをインストールします。

# macOSの場合のインストール手順
$ pip install selenium
$ brew install phantomjs

Seleniumを使って、オッズ表示画面から馬名単勝オッズの情報を取得するスクリプトを書いてみます。

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import lxml.html

# PhamtomJSのWebDriverオブジェクトを作成する。
driver = webdriver.PhantomJS()

# オッズ表示画面を開く
driver.get('http://race.netkeiba.com/?pid=odds&id=p201701010611')

# 検索結果を表示し、lxmlで解析準備
root = lxml.html.fromstring(driver.page_source)

# 馬名と単勝オッズを取得し表示する
print("馬名:単勝オッズ")
for horse,tan_odds in zip(root.cssselect('.h_name'),root.cssselect('[axis^=oddsDataTan]')):
    print("{}:{}".format(horse.text,tan_odds.text))

単勝オッズを取得する際のcssselectに若干苦戦しましたが、属性の値の最初の文字が〜〜で始まる要素を取得することで単勝オッズの情報を取得しています。("[axis^=oddsDataTan]“の部分)このスクリプトを実行してみると、想定通り、馬名と単勝オッズが取得できました。

$ python selenium_test.py
馬名:単勝オッズ
メートルダール:4.2
ロードクエスト:6.1
ブラックムーン:6.4
・・・

引数を指定して取得対象のレースを指定出来るようにする

上記のスクリプトだと、対象のレースのURLをソースコードにべた書きしているので、違うレースの情報を取得する為には、ソース自体を修正する必要があります。これではあまりにいけてないので、下記の仕様でスクリプトを作り変えてみました。

  • 日付/レース番号/開催場所/レース番号を引数で指定する
  • 特定オプションについては、引数を指定しない場合のデフォルト値を定義する
  • 標準出力モード/プログラム解析モードを切り替えられるようにする

開発したスクリプトは、下記に置いてあります。

nekteiba.comより特定レースの単勝オッズ情報を取得するスクリプト

例えば、2017年9月3日の単勝オッズ情報を取得すると

$ python get_win_odds.py -d 0903 -c 新潟 -n 11
   horse_number          horse_name tan_odds
0            11  アストラエンブレム      3.5
1            13      トーセンバジル      6.0
2            12    マイネルフロスト      8.6
3             2  ルミナスウォリアー      9.5
4             5        ロイカバード      9.6
5             1        タツゴウゲキ     12.0
・・・

こんな感じで表示されます。(ちなみに当時購入した馬券は、2着に残ったカフジプリンスを切り飛ばしていたため大敗)

まとめ

さて、クローラーを使い特定のレースの馬名と単勝オッズを取得する事が出来ました。このスクリプトをうまく使えば、レース直前に人気順を取得して買い目に反映させたり、定期的にスクリプトを実行し情報を取得し、単勝オッズの遷移状態をグラフ化して、オッズの歪みを特定する、といった分析を行うことが出来るかもしれません。