53
78

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

クローリング&スクレイピング 競馬情報抜き出してみた

Last updated at Posted at 2018-11-30

pythonは何でもできるすごい奴

pythonの何がいいってやっぱりライブラリを始め環境が整っていることだよね。
今回はライブラリ"requests"と"Beautiful Soup"を使ってweb上のデータを分析するためのcsvファイルを作っていきたいと思います。

スクレイピングとクローリング

ウェブサイトからhtmlをぶっこ抜いてきて(クローリング)、それをタグを頼りに必要な情報だけを抜き出し(スクレイピング)て出力することを目的にする。
今回は「競馬の結果をウェブサイトから引っ張ってきて分析の為にcsvに落とし込む」。

実行環境は

  • OS : Windows10
  • Python3.6.5

webクローリング

目標のウェブサイトをhttp://sp.netkeiba.com/ とします。
ここにアクセスしてhtmlを取得していきます。
ぶっこ抜くにも1986年からのデータがそろっているのでどこを抜いたらいいのか考えます。
http://race.sp.netkeiba.com/?pid=race_result&race_id=201805040201&rf=rs
このURLを見るとrace_idというのが指定されていますね。ここをいじっていきましょう。
2018:05:04:02:01と分けることができ、2018が年、05がどうやら東京、04:02あたりがレース日、01がレースの番号みたいですね。
掘っていくと2008年以降とその前ではレイアウトが変わっていました。とりあえずここ10年のデータを掘り下げる方向でいきましょう。(暇だったら追記するかも)
細かいところまで見ようか迷ったけど、とりあえず走らせてエラー出したところは無視していくスタイルでいきましょう。

  • 年 2008-2018
  • 場所(中央競馬)
    札幌:01
    函南:02
    福島:03
    新潟:04
    東京:05
    中山:06
    中京:07
    京都:08
    阪神:09
    小倉:10
  • レース 01-12

アルゴリズムはそのままfor文でぶん回すだけです。
O(n^4)とかいうクソアルゴリズムでも気にしたら負けです。
多分もっといい方法がある。(知らんけど)

スクレイピング

スクレイピングはタグを頼りに内容を振り分けていく作業です。

import requests
from bs4 import BeautifulSoup

html = requests.get(url)
soup = BeautifulSoup(html.text, 'html.parser')

これでsoupに抜いてきたhtmlをタグでパサーしたものが入ります。
さてこのどこを抜いて
右クリックして検証を押すとこんな画面になります。
image.png
ここでほしいデータを見ます。ここでは6着ダノンスイショウのジャッキーを調べています。
spanタグのclass Detail_Rightにジャッキーの名前と体重が入っています。

type(soup.find_all('span', attrs={'class', 'Detail_Right'}))
# bs4.element.ResultSet

find_allメソッドはそのタグを含む箇所をリストにして返します。pythonの指定語と被ってしまう'class'などはattrsで指定します。

soup.find_all('span', attrs={'class', 'Detail_Right'})[5]
'''Output
<span class="Detail_Right">
三浦<br/>
(55.0)
</span>
'''

中身を取り出す方法はstringとcontentsが主な方法です。
指定したタグ(今回はspan)の中にタグが含まれているとstringが無効になってしまうので個人的にオススメはcontentsです。contentsは中身がリストになって返ってきます。

soup.find_all('span', attrs={'class', 'Detail_Right'})[5].contents
# ['\n三浦', <br/>, '\n(55.0)\n']

このとき中身に改行文字がついてきてしまうのでstrip()などで適宜成型してください。

結局どうなるのさ

こんな感じのことを各ラベルについて繰り返していくと以下のようなコードになります。

import requests
from tqdm import tqdm
import time
from bs4 import BeautifulSoup
import pandas as pd

def numStr(num):
    if num >= 10:
        return str(num)
    else:
        return '0' + str(num)

Base = "http://race.sp.netkeiba.com/?pid=race_result&race_id="
dst = ''
df_col = ['year', 'date', 'field', 'race', 'race_name'
          , 'course', 'head_count', 'rank', 'horse_name'
          , 'gender', 'age', 'trainerA', 'trainerB', 'weight', 'c_weight', 'jackie', 'j_weight'
          , 'odds','popu']
df = pd.DataFrame()

