見出し画像

【競馬AI開発#2】ChromeDriverによるレースid一覧のスクレイピング

はじめに

この【競馬AI開発】シリーズでは、競馬予想AIを作ることを通して、機械学習・データサイエンスの勉強になるコンテンツの発信や、筆者が行った実験の共有などを行っていきます。

今回の記事は、以下の動画に補足を加えて簡単にまとめたものになります。



動画中の実行環境

・OS: Mac OS 14.2.1
・言語: Python 3.11.4
・エディタ: VSCode 1.87.0

VSCodeやPythonのインストール方法については様々な記事で紹介されているので、適宜参照して設定してください。

また、以下のライブラリを使用しています。

beautifulsoup4==4.12.3
pandas==2.2.1
selenium==4.18.1
tqdm==4.66.1
webdriver_manager==4.0.1

動画中のソースコード

※転載・再配布はお控えください

筆者のプロフィール

東京大学大学院卒業後、データサイエンティストとしてWEBマーケティング調査会社でWEB上の消費者行動ログ分析などを経験。
現在は、大手IT系事業会社で、転職サイトのレコメンドシステムの開発を行っています。

今回やること

前回に引き続き、まずはnetkeiba.comから2023年のレース結果のテーブルをスクレイピングにより取得して、Pythonで扱えるようにすることを目標にしています。

そのためのステップについての全体感は、前回の記事で解説しています。

今回は、開催ページから2023年の全レースid一覧をスクレイピングにより取得します。

開催ページ

スクレイピング手法の選定

前回と同様にスクレイピングをしていきたいのですが、上のページはjavascriptによってページが描画されているため、BeautifulSoupでスクレイピングすることができません

Chrome上で右クリック→「検証」からhtmlタグを辿っていくと、実際に<script>タグが見つかります。

このような場合は、ChromeDriverを使ってスクレイピングします。

ただ、ChromeDriverはBeautifulSoupに比べると動作が重い・使用しているGoogleChromeに挙動が依存するなどのデメリットがあるため、極力BeautifulSoupを使った方が良いです。今回は仕方なく、という感じですね。

ChromeDriverのおすすめ設定方法

そんなChromeDriverですが、何よりgoogle chromeとのバージョン合わせがとても厄介です。
一昔前は、google chromeのバージョンが上がると、すぐにエラーを吐いて使えなくなり、新しいバージョンをインストールする必要がありました。

ただ、便利なライブラリがあって、以下のコードを実行すると、使用しているgoogle chromeのバージョンに合わせたものを自動的にインストールしてくれます。

from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service

driver_path = ChromeDriverManager().install()

このdriver_pathに、インストールしたChromeDriverのパスが入っているので、これを使って以下のように起動することができます。

driver = webdriver.Chrome(service=Service(driver_path))

オプションを指定するには以下のようにします。

from selenium.webdriver.chrome.options import Options

options = Options()
# ヘッドレスモード(バックグラウンド)で起動
options.add_argument("--headless")
# その他のクラッシュ対策
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(service=Service(driver_path), options=options)

動画中で解説されている"--headless"に加えて、"--no-sandbox"と"--disable-dev-shm-usage"オプションが追加されていますが、これらも動作を円滑にするために一般的によく使われるものです。

また、ChromeDriverでは、webページ上の要素を読み込む前に次の処理に進んでしまい、エラーになる時があります。その対策として、ソースコード中では

# 要素を取得できない時、最大10秒待つ
driver.implicitly_wait(10)

の設定を加えています。

レースid取得の流れ

(動画中 9:20〜)

ChromeDriverが設定できたら、開催ページを取得します。

url = "https://race.netkeiba.com/top/race_list.html?kaisai_date=20230105"
driver.get(url)

こうすることで、バックグラウンドでgoogle chromeが起動し、urlで指定されたページを読み込みます。

google chromeで上のURLを開き、一つ目の「3歳未勝利」のリンクにカーソルを合わせ、右クリックから「検証」を押すと、以下のようなHTMLが表示されます。

