Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

Pythonを使ったスクレイピング

解決したいこと

Web上に無料で公開されているソースコードを使い、スクレイピングを行っております。
しかしエラーが解決できずに困っております。解決方法を教えてください。

スクレイピング先URL:https://db.netkeiba.com/

使用言語:Python

発生している問題・エラー

Traceback (most recent call last):
  File "d:\Python\hellogood.py", line 238, in <module>
    main()
  File "d:\Python\hellogood.py", line 46, in main
    df_RaceResult = pd.concat([df_RaceResult, output_RaceResult(url)], axis=0)
                                              ^^^^^^^^^^^^^^^^^^^^^^
  File "d:\Python\hellogood.py", line 184, in output_RaceResult
    tables = tables.find_all('tr')
             ^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'find_all'

該当するソースコード

# ライブラリの読み込み
import pandas as pd
import urllib
import requests
import re

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

# プログレスバーを表示するためのライブラリを読み込む
from tqdm import tqdm

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

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

    browser.implicitly_wait(10) # 指定した要素が見つかるまでの待ち時間を10秒と設定する
    search_rase(browser)        # 検索条件を設定して検索する
    link_list = []              # リンクページのURLリスト

    no_click_next = 0           # 0:次のページ無し、1:次のページ有り
    count_click_next = 0        # 0:検索結果の1ページ目、1以上:検索結果の2ページ目以上
    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)

    df_RaceResult = pd.DataFrame()

    # レース結果のリンクページにアクセスして、レース結果を取得する
    # 日本レース以外のレース結果は取得しない
    # 日本以外のレースのリンクページには、/race/2022H1a00108/のようにアルファベットが含まれている
    # isdecimalで文字列が数字のみかを判定している
    print('レース結果の詳細を取得します')
    for url in tqdm(link_list):
    #     if url.split('/')[-2].isdecimal():
             df_RaceResult = pd.concat([df_RaceResult, output_RaceResult(url)], axis=0)

    # CSVにレース結果を保存する
    df_RaceResult.to_csv('./レース結果.csv', encoding='cp932', header=False, index=False, errors="ignore")
    
# 検索条件を設定して検索する
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('検索を行います')

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'
        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]'
            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

# レース結果の詳細を取得する
def output_RaceResult(url):
    res = requests.get(url) # 指定したURLからデータを取得する
    soup = BeautifulSoup(res.content, 'html.parser') # content形式で取得したデータをhtml形式で分割する

    # レース名を取得する
    race_name = soup.find_all('h1')
    race_name = race_name[1].text

    # 開催日、開催場所を取得する
    race_base_info = soup.find('p', attrs={'class': 'smalltxt'})
    rase_base_info_text = race_base_info.text.replace(u'\xa0', u' ')
    words = rase_base_info_text.split(' ')
    race_date = words[0]
    race_place = words[1]

    # レース情報を取得する
    race_info = soup.find('diary_snap_cut')
    race_info_text = race_info.text.replace(u'\xa0', u' ')
    race_info_text = race_info.text.replace(u'\xa5', u' ')
    words = race_info_text.split('/')

    race_info_distance = int(re.sub(r'\D', '', words[0]))   # レース距離だけを取り出してint型で保存する
    race_info_weather = words[1].split(':')                 # 天候
    race_info_condition = words[2].split(':')               # 馬場の状態

    # tableデータを抽出する
    tables = soup.find('table', attrs={'class': 'race_table_01'})
    tables = tables.find_all('tr')

    # 取得したデータからindex名を抽出する
    indexs = tables[0].text.split('\n')

    # レース情報/結果を取得する
    tmp = []
    df = pd.DataFrame()
    df_tmp1 = pd.DataFrame()

    for table in tables[1:]:
        tmp = table.text.split('\n')
        df_tmp1 = pd.Series(tmp)
        df = pd.concat([df, df_tmp1], axis=1)

    # 学習に必要な情報のみを抽出する
    # 着順:要素No.1、馬名:要素No.5、性齢:要素No.7、斤量:要素No.8、騎手:要素No.10、
    # タイム:要素No.12、上り:要素No.21、単勝:要素No.23、人気:要素No.24、馬体重:要素No.25の情報を抽出する
    df_tmp2 = pd.DataFrame()
    df_tmp3 = pd.DataFrame()
    for i in (1, 5, 7, 8, 10, 12, 21, 23, 24, 25):
        df_tmp2 = df.iloc[i]
        df_tmp3 = pd.concat([df_tmp3, df_tmp2], axis=1)

    # カラム名を設定する
    tmp = ['着順', '馬名', '性齢', '斤量', '騎手', 'タイム', '上り', '単勝', '人気', '馬体重']
    df_columns = pd.Series(tmp)
    df_tmp3.columns = df_columns  # index名を設定する

    df_tmp4 = pd.DataFrame()

    try:
        # 距離、天候、馬場、状態の列を追加する
        df_tmp3['距離'] = race_info_distance
        df_tmp3['天候'] = race_info_weather[1]
        df_tmp3['馬場'] = race_info_condition[0]
        df_tmp3['状態'] = race_info_condition[1]
        df_tmp3['開催日'] = race_date
        df_tmp3['レース名'] = race_name
        df_tmp3['開催場所'] = race_place

        # 説明変数として使用しない列を削除する
        df_tmp3.drop(['着順', '上り'], axis=1, inplace=True)

        # 目的変数にするタイムを1列に変更する
        df_tmp4 = df_tmp3.reindex(columns=['タイム', '馬名', '性齢', '斤量', '騎手', '単勝',
                                           '人気', '馬体重', '距離', '天候', '馬場', '状態',
                                           '開催日', 'レース名', '開催場所'])
    except:
        print('レース情報を取得できませんでした')

    return df_tmp4

