【スクレイピング】「BeautifulSoup」でnetkeibaからレースデータを取得

競馬AI

本記事の目的

目的
  • 「netkeiba.com」からレース結果とレース情報を取得する

レース結果 & レース情報

作業概要

本サイトでは、下記の手順で「netkeiba.com」からレース結果を取得します。

ねずみくん
ねずみくん

このページでは、「②レースデータを取得」を解説するよ

本ページでは、「①開催レースのURL一覧を取得」で取得した開催レースURLを使用します。

まだ、ご覧になっていない方は是非読んでみてください!

②レースデータを取得

ソースコード

""" メソッド:レースデータ取得 """

# ライブラリ
from tqdm import tqdm
import urllib.request
from bs4 import BeautifulSoup
import time
import pandas as pd

def scrape_data(url_df):
        
    # 初期値
    race_result = pd.DataFrame()
    date_list = url_df['date']
    url_list = url_df['url']
    cnt = 0
    
    try:
    
        # tqdmで処理状況を見える化 --------------------- (※1)
        for url in tqdm(url_list):
        
            # 1秒間停止 --------------------- (※2)
            time.sleep(1)
                   
            # レース結果を取得 --------------------- (※3)
            df = pd.read_html(url)[0]
            
            # WEBデータを解析
            html = urllib.request.urlopen(url).read()
            soup = BeautifulSoup(html, 'html.parser')

            # 競走馬IDを追加する --------------------- (※4)
            div_result = soup.find('div', class_='ResultTableWrap')
            span_horse = div_result.find_all('span', class_='Horse_Name')
            horse_id_list = []
            for span in span_horse:
                href = span.a.attrs['href']
                horse_id = href.split('/')[-1]
                horse_id_list.append(horse_id)
            df['horse_ID'] = horse_id_list
                
            # 騎手IDを追加する --------------------- (※4)
            td_jockey = div_result.find_all('td', class_='Jockey')
            jockey_id_list = []
            for td in td_jockey:
                href = td.a.attrs['href']
                jockey_id = href.split('/')[-2]
                jockey_id_list.append(jockey_id)
            df['jockey_ID'] = jockey_id_list
            
            # 「開催年月日」「競馬場ID」「開催回数」「開催日」「レース回数」を追加
            race_id = url.split('race_id=')[1][0:12]
            df['date'] = [date_list[cnt]] * len(df)
            df['area_ID'] = [race_id[4:6]] * len(df)
            df['race_month'] = [race_id[6:8]] * len(df)
            df['race_day'] = [race_id[8:10]] * len(df)
            df['race_num'] = [race_id[10:12]] * len(df)
            
            # レース情報を取得
            div_01 = soup.find('div', class_='RaceData01').text
            div_02 = soup.find('div', class_='RaceData02').text

            # 「レース種類」を抽出 (0:ダート、1:芝、2:障害、3:その他)
            text = div_01.split('/')[1]
            if 'ダ' in text:
                race_type = 0
            elif '芝' in text:
                race_type = 1
            elif '障' in text:
                race_type = 2
            else:
                race_type = 3
            
            # 「レース長」を抽出      
            race_len = int(text.split('m')[0][2:])
            
            # 「天候」を抽出 (0:曇、1:晴、2:雨、3:小雨、4:小雪、5:雪、6:その他)
            text = div_01.split('/')[2]
            if '曇' in text:
                weather = 0
            elif '晴' in text:
                weather = 1
            elif '雨' in text:
                weather = 2
            elif '小雨' in text:
                weather = 3
            elif '小雪' in text:
                weather = 4
            elif '雪' in text:
                weather = 5
            else:
                weather = 6
        
            # 「馬場状態」を抽出 (0:良、1:稍重、2:稍、3:重、4:不良、5:その他)
            text = div_01.split('/')[3]
            if '良' in text:
                ground_state = 0
            elif '稍重' in text:
                ground_state = 1
            elif '稍' in text:
                ground_state = 2
            elif '重' in text:
                ground_state = 3
            elif '不良' in text:
                ground_state = 4
            else:
                ground_state = 5
        
            # 「レースクラス」を抽出
            race_class = div_02.split('\n')[5] 
                                
            # レース情報を追加
            df['race_type'] = [race_type] * len(df)
            df['race_len'] = [race_len] * len(df)
            df['weather'] = [weather] * len(df)
            df['ground_state'] = [ground_state] * len(df)
            df['race_class'] = [race_class] * len(df)
            
            race_result = race_result.append(df)
        
        # インデックスを振りなおす
        race_result = race_result.reset_index(drop=True)
        return race_result
        
    except Exception as e:
        print('■取得失敗URL : ', url)
        print('■エラー内容 : ', e)
        
        # インデックスを振りなおす
        race_result = race_result.reset_index(drop=True)
        return race_result
    

