python初学者がスクレイピングでボートレースの結果1年分を取得してみた。

記事
IT・テクノロジー
こんばんは

ブログって楽しいですね!

中川です。

帰って見てみると4つもいいねを頂いていたのでめちゃくちゃテンションあがっちゃいました。

今日は昨日作ったボートレースの順位取得するアプリを強化し、自動的に多くの情報を取得できるようにしていきたいと思います。

ちなみに前回完成したコードはこちらです。

import numpy as np
import pandas as pd
import requests
import re
from bs4 import BeautifulSoup

url="ボートレースのサイト名/owpc/pc/race/resultlist?jcd=12&hd=20220417"
res = requests.get(url)
soup = BeautifulSoup(res.text, "html.parser")
counter = 0
i=0
ranking_1 = []
ranking_2 = []
ranking_3 = []
for content in soup.find_all("span", class_='numberSet1_number'):
    i+=1
    if i%5==0 or i%5==4:
        continue
    number = re.sub(r"\D", "", content.text)
    counter += 1
    if counter%3 ==1:
        ranking_1.append(int(number))
    if counter%3 ==2:
        ranking_2.append(int(number))
    if counter%3 ==0:
        ranking_3.append(int(number))
kekka = {'一着':ranking_1,'二着':ranking_2,'三着':ranking_3}
df = pd.DataFrame(kekka)
print(df)
df.to_csv("boat_2.csv",encoding='utf_8_sig')

ここでは、太字の部分jcd=12&hd=20220417と設定していますので、住之江競艇場(競艇場コード12)の2022年4月17日のレース結果を取得し、出力しております。

このhd=20220417の部分を過去のレース日程に変えて入力していくことで、いままでのレース結果情報を取得していくことができます。

この部分をfor文によって取得したい日程の情報を選択し、情報を取得していく処理を繰り返します。

しかしこの繰り返し処理を行う上で注意しなければならないことがあります。

それはURLという文字列の中に変数を代入する場合所定の形式で記述しなければならないということです。

いくつかのやり方があるのですが僕は"文字列{変数}文字列".format{変数=変数}"と表記することで文字列の中身の変数を指定しています。

以上を踏まえ、繰り返し処理によってボートレースの結果情報を取得します。

counter = 0
i=0
ranking_1 = []
ranking_2 = []
ranking_3 = []

for day in np.arange(6,10,1):

 url="ボートレースのサイト名/owpc/pc/race/resultlist?jcd=12&hd=2022040{day}".format(day=day)

 #あとは冒頭のコードと同じ。

kekka = {'一着':ranking_1,'二着':ranking_2,'三着':ranking_3}
df = pd.DataFrame(kekka)
print(df)

すると、48レース分の結果データが取得でき出力できました。

ここではdayという変数の中にnumpyのarangeメソッドを使用して、6~9の値を挿入しています。

dayの数字が6→7→8→9と変遷していき合計で4回繰り返し処理がされることになります。

また、変数宣言とデータフレーム出力のコードの部分、最初と最後のコードの部分のインデントは一切下げずにそのままを維持するために場所を変更しています。

これは、追加されたfor文によって不必要に実行されてしまうことを防ぐためです。

これで、いままでのすべての情報を取得できるやんと思ったのですが、

for day in np.arange(6,10,1) → for day in np.arange(1,10,1)

のように変化を加えた場合、All arrays must be of the same length
というエラーが返ってきました。

これはどうやら、レースの結果に何らかの不都合があり結果が取得できなかったために番号にずれが生じ、リストの長さが変わってしまったために表示されたエラーだと考えられます。

6,7,8,9,日のレースにはとくに不具合がなかったため取得できたようですが、範囲を1日から10日にした瞬間エラーになってしまいましたね。

ですから1日から5日の間のレースに何か不都合があったに違いありません。

boat(2).png

見てみると4月5日のレースが不成立であるためにエラーが発生しているようでした。

