Pythonによるnetkeibaのスクレイピング

競馬Python

競馬ファンなら自作のAIで勝ち馬を予測してみたいと思いますよね。

この記事では、Pythonを用いたnetkeibaのスクレイピング方法についてわかりやすく解説します。

スクレイピングとはインターネット上のデータを自動で取得する行為です。

スクレイピングの方法をPython初心者向けにかなり詳しく解説していますので、上級者には冗長な内容かもしれません。

また、競馬データの解析ではPythonのプログラミングスキルが必須になります。

Pythonの基本が完全には身についていない方は、以下の本で勉強するのがおすすめです。

特に、Pythonを使って機械学習の理論について学びたい方はこちらの本がおすすめです。

ライブラリのインストール

PythonのライブラリはrequestsとBeautifulSoupを使用します。

最初にこれらのライブラリをインストールします。

windowsの場合はコマンドプロンプト、macの場合はターミナルに以下のように入力してEnterを押します。

pip3 install requests

以下のように表示されれば成功です。

同様の操作で必要な下記ライブラリもインストールしてください。

pip3 install beautifulsoup4

スポンサーリンク

HTMLについて

実際にnetkeibaからデータを抽出してみましょう。

下記のサイトをGoogle Chromeで開いて下さい。

https://db.netkeiba.com/race/202003010302

試しに「ワイドの払い戻し」を抽出してみましょう。

サイトの下の方に払い戻し一覧があります。

この中から下図の赤線で囲ったワイドの払い戻し情報をPythonを使って取り出します。

windows場合は「control + shift + i」、macの場合は「command + option + i」を押して下さい。すると、以下のような画面が立ち上がります。

これはデベロッパーツールと呼ばれるものです。

デベロッパーツールを開くことで、サイトの中身に関する情報が詰まったHTMLを見ることができます。

HTML上で「command + f」を押すと、以下のような検索バーが出でくるので、ここに「ワイド」と入力してEnterを押します。

すると下図のようにワイドに関するHTMLを見ることができます。

この「1-11」ですとか、「”560″」といった情報をPythonで取得していきます。

ここでHTMLの基礎知識について触れておきましょう。

HTMLとはサイトを文字で記述したものです。

重要な点は、HTMLは「要素」で構成されており、それぞれの要素は「属性」を含んでいると言うことです。

先程の例で言うと、要素と属性は下図のように対応しています。

要素は「<th>」のような開始タグと「</th>」のような終了タグで囲まれています。またこの場合の要素名は「th」となります。

属性は開始タグの中に「class = “wide”」のように記述されます。この場合、「class属性が”wide”」となります。

Pythonでは要素や属性をもとに情報を抽出します。

例えば先程の「”560″」を取得することを考えてみましょう。下図のように要素名が「td」でclass属性が「”txt_r”」の要素を取得すればよいということになります。

具体的なコードは下記です。

スポンサーリンク

Pythonを開いてFileからNew Fileを生成します。

エディタが開くので、上記サンプルコードをコピペして下さい。

「Run」の中の「Run Module」を押すことでコードを実行します。

先程のコードでは、「soup.find_all(“td”,class_=’txt_r’)」の部分で、要素名が「td」でclass属性が「”txt_r”」の要素を取得しています。

つまり、「find_all」の第1引数に要素名を指定し、第2引数に属性を指定することで、HTMLから要素名と属性が一致した要素を全て抽出してくれます。

実行すると以下のような結果になります。

「soup.find_all(“td”,class_=’txt_r’)」では、要素名が「td」でclass属性が「”txt_r”」の要素が全て抽出するため、たくさんの候補が表示されています。

そして、上図の赤で囲ったように確かに「”560″」を抽出できています。

スポンサーリンク

ですが、これだと流石に候補が多すぎるので、別の要素名や属性で抽出したいところです。

ワイド周辺の要素を見てみると、下図赤線で示すような”良さげ”な要素がありました。

(払い戻しという単語はサイト内それほど使われていなさそうな単語なので、候補を少なくできそうという意味での”良さげ”です)

上図の赤で囲った部分は、要素名が「table」でsammary属性が「”払い戻し” 」の要素です。これをPythonでを抽出しましょう。

この要素名が「table」でsammary属性が「”払い戻し” 」の要素を抽出するコードは「soup.find_all(“table”,summary=’払い戻し’)」です。実行すると以下のように出力されます。

先ほどよりもわかりやすく抽出できていますね。

ひとまず、インターネット上からPythonに情報を取り出す流れが掴めたでしょうか?

次から、さらに詳しく情報を取り出していきます。

スポンサーリンク

欲しい情報をスクレイピングする

先程、ざっくりとワイドの払い戻し情報を含んだデータを取得できました。ここからさらに情報を選別して「ワイドの払い戻し」を取得してみましょう。

