競馬予想AIの作成②(競馬データの取得②)

機械学習

はじめに

前回、各レース結果ページからレース結果の情報を取得するプログラムを作成しました。
今回は、レース詳細検索を行って表示された各レース結果ページのURLを自動で取得し、テキストに保存するプログラムを作成しました。
今回のプログラムを実行すると、下記のような結果を得ることができます。

今回と前回に作成したプログラムを連携させることでレース結果を保存したCSVファイルを自動で作成することができるようになります。

競馬データの取得先

以下のページから競馬データを取得します。

netkeiba.com – 国内最大級の競馬情報サイト

プログラムの概要

Webブラウザの操作はseleniumuを使用し、表示されているWebページの解析はBeautifulSoupを用いています。

該当行 概要
1~7 ライブラリの読み込み
9~43 メイン処理。各関数を呼び出してレース結果ページのURLをテキストに保存する
46~87 レース詳細検索で検索条件を指定して検索する
90~122 レース詳細検索の結果から各レース結果のURLを抽出する
125~152 レース詳細検索の結果で「次」がある場合、「次」をクリックする

プログラムの実行環境

実行環境の主なパッケージのバージョンは下記となります。
seleniumのバージョンは4.11.0以上の場合、エラーが発生する場合があります。

パッケージ バージョン
python 3.10.9
selenium 4.7.2
webdriver_manager 4.0.0

事前準備

SeleminumWebdriverのバージョンとchromeのバージョンを合わせるのが手間だったので、適切なchromeを実行時にインストールすることができる「webdriver_manager.chrome」を利用しています。下記のpipコマンドでインストールできます。

# pip install webdriver_manager

作成したプログラム

# ライブラリの読み込み
from webdriver_manager.chrome import ChromeDriverManager
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select
from bs4 import BeautifulSoup
import urllib

# メイン
def main():
    # webdriver_managerで最適なchromeのバージョンをインストールして設定する
    browser = webdriver.Chrome(ChromeDriverManager().install())

    # 競馬データベースを開く
    browser.get('https://db.netkeiba.com/?pid=race_search_detail')

    # 指定した要素が見つかるまでの待ち時間を10秒と設定する
    browser.implicitly_wait(10)

    # 検索条件を設定して検索する
    search_rase(browser)

    # リンクページのURLリスト
    link_list = []

    no_click_next = 0
    count_click_next = 0
    while no_click_next == 0:
        # レース結果のリンクページのURLを取得する
        link_list = make_raseURL(browser, link_list)

        # 「次」をクリックし、「次」の有無とクリックした回数を返す
        no_click_next, count_click_next = click_next(browser, count_click_next)

    # レース結果のリンクページにアクセスして、レース結果を取得する
    # 日本レース以外のレース結果は取得しない
    # 日本以外のレースのリンクページには、/race/2022H1a00108/のようにアルファベットが含まれている
    # isdecimalで文字列が数字のみかを判定している
    for url in link_list:
        if url.split('/')[-2].isdecimal():
            with open('race_URL.txt', 'a') as f:
                f.write(url + '\n')