BeautifulSoup のメソッドを使ってsoup.find_all("span", class_='numberSet1_number')と記述することでspanタグのクラス名numberSet1_numberを取得してきているのですが、デベロッパーツールを使ってみてみると不成立の部分は<div class ="numberSet1_row" ...>不成立</div>と表記されているようでした。

そもそも指定したクラストは異なるのでここの不成立の数字は抽出できておらず、しかも抜け落ちたまま後のデータを回収していきますので後の結果がすべてずれてしまいます。

そのずれによってエラーが発生してしまっているようなのです。。


それからエラーに対する策が思いつかず次の日になってしまいました、

学校の行き帰りで考え、ようやく思いついたのが

if len(soup.find_all("span", class_='numberSet1_number')) < 60:
 continue

 for day in np.arange(6,10,1):
    url="ボートレースのサイト名/owpc/pc/race/resultlist?jcd=12&hd=2022040{day}".format(day=day)
    res = requests.get(url)

                ・
                ・
                ・

というものです。

これは、soup.find_allで取得される情報がすべてリスト型の情報で取得されるという性質、つまりここでは[1,5,6,1,5・・・]というように辞書型に57個の数字が格納されているということになります。

もし、何も不備がなければ通常は60個の数字が格納されるはずなのでcontinue文を使って

もしもリストの中の要素が60個未満ならcontinue以下の処理をスキップするという挙動をします。

こうすることで不備が1レースでもある日のレースはデータに格納されません。

これによって、実行時間が長くなってしまうものの、一応は不測の事態に対応できるようになったはずです。

そして、完成したコードがこちらになります。

counter = 0
i=0
ranking_1 = []
ranking_2 = []
ranking_3 = []

for day in np.arange(1,10,1):
    url="ボートレースのサイト名/owpc/pc/race/resultlist?jcd=12&hd=2022040{day}".format(day=day)
    res = requests.get(url)
    soup = BeautifulSoup(res.text, "html.parser")
    if len(soup.find_all("span", class_='numberSet1_number')) < 60:
        print("処理をスキップします。")
        continue
    print("データが存在します。")#追加できる不備の無いデータがあれば表示され、以下が実行される。
    for content in soup.find_all("span", class_='numberSet1_number'):
        i+=1
        if i%5==0 or i%5==4:
            continue
        number = re.sub(r"\D", "", content.text)
        counter += 1
        if counter%3 ==1:
            ranking_1.append(int(number))
        if counter%3 ==2:
            ranking_2.append(int(number))
        if counter%3 ==0:
            ranking_3.append(int(number))
kekka = {'一着':ranking_1,'二着':ranking_2,'三着':ranking_3}
df = pd.DataFrame(kekka)
print(df)

実行すると、

処理をスキップします。
処理をスキップします。
データが存在します。
データが存在します。
処理をスキップします。
データが存在します。
データが存在します。
データが存在します。
データが存在します。
    一着 二着 三着
0   4 1 5
1     3 1 4
2   1 4 3
3   1 4 2
4   1 3 4
.. .. .. ..

のように表示されます。

太字の部分は例えば今回の場合であれば4月の1日から9日までの情報を取得しているのでここを変えることでほしい範囲の情報を取得できます。

ここでは、4月1日と4月2日のレースは存在しないため len(soup.find_all("span", class_='numberSet1_number')) の値が0になり、4月5日の場合は上述の通り57になるので処理がスキップされています。

でも、このままではいちいち月と1~9日と10~31日の場合で自分で設定しなければならないので、大して役には立たないことを完成させてから気付きました。

なので、上記太字の部分をなんとかしなければなりません。。

リストを作って中に[0101,0102,0103,]というように365日分記述するという方法も考えましたが、これではもっと大量の数字が必要になったときに対処できません。

調べているとこんな方法が見つかりました。

month = np.arange(1,13,1)
day = np.arange(1,32,1)
for i in month:
    for j in day:
        print(i,j)

実行すると

11
12
13
1230
1231

というように365個のデータが取得できました。

ですがボートレースのサイトでは1月1日は→0101と表示されていますので、数字が一桁の場合にのみ先頭に0を補完してあげる必要があります。

