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

# 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)とかいうクソアルゴリズムでも気にしたら負けです。
多分もっといい方法がある。(知らんけど)

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

```python
import requests
from bs4 import BeautifulSoup

html = requests.get(url)
soup = BeautifulSoup(html.text, 'html.parser')
```
これでsoupに抜いてきたhtmlをタグでパサーしたものが入ります。
さてこのどこを抜いて
右クリックして検証を押すとこんな画面になります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/263565/307965f3-a1e0-34a1-2fae-a8cab2e227e3.png)
ここでほしいデータを見ます。ここでは６着ダノンスイショウのジャッキーを調べています。
spanタグのclass Detail_Rightにジャッキーの名前と体重が入っています。

```python
type(soup.find_all('span', attrs={'class', 'Detail_Right'}))
# bs4.element.ResultSet
```
find_allメソッドはそのタグを含む箇所をリストにして返します。pythonの指定語と被ってしまう'class'などはattrsで指定します。

```python
soup.find_all('span', attrs={'class', 'Detail_Right'})[5]
'''Output
<span class="Detail_Right">
三浦<br/>
(55.0)
</span>
'''
```
中身を取り出す方法はstringとcontentsが主な方法です。
指定したタグ(今回はspan)の中にタグが含まれているとstringが無効になってしまうので個人的にオススメはcontentsです。contentsは中身がリストになって返ってきます。

```python
soup.find_all('span', attrs={'class', 'Detail_Right'})[5].contents
# ['\n三浦', <br/>, '\n(55.0)\n']
```
このとき中身に改行文字がついてきてしまうのでstrip()などで適宜成型してください。

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

```python
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のバグなのか？？)困ったら消してください。