見出し画像

Bリーグのスタッツを使ってレーティングを計算する(スタッツのスクレイピング)

前回に引き続き、バスケットボールのアドバンス・スタッツとWINNERの結果との関係を検証する仕組みを作っていきたいと思います。

今回はアドバンス・スタッツを計算するためのデータをBリーグ公式ページから抜き出して、データベースにする部分をPythonで作っていきたいと思います。

Bリーグの公式ページにアドバンス・スタッツを算出するために必要なデータはすべて揃っています。個人スタッツだけでなくチームスタッツもあるのですが、今回は個人スタッツだけをデータベースにします。所属チームをデータベースに追加すればチームスタッツを求めるのは難しくないからです。

1.公式ホームページを調べる

1-1.URL

例えば2023年シーズンのレバンガ北海道の選手スタッツを出場ゲーム数が多い順番に表示するURLは以下のようになります。

https://www.bleague.jp/stats/?year=2023&tab=1&target=player-b1&value=StartingCount&o=desc&e=2&club=702&dt=tot

この中で、今回変える可能性がある部分が、year=とclub=の後ろにある値です。year=は表示したいシーズン、club=は表示したいチームです。
そこでURLをf文字列で定義してyearとclubを変更して別のページにアクセスできるようにします。

year = 2023
club = 702

url = f"<https://www.bleague.jp/stats/?year={year}&tab=1&target=player-b1&value=GameCount&o=desc&e=2&club={club}&dt=tot>"

なお、clubのID番号は公式ホームページで以下のように割り振られています。

北海道:702
仙台:692
秋田:693
茨城:712
宇都宮:703
群馬:713
千葉J:704
A東京:706
SR渋谷:726
川崎:727
横浜BC:694
富山:696
信州:716
三遠:697
三河:728
FE名古屋:717
名古屋D:729
京都:699
大阪:700
島根:720
広島:721
佐賀:1638
長崎:2488
琉球:701

1-2.情報の場所を特定する

URLにアクセスするとチーム別に個人スタッツを見ることができます。ここから欲しいデータを抜き出します。ブラウザのデベロッパーツールから該当するところを見つけてもよいですが、webページの表を行単位で取得するのであればhtmlでtrタグのため、いきなりtrタグで以下のようにPythonコードでスクレイピングし、結果を見てみたいと思います。

import urllib.request
from bs4 import BeautifulSoup

year = 2023
club = 702

url = f"<https://www.bleague.jp/stats/?year={year}&tab=1&target=player-b1&value=GameCount&o=desc&e=2&club={club}&dt=tot>"

html_doc = urllib.request.urlopen(url).read()
soup = BeautifulSoup(html_doc, "html.parser")

stats = soup.find_all("tr")

print(stats)
print(name)

上のコードを実行しますと、2023年シーズン(year=2023)、レバンガ北海道(club=702)の個人スタッツをスターティングメンバーの多い順に変数statsに持ちます。webで表示された画面の一部は以下のように表示されています。

実際に変数statsが抜き出した先頭のリストを参照しますと、表の1行目のヘッダーが取れていることが分かります。

>>> stats[0]

<tr class="tips-parent">
<th class="table-title table-title-nosort js-table-title-nosort table-stickey-col" data-tips="順位" id="up" rowspan="2">順位</th>
<th class="table-title table-stickey-col" data-sort-order="desc" data-sort-value="PlayerNameJ" data-tips="選手情報" id="up" rowspan="2">選手</th>
<th class="table-title" data-sort-order="desc" data-sort-value="TeamNameJ" data-tips="クラブ名" id="up" rowspan="2">クラブ</th>
<th class="table-title" colspan="2">試合数</th>
<th class="table-title" colspan="2">プレイ時間</th>
<th class="table-title">得点</th>
<th class="table-title" colspan="3">フィールドゴール</th>
<th class="table-title" colspan="3">2Pt</th>
<th class="table-title" colspan="3">3Pt</th>
<th class="table-title" colspan="3">フリースロー</th>
<th class="table-title" colspan="3">リバウンド</th>
<th class="table-title">アシスト</th>
<th class="table-title">ターンオーバー</th>
<th class="table-title">スティール</th>
<th class="table-title" colspan="2">ブロック</th>
<th class="table-title" colspan="2">ファウル</th>
<th class="table-title" colspan="2">貢献度</th>
</tr>