# 検索条件を設定して検索する
def search_rase(browser):
    print('検索条件を設定します')
    # 競争種別で「芝」と「ダート」にチェックを入れる
    elem_check_track_1 = browser.find_element(By.ID, value='check_track_1')
    elem_check_track_2 = browser.find_element(By.ID, value='check_track_2')

    elem_check_track_1.click()
    elem_check_track_2.click()

    # 期間を2010年から2022年に設定する
    elem_start_year = browser.find_element(By.NAME, value='start_year')
    elem_start_year_select = Select(elem_start_year)
    elem_start_year_select.select_by_value('2022')

    # 月を指定する場合は、select_by_valueで月数を指定する
    elem_start_month = browser.find_element(By.NAME, value='start_mon')
    elem_start_month_select = Select(elem_start_month)
    elem_start_month_select.select_by_value('1')

    elem_end_year = browser.find_element(By.NAME, value='end_year')
    elem_end_year_select = Select(elem_end_year)
    elem_end_year_select.select_by_value('2022')

    # 月を指定する場合は、select_by_valueで月数を指定する
    elem_end_month = browser.find_element(By.NAME, value='end_mon')
    elem_end_month_select = Select(elem_end_month)
    elem_end_month_select.select_by_value('2')

    # 画面を下にスクロールする
    # browser.execute_script('window.scrollTo(0, 400);')

    # 表示件数を100件にする
    elem_list = browser.find_element(By.NAME, value='list')
    elem_list_select = Select(elem_list)
    elem_list_select.select_by_value('100')

    # 検索をクリック(submit)する
    elem_search = browser.find_element(By.CLASS_NAME, value='search_detail_submit')
    elem_search.submit()

    print('検索を行います')


# 取得したHTMLからレース結果のURLを抽出し、URLリストを作成する
def make_raseURL(browser, link_list):
    print('取得したHTMLからレース結果のURLを抽出します')
    html = browser.page_source.encode('utf-8')      # UTF-8でHTMLを取得する
    soup = BeautifulSoup(html, 'html.parser')       # 検索結果をbeautifulSoupで読み込む
    table_data = soup.find(class_='nk_tb_common')   # 検索結果のテーブルを取得する

    for element in table_data.find_all('a'):
        url = element.get('href')   # リンクページを取得する

        # リンクページがjavascriptの場合は、次のリンクページの処理に移る
        if 'javascript' in url:
            continue

        # リンクページの絶対URLを作成する
        link_url = urllib.parse.urljoin('https://db.netkeiba.com', url)

        # レース結果のみを抽出する
        # レース結果のURLは'https://db.netkeiba.com/rase/yyyyXXmmddXX'となる
        # 馬情報のURLは'https://db.netkeiba.com/horse/yyyymmddXXXX'
        # 騎手情報のURLは'https://db.netkeiba.com/jockey/result/recent/'
        # レース結果以外のリンクページを除外するための単語リストを用意する
        word_list = ['horse', 'jockey', 'result', 'sum', 'list', 'movie']

        tmp_list = link_url.split('/') # リンクページのURLを'/'で分割する
        and_list = set(word_list) & set(tmp_list) # word_listとtmp_listを比較し、一致している単語を抽出する

        # 一致している単語が0のリンクページのURLのリストを作成する
        if len(and_list) == 0:
            link_list.append(link_url)

    print('URLを抽出しました')
    return link_list


# 画面下にスクロールして「次」をクリックする
def click_next(browser, count_click_next):
    # 画面を下にスクロールする
    browser.execute_script('window.scrollTo(0, 2500);')

    # 次をクリックする
    # 検索1ページ目のxpathは2ページ以降とは異なるため、count_click_nextで
    # 検索1ページ目なのかを判定している
    if count_click_next == 0:
        # xpath = '//*[@id="contents_liquid"]/div[2]/a' # 20230308 サイト変更に伴いxpathの指定を変更
        xpath = '//*[@id="contents_liquid"]/div[2]/ul[1]/li[14]/a/span/span'
        elem_search = browser.find_element(By.XPATH, value=xpath)
        elem_search.click()
        no_click_next = 0
    else:
        # 検索最後のページで次をクリックしようとすると例外処理が発生する
        # exceptで例外処理を取得し、no_click_nextに1を代入する
        try:
            # xpath = '//*[@id="contents_liquid"]/div[2]/a[2]' # 20230308 サイト変更に伴いxpathの指定を変更
            xpath = '//*[@id="contents_liquid"]/div[2]/ul[1]/li[14]/a/span/span'
            elem_search = browser.find_element(By.XPATH, value=xpath)
            elem_search.click()
            no_click_next = 0
        except:
            print('次のページ無し')
            no_click_next = 1

    count_click_next += 1 # ページ数を判別するためのフラグに1を加算する

    return no_click_next, count_click_next