メソッドの引数と戻り値

カテゴリ 変数 説明
引数 url_df DataFrame 開催レースのURL
戻り値 race_data DataFrame レースデータ

ねずみくん
ねずみくん

引数の “url_df” は、「開催レースのURL一覧を取得」で解説しているメソッドの戻り値だよ!

解説

プログレスバーを表示する(※1)

本処理では、

メソッド

tqdm()

を使用することでプログレスバーを表示しています。

処理状況が分かるようになるので、処理時間が長いループで利用することをおすすめします。

# tqdmで処理状況を見える化 --------------------- (※1)
for url in tqdm(url_list):

時間間隔を設定する(※2)

スクレイピングする際には、Webサイトへの負荷を考慮する必要があります。

Webサーバーに負荷をかけると、他のユーザーがそのWebサイトを参照できなかったり、ひどい場合はサーバーが落ちてしまう場合もあります。
そのような迷惑をかけないためにも、クローラーは間隔をあけてWebサーバーにアクセスするといった対応が必要となります。最低1秒以上は間隔をあけるようにしましょう。

Pythonオンライン学習サービス PyQ

上記のように、スクレイピングする際には最低1秒以上は間隔を空けましょう。

クラス.メソッド

time.sleep()

# 1秒間停止 --------------------- (※2)
time.sleep(1)

レース結果を取得する(※3)

スクレイピングする際には、まずは欲しい情報のHTMLを確認します。

「欲しいデータを選択 → 右クリック → 検証」の順に操作することで、HTMLを確認できます。

そうすると、今回入手するデータは<table>タグであることが分かります。

この場合は、pandasの

メソッド

read_html()

を使用することで簡単にレース結果を入手できます。

# レース結果を取得 --------------------- (※3)
df = pd.read_html(url)[0]

「馬名」と「騎手」をIDを追加する(※4)

入手したデータを機械学習で使用する際には、数値以外が使えないケースがあります。

なので本処理では、「馬名」「騎手」のID情報をWebデータから入手します。

ここでHTMLを確認してみると、

  • 該当箇所の「class属性」は、’Horse_Name’と’Jockey’
  • <a>タグにおける「href属性」に、それぞれのID情報が含まれている

ということが分かります。

本処理では、”Beautiful Soup”の

メソッド

find()

を使用して、競走馬ID・騎手IDを入手します。

(※4)の処理内容
  • ①Beautiful Soupのインスタンスを作成
  • ②「class属性」が「ResultTableWrap」である、<div>タグの要素を抽出
  • ③取得した要素の中から、「class属性」が「Horse_Name」、「Jockey」である<span>タグ・<td>タグの要素を抽出
  • ④取得した要素の中から、<aタグ>の「href属性」を取得
  • ⑤文字列を編集して、競走馬IDと騎手IDを取得
# WEBデータを解析
html = urllib.request.urlopen(url).read()
soup = BeautifulSoup(html, 'html.parser')

# 競走馬IDを追加する --------------------- (※4)
div_result = soup.find('div', class_='ResultTableWrap')
span_horse = div_result.find_all('span', class_='Horse_Name')
horse_id_list = []
for span in span_horse:
    href = span.a.attrs['href']
    horse_id = href.split('/')[-1]
    horse_id_list.append(horse_id)
df['horse_ID'] = horse_id_list
                
# 騎手IDを追加する --------------------- (※4)
td_jockey = div_result.find_all('td', class_='Jockey')
jockey_id_list = []
for td in td_jockey:
    href = td.a.attrs['href']
    jockey_id = href.split('/')[-2]
    jockey_id_list.append(jockey_id)
df['jockey_ID'] = jockey_id_list