次に二番目のリストを見ます。こちらは2行目のヘッダーです。

>>> stats[1]

<tr class="tips-parent">
<th class="table-title" data-sort-order="desc" data-sort-value="GameCount" data-tips="試合数" id="up">G</th>
<th class="table-title js-first-sort headerSortUp" data-sort-order="asc" data-sort-value="StartingCount" data-tips="スターター数" id="up">GS</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PlayTimeSec" data-tips="プレイ時間" id="up">MIN</th>
<th class="table-title" data-sort-order="desc" data-sort-value="MINPG" data-tips="平均プレイ時間" id="up">MINPG</th>
<th class="table-title" data-sort-order="desc" data-sort-value="TotalPoints" data-tips="得点数" id="up">PTS</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PTM" data-tips="フィールドゴール成功数" id="up">FGM</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PTA" data-tips="フィールドゴール試投数" id="up">FGA</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PTR" data-tips="フィールドゴール成功率" id="up">FG%</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PT2M" data-tips="2Pt成功数" id="up">2FGM</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PT2A" data-tips="2Pt試投数" id="up">2FGA</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PT2R" data-tips="2Pt成功率" id="up">2FG%</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PT3M" data-tips="3Pt成功数" id="up">3FGM</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PT3A" data-tips="3Pt試投数" id="up">3FGA</th>
<th class="table-title" data-sort-order="desc" data-sort-value="PT3R" data-tips="3Pt成功率" id="up">3FG%</th>
<th class="table-title" data-sort-order="desc" data-sort-value="FTM" data-tips="フリースロー成功数" id="up">FTM</th>
<th class="table-title" data-sort-order="desc" data-sort-value="FTA" data-tips="フリースロー試投数" id="up">FTA</th>
<th class="table-title" data-sort-order="desc" data-sort-value="FTR" data-tips="フリースロー成功率" id="up">FT%</th>
<th class="table-title" data-sort-order="desc" data-sort-value="RB_OFF" data-tips="オフェンスリバウンド数" id="up">OR</th>
<th class="table-title" data-sort-order="desc" data-sort-value="RB_DEF" data-tips="ディフェンスリバウンド数" id="up">DR</th>
<th class="table-title" data-sort-order="desc" data-sort-value="RB_TOT" data-tips="トータルリバウンド数" id="up">TR</th>
<th class="table-title" data-sort-order="desc" data-sort-value="AS" data-tips="アシスト数" id="up">AS</th>
<th class="table-title" data-sort-order="desc" data-sort-value="TO" data-tips="ターンオーバー数" id="up">TO</th>
<th class="table-title" data-sort-order="desc" data-sort-value="ST" data-tips="スティール数" id="up">ST</th>
<th class="table-title" data-sort-order="desc" data-sort-value="BS" data-tips="ブロック数" id="up">BS</th>
<th class="table-title" data-sort-order="desc" data-sort-value="BSON" data-tips="被ブロック数" id="up">BSR</th>
<th class="table-title" data-sort-order="desc" data-sort-value="FOUL" data-tips="ファウル数" id="up">F</th>
<th class="table-title" data-sort-order="desc" data-sort-value="FOULON" data-tips="被ファウル数" id="up">FD</th>
<th class="table-title" data-sort-order="desc" data-sort-value="EFF" data-tips="貢献度" id="up">EFF</th>
<th class="table-title" data-sort-order="desc" data-sort-value="sPlusMinusPoints" data-tips="その選手が出場していた時間帯のチーム全体の得失点差" id="up">+/-</th>
</tr>

この流れで次の行を見ると何となく想像できるかもしれません。次のリストには最初の選手スタッツが入っています。
選手名についてはtrでスクレイピングすると、選手名までの階層が深いため、データの取得が面倒なのです。そこでclass_="link-line text"でスクレイピングします。
チーム名も同様にclass_="table-team-name"でスクレイピングします。

>>> stats[2]