if __name__ == "__main__":
    main()

プログラムの実行結果

プログラム実行のカレントフォルダに「race_URL.txt」が作成されます。
また、標準出力では下記のメッセージが表示されます。

====== WebDriver manager ======
Current google-chrome version is 102.0.5005
Get LATEST chromedriver version for 102.0.5005 google-chrome
Driver [C:\Users\xxxxx\.wdm\drivers\chromedriver\win32\102.0.5005.61\chromedriver.exe] found in cache
C:\Users\xxxxx\Google ドライブ\Web_Scraping\collect_HorseRace_ResultURL.py:12: DeprecationWarning: executable_path has been deprecated, please pass in a Service object
  browser = webdriver.Chrome(ChromeDriverManager().install())
検索条件を設定します
検索を行います
取得したHTMLからレース結果のURLを抽出します
URLを抽出しました
取得したHTMLからレース結果のURLを抽出します
URLを抽出しました
~中略~
取得したHTMLからレース結果のURLを抽出します
URLを抽出しました
取得したHTMLからレース結果のURLを抽出します
URLを抽出しました
次のページ無し

プログラム作成時にハマったところ

「Message: element click intercepted」で「次」をクリックできない

下記のメッセージが表示されてクリックが実行できませんでした。

selenium.common.exceptions.ElementClickInterceptedException: Message: element click intercepted: Element

「次」の画面上にGoogleアドセンスの広告が表示されていたため、クリックできないようでした。
下記のコードを実行して、chromeの画面をスクロールして「次」とGoogleアドセンスの広告が重ならないようにする必要がありました。

browser.execute_script('window.scrollTo(0, 2500);')

「ActionChains」で「次」をクリックできない

プログラム作成の当初は、「次」の見つけてクリックする動作をActionChainsで実施する予定でした。

elem_tmp = browser.find_elements(By.CLASS_NAME, value='pager')
elem_next = elem_tmp[0].find_element(By.TAG_NAME, value='a')
loc_next = elem_next.location
x, y = loc_next['x'], loc_next['y']

actions.move_by_offset(x, y)
actions.click()
actions.perform()

検索結果の1ページ目では、想定通りに「次」をクリックできましたが、2ページ目以降で「次」を見つけることができませんでした。
試行錯誤することを考えたのですが、データ取得が目的だったので、シンプルにxpathで「次」を指定してクリックすることにしました。

次回、試すこと

前回と今回で作成したプログラムを連結して、指定期間の競馬結果のデータを取得します。

作成するAIで何を予測するのかも検討を始めます。
取得する競馬結果のデータには、タイムが含まれているので、タイムを予測するのが筋なのかな、と考えてます。
できるだけ早くAIのプロトタイプを作って、ブラッシュアップしていきます。