構造を見ていくと、<li class="RaceList_DataItem …>というタグが、一つのレースに対応していることが分かります。

よって、まずはこのタグ一覧を取得します。

from selenium.webdriver.common.by import By

li_list = driver.find_elements(By.CLASS_NAME, "RaceList_DataItem")
li_list

すると、以下のように各レースの<li>要素が、webelementのリストとして返ってきます。

[<selenium.webdriver.remote.webelement.WebElement ...,
 <selenium.webdriver.remote.webelement.WebElement ...,
 <selenium.webdriver.remote.webelement.WebElement ...,
...

説明のため、先頭の<li>要素を取り出します。

li = li_list[0]

もう一度HTMLを見ると、このタグの中に<a>タグがあり、そこの"race_id="という形でレースidの情報が入っています。

よって、ここからさらに<a>タグに絞り込みます。

li.find_element(By.TAG_NAME, "a")

これが、<a href="…">を表しているので、ここからhref=の部分だけ取り出します。

href = li.find_element(By.TAG_NAME, "a").get_attribute("href")
href

出力:

'https://race.netkeiba.com/race/result.html?race_id=202306010101&rf=race_list'

あとは、前回と同様、正規表現を使ってレースidの部分を取り出します。

import re

re.findall(r"race_id=(\d{12})", href)[0]

出力:

'202306010101'

以上が、レースidを取得する流れになります。

scrape_race_id_list()の作成

(動画中 17:30〜)

レースid取得の流れが分かったので、開催日の一覧をリストで入れると、その開催日のレースid一覧をリストで返してくれるscrape_race_id_list()関数を作成します。

詳しい流れは動画上で解説しているので、そちらをご確認ください。

def scrape_race_id_list(kaisai_date_list: list[str]) -> list[str]:
    """
    開催日(yyyymmdd形式)をリストで入れると、レースid一覧が返ってくる関数。
    """
    options = Options()
    # ヘッドレスモード(バックグラウンド)で起動
    options.add_argument("--headless")
    # その他のクラッシュ対策
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    driver_path = ChromeDriverManager().install()
    race_id_list = []

    with webdriver.Chrome(service=Service(driver_path), options=options) as driver:
        # 要素を取得できない時、最大10秒待つ
        driver.implicitly_wait(10)
        for kaisai_date in tqdm(kaisai_date_list):
            url = f"https://race.netkeiba.com/top/race_list.html?kaisai_date={kaisai_date}"
            try:
                driver.get(url)
                time.sleep(1)
                li_list = driver.find_elements(By.CLASS_NAME, "RaceList_DataItem")
                for li in li_list:
                    href = li.find_element(By.TAG_NAME, "a").get_attribute("href")
                    race_id = re.findall(r"race_id=(\d{12})", href)[0]
                    race_id_list.append(race_id)
            except:
                print(f"stopped at {url}")
                print(traceback.format_exc())
                break
    return race_id_list

この関数と、前回作成したscrape_kaisai_date()を組み合わせることによって、ソースコードのmain.ipynbから以下のように実行すれば、2023年の全てのレースidを取得することができます

import scraping

# 開催日一覧を取得
kaisai_date_list = scraping.scrape_kaisai_date(
    from_="2023-01", to_="2023-12"
)
# レースid一覧を取得
race_id_list = scraping.scrape_race_id_list(kaisai_date_list)

次回は、このrace_id_listに含まれるrace_idのレース結果をスクレイピングし、学習させるデータを作っていきます。

ここから先は

0字
・単品購入するより、マガジンの定期購読がお得です。(全て単品購入した場合の【半額】程度になります。) ・月の途中で入っても、その月に追加された有料記事は全て読めます。 ・「定期購読していない月」に追加された有料記事は読めませんので、面白いと思っていただけましたら、定期購読しておくことをおすすめします。

「競馬予想AIを1から作る」ことを通して、機械学習・データサイエンスの勉強になるコンテンツの発信や、筆者が行った実験の共有などを行っていき…

この記事が気に入ったらサポートをしてみませんか?