<tr>
<td class="table-stickey-col">1</td>
<td class="table-stickey-col">
<a class="table-player-name text-link" href="https://www.bleague.jp/roster_detail/?PlayerID=10850">
<span class="link-line text" data-font="sm" data-font-sp="xs">関野 剛平</span>
</a>
<span class="table-player-info">
<span class="table-player-number">
<span>#81</span>
</span>
<span class="table-player-pos">
<span>SG/SF</span>
</span>
</span>
</td>
<td>
<span class="table-team-name">北海道</span>
</td>
<td>7</td>
<td>7</td>
<td>138:17</td> <td>19:45</td> <td>11</td> <td>4</td> <td>23</td> <td>17.4%</td> <td>1</td> <td>2</td> <td>50.0%</td> <td>3</td> <td>21</td> <td>14.3%</td> <td>0</td> <td>0</td> <td>0.0%</td> <td>0</td> <td>6</td> <td>6</td> <td>6</td> <td>7</td> <td>4</td> <td>1</td> <td>0</td> <td>14</td> <td>8</td> <td>-4</td> <td>-25</td> </tr>

1-3.スクレイピングするときに不要なデータを取り除く

スクレイピングしたデータをみますと改行や半角スペースがあることがわかります。視覚的には見やすいのですが、処理する上では面倒ですので、スクレイピングしたときに取り除いてしまいます。

import urllib.request
from bs4 import BeautifulSoup

year = 2023
club = 702

url = f"<https://www.bleague.jp/stats/?year={year}&tab=1&target=player-b1&value=GameCount&o=desc&e=2&club={club}&dt=tot>"

html_doc = urllib.request.urlopen(url).read()
soup = BeautifulSoup(html_doc, "html.parser")

# スクレイピングしたデータから改行と半角スペースを取り除く
[tag.extract() for tag in soup(string='\n')]
[tag.extract() for tag in soup(string=' ')]

stats = soup.find_all("tr")
name = soup.find_all(class_ = "link-line text")
team = soup.find_all(class_ = "table-team-name")

print(stats)
print(name)

これで最初の選手データは以下のように出力されます。

>>> stats[2]

<tr><td class="table-stickey-col">1</td><td class="table-stickey-col"><a class="table-player-name text-link" href="https://www.bleague.jp/roster_detail/?PlayerID=10850"><span class="link-line text" data-font="sm" data-font-sp="xs">関野 剛平</span></a><span class="table-player-info"><span class="table-player-number"><span>#81</span></span><span class="table-player-pos"><span>SG/SF</span></span></span></td><td><span class="table-team-name">北海道</span></td><td>7</td><td>7</td><td>138:17</td><td>19:45</td><td>11</td><td>4</td><td>23</td><td>17.4%</td><td>1</td><td>2</td><td>50.0%</td><td>3</td><td>21</td><td>14.3%</td><td>0</td><td>0</td><td>0.0%</td><td>0</td><td>6</td><td>6</td><td>6</td><td>7</td><td>4</td><td>1</td><td>0</td><td>14</td><td>8</td><td>-4</td><td>-25</td></tr>

2.必要なデータを抜き出す

必要なデータは(1)対象データの特定に使うデータ、(2)アドバンスド・スタッツに必要なデータに分かれます。「1-3.スクレイピングするときに不要なデータを取り除く」で紹介したコードを実行しますと変数statsには全情報、nameには選手名、teamにはチーム名が抜き出されています。

2-1.対象データの特定に使うデータ

これに分類されるのは選手名(name)とチーム名(team)です。選手名はデータベース更新の際に使います。チーム名はチームの合計成績を求めるときに使います。

選手名は以下のように抜き出します。nameのリスト番号を変更すれば次の選手名を見れます。

>>> name[0].text
'関野 剛平'

>>> name[1].text
'トーマス・ウェルシュ'

チーム名は以下のように抜き出します

>>> team[0].text
'北海道'

2-2.アドバンスド・スタッツに必要なデータ

アドバンスド・スタッツの計算に必要なデータは以下の通りです。以下の抜き出し例は表の先頭の選手のデータを取得していますのでstats[2]を指定しています。stats[0]、stats[1]は表のヘッダーです。