レース情報を取得(※5)

レース結果を取得する(※3)」と同様にHTMLを確認すると、

  • レース情報は、<div>タグにおける「class属性」が「RaceData01」または「RaceData02」である

ということが分かります。

本処理でも、”Beautiful Soup”の

メソッド

find()

を使用して、レース情報(レース種類、レース長、天候、馬場状態、レースクラス)を取得します。

# レース情報を取得 --------------------- (※5)
div_01 = soup.find('div', class_='RaceData01').text
div_02 = soup.find('div', class_='RaceData02').text

# 「レース種類」を抽出 (0:ダート、1:芝、2:障害、3:その他)
text = div_01.split('/')[1]
if 'ダ' in text:
    race_type = 0
elif '芝' in text:
    race_type = 1
elif '障' in text:
    race_type = 2
else:
    race_type = 3
            
# 「レース長」を抽出      
race_len = int(text.split('m')[0][2:])
            
# 「天候」を抽出 (0:曇、1:晴、2:雨、3:小雨、4:小雪、5:雪、6:その他)
text = div_01.split('/')[2]
if '曇' in text:
    weather = 0
elif '晴' in text:
    weather = 1
elif '雨' in text:
    weather = 2
elif '小雨' in text:
    weather = 3
elif '小雪' in text:
    weather = 4
elif '雪' in text:
    weather = 5
else:
    weather = 6
        
# 「馬場状態」を抽出 (0:良、1:稍重、2:稍、3:重、4:不良、5:その他)
text = div_01.split('/')[3]
if '良' in text:
    ground_state = 0
elif '稍重' in text:
    ground_state = 1
elif '稍' in text:
    ground_state = 2
elif '重' in text:
    ground_state = 3
elif '不良' in text:
    ground_state = 4
else:
    ground_state = 5

# 「レースクラス」を抽出
race_class = div_02.split('\n')[5] 

「df」にレース情報を追加(※6)

入手したレース情報は文字列型のため、このままデータフレーム型変数「df」に追加できません。

そこで本処理では、↓のように記述することで

[文字列] * n   ※n:「df」のレコード件数

入手したデータを「df」のレコード件数分だけ繰り返してリスト化し、「df」に追加しています。

# レース情報を追加 --------------------- (※6)
df['race_type'] = [race_type] * len(df)
df['race_len'] = [race_len] * len(df)
df['weather'] = [weather] * len(df)
df['ground_state'] = [ground_state] * len(df)
df['race_class'] = [race_class] * len(df)

例外発生時は「取得失敗URL」と「エラー内容」を出力する(※7)

本処理では意図しないデータが原因で、例外が発生する可能性があります。

そこで、

構文

try:
正常処理

except Exception as e:
異常処理

という構文を使用することで、例外が発生した際の「取得失敗URL」と「例外内容」を出力しています。

# 例外発生時は「取得失敗URL」と「例外内容」を出力する --------------------- (※7)    
except Exception as e:
    print('■取得失敗URL : ', url)
    print('■例外内容 : ', e)

参考図書

本記事の内容は、以下の図書を参考にしています。

本書は、下記のようにスクレイピングについてとても詳しく解説されています。

本書の目次
  • 第一章:クローリングとスクレイピング
    • データのダウンロード
    • BeautifulSoupでスクレイピング
    • CSSセレクタについて
    • リンク先を丸ごとダウンロード
  • 第二章:高度なスクレイピング
    • ログインの必要なサイトからダウンロード
    • ブラウザーを経由したスクレイピング
    • スクレイピング道場
    • Web APIからのデータ取得
    • cronと定期的なクローリング
  • 第三章:…
ねずみくん
ねずみくん

本書の内容は「BeautifulSoupでスクレイピング」を参考にしているよ!

本書は、上記以外にも

  • データベースへの登録方法
  • 機械学習
  • ディープラーニング

等も内容に含まれています。

Python初心者にも理解しやすい内容になっているので、ぜひ参考にしてください。

本書の特徴
  • Python初心者でも理解できるくらい具体的に説明されている
  • ソースコードも併せて紹介されているため、コード記述の際に躓きづらい

次回の内容

次回は、本記事で取得したレースデータを、機械学習で使えるように加工します。

ぜひ読んでみてください!

コメント

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