if __name__ == "__main__":
    main()

自分で試したこと

他言語の経験はありますが、Pythonは初めて使います。一つ一つの意味を調べながらエラー解決を目指しておりましたが、問題が解決できませんでした。

追記
 コメント頂き、mainの呼び出しを最後に致しました。

1 likes

2Answer

Pythonのプログラムは上から順に実行します。

if __name__ == "__main__":
    main()

main関数を呼び出したとき、def output_RaceResult(url): の部分はまだ実行されていないのでoutput_RaceResult関数は未定義です。
関数定義をその前に移動してmain関数の呼び出しを最後にしましょう。

0Like

Comments

  1. @soccerdjmptw

    Questioner

    回答ありがとうございます。
    分かりやすい説明で、無事に進めることができました。
    しかし次の壁にぶち当たりました。

    下記エラー発生

    Traceback (most recent call last):
    File "d:\Python\hellogood.py", line 238, in <module>
    main()
    File "d:\Python\hellogood.py", line 46, in main
    df_RaceResult = pd.concat([df_RaceResult, output_RaceResult(url)], axis=0)
    ^^^^^^^^^^^^^^^^^^^^^^
    File "d:\Python\hellogood.py", line 184, in output_RaceResult
    tables = tables.find_all('tr')
    ^^^^^^^^^^^^^^^
    AttributeError: 'NoneType' object has no attribute 'find_all'

    現在はこちらのエラーを解消しております。
  File "d:\Python\hellogood.py", line 184, in output_RaceResult
    tables = tables.find_all('tr')
             ^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'find_all'

上で184行目のNoneTypeオブジェクトにはfind_allがないよと言われているので
tablesが予想に反してNoneTypeとして返ってきてしまっているのが問題です

で、そのtablesですが、以下の文から作っています

tables = soup.find('table', attrs={'class': 'race_table_01'})

この文はsoupオブジェクト内からrace_table01属性のtableタグを取ってくる文なので、
このsoupオブジェクトを作るときに渡しているhtmlのソースコードをprint(res.text)とかで確認してみるのが解決の糸口になるのではないでしょうか?

race_table_01属性が無いとか、そういった状況になっているのではないかと思います

0Like

Comments

  1. あるいは、findが完全一致検索になっているせいで見つからないとかでしょうか?

    部分一致のsoup.select_one("table.race_table_01")としたらうまくいくかもしれません。
  2. @soccerdjmptw

    Questioner

    返信遅れてしまい申し訳ありません。
    ご親切にありがとうございます。

    ご指摘の通り
    soup.select_one("table.race_table_01")
    と書き換えて実行致しました。

    以下実行結果になります。

    Traceback (most recent call last):
    File "d:\Python\speed\hellogood.py", line 239, in <module>
    main()
    File "d:\Python\speed\hellogood.py", line 46, in main
    df_RaceResult = pd.concat([df_RaceResult, output_RaceResult(url)], axis=0)
    ^^^^^^^^^^^^^^^^^^^^^^
    File "d:\Python\speed\hellogood.py", line 185, in output_RaceResult
    tables = tables.find_all('tr')
    ^^^^^^
    UnboundLocalError: cannot access local variable 'tables' where it is not associated with a value

    上記のようなエラーが発生致しました。
  3. tables = を消していませんか?
    select_oneはfindの代替なのでコードは以下になります。

    tables = soup.select_one("table.race_table_01")
    tables = tables.find_all('tr')
  4. @soccerdjmptw

    Questioner

    確認しました。
    たしかに消してしまいました。
    改めて実行結果を記載します

    Traceback (most recent call last):
    File "d:\Python\speed\hellogood.py", line 238, in <module>
    main()
    File "d:\Python\speed\hellogood.py", line 46, in main
    df_RaceResult = pd.concat([df_RaceResult, output_RaceResult(url)], axis=0)
    ^^^^^^^^^^^^^^^^^^^^^^
    File "d:\Python\speed\hellogood.py", line 184, in output_RaceResult
    tables = tables.find_all('tr')
    ^^^^^^^^^^^^^^^
    AttributeError: 'NoneType' object has no attribute 'find_all'

    同じようなエラーとなりました。
    こちらでもなんとかエラーが解消できるように調べていますが、上手くいかないです
  5. そうなると res = requests.get(url) で取得した res の中に tableタグでrace_table_01という属性を持つ要素がないのかもしれません。

    res = requests.get(url)の前の行にprint(url)を書いてurlを表示してみてください。

    res をprint(res.text)で表示して、race_table_01が有るか確認してみてください。
  6. @soccerdjmptw

    Questioner

    res = requests.get(url)
    を実行致しました。

    結論から申し上げますと解決致しました。
    print表示させるとある特定の条件でのみ情報が取得できないいとわかりました。
    ですので、それを取り除く機能を実装し問題なく実行できました。

    これまで丁寧にアドバイス下さりありがとうございました。
    ここからはこの情報を使ったAIアルゴリズムを少しやってみようかとおもいますので、また何かあればよろしくお願いいたします。

Your answer might help someone💌