そこで使用するのが

新しい変数 = f'{古い変数:02}'   

このコードの意味は、数字が2桁になるように0で補完してください。という意味になります。

太字の2の部分が桁数を表しています。

太字の2の部分を例えば 4 などに変更すると数字が 4 桁になるように0で補完してください。という意味に代わります。

これを考慮して、

day = np.arange(1,32,1)
month = np.arange(1,13,1)
for i in month:
    for j in day:
        #if len(i) == 1:
        i_2 = f'{i:02}'
        j_2 = f'{j:02}'
        print(i_2,j_2)   

と、すると
01 01
01 02
01 03
01 04
01 05
01 06
12 31

とデータが返ってきます。

これを使えば365日分のボートレースの結果が取得できそうです。

変化を加えたい部分は上述の url="ボートレースのサイト名/owpc/pc/race/resultlist?jcd=12&hd=2022040{day}".format(day=day)の部分ですのでここを

→ url="ボートレースのサイト名/owpc/pc/race/resultlist?jcd=12&hd=2020{m_2}{d_2}".format(m_2 = m_2, d_2 = d_2)

に変更します。

format関数は{}の中に変数を入れることで文字列の中に変数を挿入することができます。

そしてここでは1年分のデータを取得するために2020年を指定しています。

また、for day in np.arange(1,10,1):

の部分は

→for m in month:
    for d in day:

に変更します。

以上を踏まえ、完成したコードがこちらになります。

#完成品4
month = np.arange(1,13,1)
day = np.arange(1,32,1)

counter = 0
i=0
ranking_1 = []
ranking_2 = []
ranking_3 = []



#for day in np.arange(1,10,1):

for m in month:
    for d in day:

        m_2 = f'{m:02}' 
        d_2 = f'{d:02}'

        url="ボートレースのサイト名/owpc/pc/race/resultlist?jcd=12&hd=2020{m_2}{d_2}".format(m_2 = m_2, d_2 = d_2)

        res = requests.get(url)

        soup = BeautifulSoup(res.text, "html.parser")


        if len(soup.find_all("span", class_='numberSet1_number')) < 60:

            print("処理をスキップします。")

            continue

        print("データが存在します。") 



        for content in soup.find_all("span", class_='numberSet1_number'):
            i+=1
            if i%5==0 or i%5==4:
                continue
            number = re.sub(r"\D", "", content.text)
            counter += 1
            if counter%3 ==1:
                ranking_1.append(int(number))
            if counter%3 ==2:
                ranking_2.append(int(number))
            if counter%3 ==0:
                ranking_3.append(int(number))

kekka = {'一着':ranking_1,'二着':ranking_2,'三着':ranking_3}

df = pd.DataFrame(kekka)

print(df)

    一着 二着 三着
0   2 4 5
1   1 4 3
2   4 5 2
3   1 2 3
4   1 5 2
... .. .. ..
2227 1 2 5
2228  1 2 3
2229  1 3 2
2230  5 4 6
2231  3 5 2
[2232 rows x 3 columns]

すると、無事1年分のデータを取得することができました!

monthとdayまたは2020の部分を変更することで任意の範囲の結果を取得することができます。

2232レース分、つまり186日分のデータをしっかりとずれることなく取得出来ています。

実行に10分ほど時間を要してしまうので、もっとスマートなコードにしたり他の言語を利用するなど対策は考えられます。

これらのデータを使って統計を取り、機械学習による予測をしたりなど、まだまだやりたいことがあるのですが、今日はここで区切りとさせていただきます。

前回閲覧してくれた方、いいねをくれた方、そして今回閲覧してくれた方、本当にありがとうございました。

前回は初めての記事にも関わらず多くの人に見て頂けてびっくりしちゃいました。

0人だと思っていた僕にとって、7人という人数はとても大きな数字でした。

できるだけ、間を開けずに投稿していきたいと考えておりますので、今後とも宜しくお願いします。






サービス数40万件のスキルマーケット、あなたにぴったりのサービスを探す