# GAME
>>> stats[2].contents[3].text
'9'
# MIN
>>> stats[2].contents[5].text
'138:17'
# PTS
>>> stats[2].contents[7].text
'11'
# FGM
>>> stats[2].contents[8].text
'4'
# FGA
>>> stats[2].contents[9].text
'23'
# 3FGM
>>> stats[2].contents[14].text
'3'
# FTM
>>> stats[2].contents[17].text
'0'
# FTA
>>> stats[2].contents[18].text
'0'
# ORB
>>> stats[2].contents[20].text
'0'
# DRB
>>> stats[2].contents[21].text
'6'
# ASST
>>> stats[2].contents[23].text
'6'
# TOV
>>> stats[2].contents[24].text
'7'
# ST
>>> stats[2].contents[25].text
'4'
# BS
>>> stats[2].contents[26].text
'1'
# F
>>> stats[2].contents[28].text
'14'

3.データベースの雛形を作る

データベースの雛形作成、および書き込んだ内容の確認にはDB Browser for SQliteというアプリケーションを使います。

まずbleague_stat.db という空のデータベースを作成します。次にテーブルとリストを以下のように設定します。

4.データベースに書き込む

前章で作成したデータベースbleague_stat.dbをPythonで書いたプログラムから更新したいと思います。
下記PythonプログラムでB1のチーム全体の個人スタッツを取得します。異なるURLにアクセスする際には短時間に何度もアクセスすると接続先のサーバーに負荷を掛けてしまうため、URLにアクセスしたら3秒くらいはスリープするようにします。これでB1チームの選手スタッツのデータベースを作成完了です。

import sqlite3
import urllib.request
from bs4 import BeautifulSoup
import datetime
import time

def get_html(url):
    try:
        with urllib.request.urlopen(url) as response:
            html_doc = response.read()
        return html_doc
    except Exception as e:
        print(f"Error fetching URL: {e}")
        return None

def parse_stats(html_doc):
    soup = BeautifulSoup(html_doc, "html.parser")
    [tag.extract() for tag in soup(string='\n')]
    [tag.extract() for tag in soup(string=' ')]

    stats = soup.find_all("tr")
    name = soup.find_all(class_="link-line text")
    team = soup.find_all(class_="table-team-name")
    return stats, name, team

def insert_data_to_db(conn, data):
    try:
        cur = conn.cursor()
        sql = '''insert into player_stats (date, team, name, g, minute, pts, fgm, fga,
                 tfgm, ftm, fta, orb, drb, asst, tov, st, bs, f)
                 values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'''
        cur.execute(sql, data)
        conn.commit()
    except Exception as e:
        print(f"Error inserting data: {e}")

def main():
    year = 2023
    clubs = [702, 692, 693, 712, 703, 713, 704, 706, 726, 727, 694, 696, 716, 697, 728, 717, 729, 699, 700, 720, 721, 1638, 2488, 701]

    conn = sqlite3.connect("bleague_stat.db")
    for club in clubs:
        url = f"https://www.bleague.jp/stats/?year={year}&tab=1&target=player-b1&value=GameCount&o=desc&e=2&club={club}&dt=tot"
        html_doc = get_html(url)
        if html_doc:
            stats, name, team = parse_stats(html_doc)
            date = str(datetime.date.today())

            for j in range(len(name)):
                split_time = stats[j+2].contents[5].text.split(':')
                minute = int(split_time[0]) + round((int(split_time[1]) / 60), 2)

                data = (date, team[j].text, name[j].text,
                        int(stats[j+2].contents[3].text), minute,
                        int(stats[j+2].contents[7].text),
                        int(stats[j+2].contents[8].text),
                        int(stats[j+2].contents[9].text),
                        int(stats[j+2].contents[14].text),
                        int(stats[j+2].contents[17].text),
                        int(stats[j+2].contents[18].text),
                        int(stats[j+2].contents[20].text),
                        int(stats[j+2].contents[21].text),
                        int(stats[j+2].contents[23].text),
                        int(stats[j+2].contents[24].text),
                        int(stats[j+2].contents[25].text),
                        int(stats[j+2].contents[26].text),
                        int(stats[j+2].contents[28].text))

                insert_data_to_db(conn, data)
            time.sleep(3)
    conn.close()

if __name__ == "__main__":
    main()

5.次回予告

次回は作成したデータベースからアドバンス・スタッツを計算します。もしよろしければ続きも見ていただけると幸いです。

この記事が参加している募集

Bリーグ

この記事が気に入ったらサポートをしてみませんか?