まず初めに、Pythonでは型を意識することが重要であるということを強調しておきます。

今回で言うと、「soup.find_all()」を使うと「bs4.element.ResultSet」という型でデータを返すということを覚えておいてください。

この「bs4.element.ResultSet」とは「bs4.element.Tag」型を要素とするリストみたいなイメージです。

「bs4.element.ResultSet」と「bs4.element.Tag」は型が異なるので、当然使えるメソッドも異なります。

「bs4.element.ResultSet」と「bs4.element.Tag」の違いを意識してコードを書かないとエラーを吐きまくるので注意しましょう。

さて、いよいよワイドの払い戻しを抽出します

先述の「soup.find_all(“table”,summary=’払い戻し’)」は「bs4.element.ResultSet」型です。

これをリストと同じような操作で、「soup.find_all(“table”,summary=’払い戻し’)[1]」とすると「bs4.element.Tag」型を返します。

そして「bs4.element.Tag」型に対しては「.contents」というメソッドを使用可能で、「bs4.element.Tag」型を「list」型に変換できます。

この変換後のlistの要素が「bs4.element.Tag」型になっていて、、、、といった具合に「bs4.element.Tag」型と「list」型 を行き来しながら欲しいデータに辿り着きましょう。

さて、今回の目的はワイドの払い戻しを取得することでした。実際にやってみてください。といっても難しいと思いますので、答えは以下です。

ポイントは「型を意識すること」に尽きます。型の確認は「type()」を使います。

このような流れで欲しい情報を抽出します。

スポンサーリンク

csvファイルに保存する

ここまでで欲しい情報をインターネット上から取り出す方法を解説しました。

次に、取得した情報を使いやすい形で保存してみましょう。

まず、ワイドだけでは味気ないので、単勝や複勝、三連単といった払い戻しも抽出してみましょう。

再びPythonを開いてFileからNew Fileを生成します。

エディタが開くので、そこに下記のコードをコピペして下さい。

defの部分で関数を作成していますが、コードを短くする以外特に効果はないです。

下記コードでは、「beautifulSoup」で取ってきた情報を「contents」を用いてさらに抽出して、「payBackList」に保存していきます。

「Run」の中の「Run Module」を押すことでコードを実行します。下図のようにIDLE Shellが立ち上がります。

うまくいったかを確認してみましょう。下図のように「payBackList」と打ち込んでEnterを押します。以下のように表示されれば成功です。

「payBackList」の中に馬番と払い戻し情報が規則的に格納されてますね。

ついでに馬名や騎手の情報もまとめてみましょう。筆者が作成したコードを後で載せます。

スポンサーリンク

欲しい情報を取り終えたら、csvファイルに保存してみましょう。

まず、csvファイルとして保存するためにライブラリ「csv」をインポートします。

「csv」はデフォルトで入っているライブラリであるため、pip3でインストールする必要はありません。

以下のコードを実行すると、指定した場所に競馬データをcsvファイルとして保存します。

ただし、101行目のwith open()の第1引数の部分にご自身の保存先のパスとファイル名を記述してください。

パス名の調べ方はwindowsとmacで異なります。windowsの場合は、「Shift」を押しながらパス名を調べたいファイルを右クリックして「パスのコピー」をクリックするとパス名をコピーできます。macの場合は、「option」を押しながらパス名を調べたいファイルを右クリックして「◯◯◯◯のパス名をコピー」をクリックするとパス名をコピーできます。

(上記コードは2022年1月5日時点で有効であることを確認しました。)

うまくいっていればご自身でパスを設定した場所にcsvファイルが出力されます。

スポンサーリンク

csvファイルを読み込む

先程保存したcsvファイルを読み込んでみましょう。

csvファイルの読み込みにはpandasというライブラリを使用します。

いつも通り、ターミナルに「pip3 install pandas」と入力してインストールしましょう。

次に、下図のように「import pandas」と記述してEnterを押して下さい。

その後、下図のように「pandas.read_csv(‘/XXXX/XXX.csv’,encoding=”SHIFT-JIS”,header=None)」と入力してEnterを押します。

ここで、「’/XXXX/XXX.csv’」の部分にはご自身のパスとファイル名を記述してください。(先程csvを生成するときに使ったものをコピペでOKです。)

以下のように出力されれば成功です。

きちんと1レース分の競馬データを保存できていますね。

この操作を繰り返すことで競馬データを収集できます。

しかしながら、毎年開催されている約3000レースを毎回URLを書き直してスクレイピングするのは現実的ではないですよね、、、、

そこでfor構文を使うことで1年分のレース結果をまとめてスクレイピングする方がよさそうです。

スポンサーリンク

1年分をまとめてスクレイピング

netkeibaの1年分のレース結果をスクレイピングするためには、netkeibaのURLの仕組みを理解する必要があります。