for year in tqdm(range(2008, 2019)):
    for i in tqdm(range(1, 11)):
        for j in tqdm(range(1, 11)):
            for k in tqdm(range(1, 11)):
                for l in range(1, 13):
                    # urlでぶっこ抜く
                    url = Base + str(year) + numStr(i) + numStr(j) + numStr(k) + numStr(l)
                    time.sleep(1)
                    html = requests.get(url)
                    html.encoding = 'EUC-JP'
                    
                    # scraping
                    soup = BeautifulSoup(html.text, 'html.parser')
                    # ページがあるかの判定
                    if soup.find_all('div', attrs={'class', 'Result_Guide'})!=[]:
                        break
                    else:
                        #共通部分を抜き出す
                        CommonYear = year
                        CommonDate = soup.find_all('div', attrs={'class', 'Change_Btn Day'})[0].string.strip()
                        CommonField= soup.find_all('div', attrs={'class', 'Change_Btn Course'})[0].string.strip()
                        CommonRace = soup.find_all('div', attrs={'Race_Num'})[0].span.string
                        CommonRname= soup.find_all('dt', attrs={'class', 'Race_Name'})[0].contents[0].strip()
                        CommonCourse= soup.find_all('dd', attrs={'Race_Data'})[0].span.string
                        CommonHcount= soup.find_all('dd', attrs={'class', 'Race_Data'})[0].contents[3].split()[1]
                        
                        for m in range(len(soup.find_all('div', attrs='Rank'))):
                            dst = pd.Series(index=df_col)
                            try:
                                dst['year'] = CommonYear
                                dst['date'] = CommonDate
                                dst['field']= CommonField #開催場所
                                dst['race'] = CommonRace
                                dst['race_name'] = CommonRname
                                dst['course'] = CommonCourse
                                dst['head_count'] = CommonHcount #頭数
                                dst['rank'] = soup.find_all('div', attrs='Rank')[m].contents[0]
                                dst['horse_name'] = soup.find_all('dt', attrs=['class', 'Horse_Name'])[m].a.string
                                detailL = soup.find_all('span', attrs=['class', 'Detail_Left'])[m]
                                dst['gender'] = list(detailL.contents[0].split()[0])[0]
                                dst['age'] = list(detailL.contents[0].split()[0])[1]
                                dst['trainerA'] = detailL.span.string.split('')[0]
                                dst['trainerB'] = detailL.span.string.split('')[1]
                                if len(detailL.contents[0].split())>=2:
                                    dst['weight'] = detailL.contents[0].split()[1].split('(')[0]
                                    if len(detailL.contents[0].split()[1].split('('))>=2:
                                        dst['c_weight'] = detailL.contents[0].split()[1].split('(')[1].strip(')') #多分馬の体重変動
                                detailR = soup.find_all('span', attrs=['class', 'Detail_Right'])[m].contents
                                if  "\n" in detailR or "\n" in detailR or '\n' in detailR:
                                    detailR.pop(0)
                                dst['jackie'] = detailR[0].string.strip()
                                dst['j_weight'] = detailR[2].strip().replace('(', '').replace(')', '') #多分jackieの体重変動
                                Odds = soup.find_all('td', attrs=['class', 'Odds'])[m].contents[1]
                                if Odds.dt.string is not None:
                                    dst['odds'] = Odds.dt.string.strip('')
                                    dst['popu'] = Odds.dd.string.strip('人気') #何番人気か
                            except:
                                pass
                            dst.name = str(year) + numStr(i) + numStr(j) + numStr(k) + numStr(l) + numStr(m)
                          
                            df = df.append(dst)

df.to_csv('keiba_PS.csv', encoding='shift-jis')

(クソアルゴリズムで)すごい時間かかるのでサーバーかなんかに投げることをお勧めします。
たぶん1レコードごとにSerise作ってくっつけているので時間がかかるんだと思います(じゃあ直せよってね)

最後に

ご承知とは思いますが、requestはやりすぎるとサーバー負荷になるので少なくとも1秒は間をあけるのがマナーです。

こんなことをする前にAPIを探してホントにそういうことをできるものはないのか調べるのが妥当です。クローリング、スクレイピングでデータをサルベージするのは最終手段というのを覚えておいていただきたいです。

※tqdmでなんかruntimeエラー吐いたりするので(調べたらなんかtqdmのバグなのか??)困ったら消してください。

53
78
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
78

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?