コメント

  1. tty より:

    有益な記事ありがとうございます。
    専門学校の授業でこちらのコードを活用させて頂いております。ありがとうございます。
    こちらのコードを実行すると、下記のエラーが出たのでclass名が変わったのかなと思ったのでデベロッパーツールで確認したところ『nk_tb_common race_table_01』というクラスに変わっていました。
    その後、94行目をtable_data = soup.find(class_=’nk_tb_common race_table_01′) と変更して実行してみました。
    しかし、下記のエラーが再度でました。コードの一部を変えてみたりしたのですが、結局下記のエラーが主に出てきました。
    宜しければお助けいただけないでしょうか。
    返信お待ちしております。

    ———————————————————————–
    browser = webdriver.Chrome(ChromeDriverManager().install())
    検索条件を設定します
    検索を行います
    取得したHTMLからレース結果のURLを抽出します
    Traceback (most recent call last):
    File “/Users//Desktop/memo.txt/keiba.py”, line 155, in
    main()
    File “/Users//Desktop/memo.txt/keiba.py”, line 30, in main
    link_list = make_raseURL(browser, link_list)
    File “/Users//Desktop/memo.txt/keiba.py”, line 96, in make_raseURL
    for element in table_data.find_all(‘a’):
    AttributeError: ‘NoneType’ object has no attribute ‘find_all’
    ———————————————————————–

    • sakurater より:

      掲載しているプログラムの動作確認をしてみました。
      正常に動作していることを確認しました。

      掲載しているプログラムの動作概要は下記となります。
      [1] chromeのインストールが開始
      [2] chromeが自動起動して「https://db.netkeiba.com/?pid=race_search_detail」にアクセス
      [3] ページ表示されるまで最大で10秒ほど待つ
      [4] chrome上で検索条件を自動入力
      [5] 検索結果からレース結果のリンクを取得
      [6] 取得したレース結果のリンクを保存

      ■試していただきたいこと
      [2]の処理に時間が掛かっているかもしれません。
      [3]の待ち時間を10秒から30秒に変えるとうまくいくかもしれません。
      変更箇所は18行目です。

  2. mr より:

    python初心者です。

    ^^^^^^^^^^^^^^^^^^^^
    AttributeError: ‘str’ object has no attribute ‘capabilities’msg = f”Unable to obtain driver for {options.capabilities[‘browserName’]} using Selenium Manager.”
    ^^^^^^^^^^^^^^^^^^^^
    AttributeError: ‘str’ object has no attribute ‘capabilities’

    上記のようなエラーが発生したのですが、対処はどのようにすればよろしいでしょうか?

    seleniumやwebdriver_managerはバージョンは最新の状態です。

    • sakurater より:

      掲載しているプログラムを、こちらの環境で実行したところ「race_URL.txt」を生成できました。
      「seleniumやwebdriver_managerは最新バージョン」とのことなので、こちらの実行環境とseleniumとwebdriver_managerのバージョンが異なっているため、webdriver_managerの挙動が変わっている可能性があります。
      対処案を2つ考えました。

      ■対処案1.seleniumとwebdriver_managerのバージョンを変更する
      こちらの実行環境のバージョンは下記でした。
      実行環境のバージョンを合わせて実行してみてください。
      selenium:4.7.2
      webdriver_manager:4.0.0

      ■対処案2.optionsを設定する
      エラーの内容を見るに、WebDriverの初期化に関連していそうです。
      「options」は文字列strであり、”capabilities”属性を持たないのでエラーが出ています。
      WebDriverを初期化するとき、オプションを明示的に設定して実行し、エラーを回避できるか試してみてください。

      オプションを明示的にしたコードは下記となります。


      # メイン
      def main():
      # WebDriverのオプションを設定
      options = webdriver.ChromeOptions()
      options.add_argument('--headless')

      # webdriver_managerで最適なchromeのバージョンをインストールして設定する
      browser = webdriver.Chrome(ChromeDriverManager().install(), options=options)

      # 競馬データベースを開く
      以下のコードに変更はなし

  3. mr より:

    ありがとうございます。両対処後に実行できました。ちなみになのですが、最新版にしたときに原因ってわかりますか?

    • sakurater より:

      動作したようで安心しました。
      seleniumとwebdriver_managerのリリースノートを確認してみました。
      詳細は不明ですが、 seleniumの4.11.0以降を使用すると、エラーになる可能性がありそうです。
      seleniumの4.11.0のリリースノートに下記の内容が記載されていました。
      「* removed redundant attributes `capabilities’ and ‘set_capability` in wpewebkit/options.py (#12169)」
      今回のエラーで表示されたキーワード 「options」 「capabilities」 が含まれています。 翻訳すると、
      「* wpewebkit/ options.py の冗長な属性 “capabilities と `set_capability を削除しました (#12169)」 となります。
      冗長な属性を削除した結果、 その属性を参照できなくなったのがエラーの要因、と考えています。

      また、本文に動作確認した実行環境のバージョン情報を掲載しました。

タイトルとURLをコピーしました