netkeibaではURLの最後に12桁の数字がついており、これが特定のレースに対応しています。

具体的には以下のような対応になっています。

最初の4つの数字が西暦、次の2つの数字が競馬場、次の2つの数字が第何回開催か、次の2つの数字が開催何日目か、最後の2つの数字が何レースかに対応しています。

特に競馬場と数字の対応は以下の通りです。

01:札幌、02:函館、03:福島、04:新潟、05:東京、06:中山、07:中京、08:京都、09:阪神、10:小倉

この数字をPythonのFor構文でループさせることで1年分のレースデータをまとめてスクレイピングします。

サンプルコードは以下になります。

上記コードは2022年1月5日時点で有効であることを確認しました。

上記コードの使い方を説明いたします。

まずPythonを起動して、FileからNew Fileでエディタを開いて、上記コードをコピペして下さい。

次に5行目の「#西暦を入力」の部分で西暦を指定してください。例えば「year = “2013”」とすると、2013年のレース結果をまとめて取得できます。

また150行目で保存先のパスを指定してください。サンプルのままですと、Usersの下に2020.csvが生成されます。

最後に「Run」から「Run Module」をクリックするとでコードを実行します。

28から36行目の「w、z、y、x」がそれぞれ、「競馬場、開催、日、レース」に対応しています。これらをfor構文で更新しています。

進捗を表示するために142行目で、第何回開催、何競馬場、何日目、何レース目をスクレイピングしたかを表示するように指定しています。

またコードの途中で「try」と「Except」が使用していますが、これはエラー対策に有効です。

筆者の環境ですと、2022年1月時点では上記コードでスクレイピングできますが、環境やバージョンの更新でバグが出るかもしれません。

その際はきちんと原因を突き止めて、「try、except」などを使いつつデバッグしてください。

成功していたら、指定したパスにcsvファイルが出力されます。

試しにkeynoteなどでで開いてみてください、以下のようにデータが入っていれば成功です。

以上でnetkeibaからレース結果をスクレイピングする方法の解説を終了します。

長い間、お疲れ様でした。

2022年6月15日追記
上記コードでバグが出ると言う報告がありました。
2022年でしか機能することを確かめていませんが、以下のコードも試してみてください。

スポンサーリンク

なお上記コードは中央競馬でのみ使用できます。

地方競馬をスクレイピングする方法はこちらの記事で解説しています。

また、Twitterで競馬データ解析についてツイートをしていますので、フォローしていただけると嬉しいです。

