はじめに
前回、各レース結果ページからレース結果の情報を取得するプログラムを作成しました。
今回は、レース詳細検索を行って表示された各レース結果ページのURLを自動で取得し、テキストに保存するプログラムを作成しました。
今回のプログラムを実行すると、下記のような結果を得ることができます。
今回と前回に作成したプログラムを連携させることでレース結果を保存したCSVファイルを自動で作成することができるようになります。
競馬データの取得先
以下のページから競馬データを取得します。
プログラムの概要
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のプロトタイプを作って、ブラッシュアップしていきます。
コメント
有益な記事ありがとうございます。
専門学校の授業でこちらのコードを活用させて頂いております。ありがとうございます。
こちらのコードを実行すると、下記のエラーが出たので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’
———————————————————————–
掲載しているプログラムの動作確認をしてみました。
正常に動作していることを確認しました。
掲載しているプログラムの動作概要は下記となります。
[1] chromeのインストールが開始
[2] chromeが自動起動して「https://db.netkeiba.com/?pid=race_search_detail」にアクセス
[3] ページ表示されるまで最大で10秒ほど待つ
[4] chrome上で検索条件を自動入力
[5] 検索結果からレース結果のリンクを取得
[6] 取得したレース結果のリンクを保存
■試していただきたいこと
[2]の処理に時間が掛かっているかもしれません。
[3]の待ち時間を10秒から30秒に変えるとうまくいくかもしれません。
変更箇所は18行目です。
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はバージョンは最新の状態です。
掲載しているプログラムを、こちらの環境で実行したところ「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)
# 競馬データベースを開く
以下のコードに変更はなし
ありがとうございます。両対処後に実行できました。ちなみになのですが、最新版にしたときに原因ってわかりますか?
動作したようで安心しました。
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)」 となります。
冗長な属性を削除した結果、 その属性を参照できなくなったのがエラーの要因、と考えています。
また、本文に動作確認した実行環境のバージョン情報を掲載しました。