Comments

  1. うる より:

    はじめまして、非常に有益な情報を公開してくださったことに感謝致します。質問があるのですが、2005-2008年の三連単がない場合の処理はどのようにしておられますでしょうか?(try:, except: pass)でcsvファイルを作成したのですがデータ整形時に、( for for_lists in paybackList:
    var_list.append(for_lists[for_races]))の部分でインデックスエラーが出ました。代わりに何かの値を入れているのでしょうか?ご教授していただければ幸いです。

    • 管理人 より:

      三連単がない時は、三連複や馬連の値を適当に入れています、、、、
      ちなみに馬単がない場合も、そんな感じで適当に処理しています、、、、
      最初に書いたコードをtryでデバグしていくうちに、三連単などの処理が適当になってしまいました。
      htmlを取得した後に、ifを使って「三連単というワードがあるか」で分岐処理をすれば解決できるとは考えています。

      • うる より:

        なるほど、、
        分岐処理をやってみて、わからなければ適当に入れようと思います
        とても有益な情報を公開していただきありがとうございます!

  2. まこちゃん より:

    有益な情報ありがとうございます。

    レース情報のところでうまくいかないのでsoup_span[8]をsoup_span[6]に変更すると
    実行はできるものの「list index out of range」となってしまいます。
    試行錯誤したもののうまく抜き取れませんでした。
    どのようにしたらいいでしょうか。
    よろしくお願いします。

    • 管理人 より:

      コメントありがとうございます。
      もしかしたらnetkeibaの仕様が変わったのかもしれません。
      2022年でしか検証していないですが、コードを追記したのでそちらもお試しください。
      今後ともよろしくお願いいたします。

  3. まこちゃん より:

    返信ありがとうございます。

    修正されました通りのこーどを実行したところ問題なく出来てました。

    2012年のやつですが出力までされてました。

  4. ナイナー より:

    始めまして
    スクレイピングの勉強がてらこちらのサイトを見させてもらっていたのですが、sampleコードをPythonのIDEsellからmodulerunすると、処理が終わらないままになってしまいます。
    これは環境の問題なのでしょうか。

    • 管理人 より:

      環境の問題かもしれないですね、、、
      私はjupyter labを使っているので、そちらなら動くかもしれないです。
      また一般論として、デバグの際はprint()で色々出力しまくるといいと思います。
      ご参考になりましたら幸いです。

  5. japanman より:

    お疲れ様です。有益な情報ありがとうございます!

    追記されたソースコードをざっと見てたら、ちょっとしたバグっぽいのを見つけたので念のため報告しておきます。Python初心者なので間違ってたら無視してください!

    大した内容ではないのですが、端的に言うと177行目のif yBreakCounter == 12:のルートに入らないんじゃないかというものです。
    36行目で0~12の数値でループしているため、1R~13Rまで叩かれておりyBreakCounterが最大13までカウントアップされてしまいます。
    そのためxのforを抜けた後if文の中に入らずそのままyのループが継続していて、想定よりも余分にループが回っているかと思います。

    今年のデータだと実行時間に顕著な差が出そうな気がしてます。

    細かいことで申し訳ありません。
    これから色々参考に勉強させていただきますmm

    • 管理人 より:

      コメントありがとうございます!!
      for n in range(12):
      print(n)
      を試すと0,1,2,,,,11が出力されるので、yBreakCounterが最大は12であってるように思います。
      もしyBreakCounterが機能していないようでしたら、なにか他の場所にバグがあると思われます、、、
      またお気づきの点がありました、らお教えいただけますとますかります!!

      • japanman より:

        なるほど。見直してみたら修正前のソースだとrange(12)になってるので問題なかったんですね。
        2022年6月15日追記の方ではrangeに13が入っていたことで、うまく機能していなかったみたいですmm

  6. pyてょnなんもわからん より:

    お世話になっております。
    バグというよりかは高速化のためのアイデアを共有させていただきたいのですが、
    現在の実装では存在しない日程のレースもforループで回し、そこでもサーバーの負荷を減らすため1秒待つので、非常に遅いプログラムになっています。
    そこで
    if allnum < 1:の行に新しく2つの変数を追加して
    yループ,zループで抜ける判定を加えてはどうでしょうか。

    具体的には

    for w in range(len(l)):
    z_loop_judge=0
    for z in range(8):#開催、6まで十分だけど保険で7
    y_loop_judge=0
    if z_loop_judge==2:
    break
    for y in range(16):#日、12まで十分だけど保険で14
    if y_loop_judge:
    break
    else:
    z_loop_judge=0

    *********

    if allnum < 1:#urlにデータがあるか判定
    yBreakCounter+=1
    y_loop_judge=1
    if z_loop_judge==1:
    z_loop_judge=2
    print("break")
    break

    で速くなるように思います。
    これで意図したとおりに動くでしょうか。

    • 管理人 より:

      コメントありがとうございます!
      確かに良いアイデアかもしれないですね。
      試してみた方がいれば、動くかどうか教えていただけると助かります!!

  7. python-AI勉強中 より:

    始めまして、非常に有益な情報ありがとうございます。

    こちらのサイトを参考に競馬AIをひとまず学習させることまではできたのですが、
    実践テストをしようとしたところで、データベースにない未実施のレース情報を
    取得してAIの予測を実行することができないことに気づきました。

    netkeibaの出馬表からデータを持ってくることも検討したのですが、
    データのフォーマットが大きく異なるようでまだうまくできていません。
    もし参考になるサンプルコードがあれば、共有いただくことは可能でしょうか。

  8. Python初心者 より:

    2回札幌8日目12Rで毎回フリーズしてしまうのですが何が原因なのでしょうか?

  9. py初心者勉強中 より:

    お世話になっております。

    データをスクレイピングしようとしているのですが、フリーズしてしまい1日経っても終わりません

    もし可能であれば、10年分のCSVデータを送ってもらえますでしょうか

  10. ななっし より:

    有益な情報ありがとうございます。
    初心者なので、コードの練習をしながら進めていっているのですが、
    下記のエラーがでます。
    jupiter labを使用しているのですが、何が問題なのでしょうか?
    しょうもない質問で申し訳ございません。
    宜しくお願い致します。

    IndexError Traceback (most recent call last)
    Cell In [10], line 118
    116 rou=str(soup_span[8]).split(“/”)[0].split(“>”)[1][1]
    117 dis=str(soup_span[8]).split(“/”)[0].split(“>”)[1].split(“m”)[0][-4:]
    –> 118 con=str(soup_span[8]).split(“/”)[2].split(“:”)[1][1]
    119 wed=str(soup_span[8]).split(“/”)[1].split(“:”)[1][1]
    120 soup_smalltxt = soup.find_all(“p”,class_=”smalltxt”)

    IndexError: list index out of range

    • 管理人 より:

      一般論として、デバクの際は「中身を出力する」が基本になります。
      まず「soup_span」の中身を見て、次に「str(soup_span[8]).split(“/”)」の中身を見ることをおすすめします。
      (おそらくnetkeibaのhtmlが新しくなったため、私の書いたコードでバグが出ているのだと思われます。)