【Python】Jリーグの試合スタッツをWebスクレイピングで収集する②

前回はPythonを使ってJリーグの試合を1つ取り上げてスタッツデータを収集する方法を紹介しました。

今回は2022シーズンのJ1全306試合のスタッツデータを収集する方法を紹介します。

今回もPythonライブラリrequests、BeautifulSoupを利用してデータスタジアム株式会社様の運営するFootball LAB(https://www.football-lab.jp/)からデータ収集していきます。

手順は次の通りです。

  1. 2022シーズン全306試合のURLを収集する
  2. URLを引数に試合スタッツを収集するコードを関数化する
  3. 1で収集したURLを2で作成した関数の引数に順番に渡す

まずは特定のチームの2022シーズン全試合URLを収集するコードを書いていきます。

1. 2022シーズン全306試合のURLを収集する

Webページ上の該当箇所を確認します。

引用元:Football LAB(https://www.football-lab.jp/)

該当箇所のHTMLコードを確認します。

<h2 class="boxHeader"><span>Schedule, Results</span></h2>
<table class="statsTbl10">
<thead>
・・・一部省略・・・
</thead>
<tfoot>
・・・一部省略・・・
</tfoot>
<tbody>
<tr>
<td>1</td>
<td data-order="20220219">2.19</td>
<td class="dsktp"><span class="s">(土)</span></td>
<td><a href="/c-os/">C大阪</a></td>
<td><em class="resultD"></em><a href="/y-fm/report/?year=2022&month=02&date=19">2-2</a></td>
<td><span class="home">H</span></td>
<td>日産ス</td>
<td><em>13,737</em></td>
<td>曇</td><td><em>66</em></td>
<td><em>60</em></td> <td><em class="num1st">20.6<span class="s">%</span></em></td>
<td><em>26</em></td>
<td><em>7.7<span class="s">%</span></em></td>
<td><em>64.8<span class="s">%</span></em></td>
<td><em>28.73</em></td>
<td><em>23.61</em></td>
<td><em>107.86</em></td>
<td><em>10.88</em></td>
<td><em>仲川,</em><em>Aロペス</em></td><td>ケヴィン マスカット</td>
</tr>
・・・一部省略・・・
</tbody>
</table>

各試合ページに遷移するためのリンクが「スコア」列に埋め込まれているため、この部分を抽出するコードを記述します。

#HTML解析する
season_page = requests.get("https://www.football-lab.jp/y-fm/match/").text#FC東京の全試合日程ページをスクレイピングする
season_html = BeautifulSoup(season_page, 'html.parser')#HTMLを解析する

#tbodyタグ部分を抽出する
season_html_tbodytag = season_html.find("tbody")

#trタグ部分を抽出する
season_html_trtags = season_html_tbodytag.find_all("tr")

#tdタグだけを抽出する
match_urls = []  #URLを格納する変数を用意する
for season_html_trtag in season_html_trtags:
    season_html_tdtag = season_html_trtag.find_all("td")[4] #5番目だけ抽出したい
    season_html_atag = season_html_tdtag.find("a")
    if season_html_atag is not None:
        match_urls.append(season_html_atag.get('href'))

正しく取得できたかコードを実行して確認します。

match_urls
['/y-fm/report/?year=2022&month=02&date=19',
 '/y-fm/report/?year=2022&month=02&date=23',
 '/y-fm/report/?year=2022&month=02&date=27',
 '/y-fm/report/?year=2022&month=03&date=02',
 '/y-fm/report/?year=2022&month=03&date=06',
 '/y-fm/report/?year=2022&month=03&date=12',
 '/y-fm/report/?year=2022&month=03&date=18',
 '/y-fm/report/?year=2022&month=04&date=02',
 '/y-fm/report/?year=2022&month=04&date=06',
 '/y-fm/report/?year=2022&month=04&date=10',
 '/y-fm/report/?year=2022&month=05&date=07',
 '/y-fm/report/?year=2022&month=05&date=14',
 '/y-fm/report/?year=2022&month=05&date=18',
 '/y-fm/report/?year=2022&month=05&date=21',
 '/y-fm/report/?year=2022&month=05&date=25',
 '/y-fm/report/?year=2022&month=05&date=29',
 '/y-fm/report/?year=2022&month=06&date=18',
 '/y-fm/report/?year=2022&month=06&date=25',
 '/y-fm/report/?year=2022&month=07&date=02',
 '/y-fm/report/?year=2022&month=07&date=06',
 '/y-fm/report/?year=2022&month=07&date=10',
 '/y-fm/report/?year=2022&month=07&date=16',
 '/y-fm/report/?year=2022&month=07&date=30',
 '/y-fm/report/?year=2022&month=08&date=07',
 '/y-fm/report/?year=2022&month=09&date=03',
 '/y-fm/report/?year=2022&month=09&date=07',
 '/y-fm/report/?year=2022&month=09&date=10',
 '/y-fm/report/?year=2022&month=09&date=14',
 '/y-fm/report/?year=2022&month=09&date=18',
 '/y-fm/report/?year=2022&month=10&date=01',
 '/y-fm/report/?year=2022&month=10&date=08',
 '/y-fm/report/?year=2022&month=10&date=12',
 '/y-fm/report/?year=2022&month=10&date=29',
 '/y-fm/report/?year=2022&month=11&date=05']

ここまでで1チーム分の全試合URLを収集することが出来ました。

def get_match_urls(team_url): #試合日程URLを引数にして、1チーム分の全試合URLを収集する関数を定義する
    
    #HTML解析する
    season_page = requests.get("https://www.football-lab.jp" + str(team_url)).text #試合日程ページからHTMLを収集する
    season_html = BeautifulSoup(season_page, 'html.parser')#HTMLを解析する

    #tbodyタグ部分を抽出する
    season_html_tbodytag = season_html.find("tbody")

    #trタグ部分を抽出する
    season_html_trtags = season_html_tbodytag.find_all("tr")

    #tdタグだけを抽出する
    for season_html_trtag in season_html_trtags:
        season_html_tdtag = season_html_trtag.find_all("td")[4] #5番目だけ抽出したい
        season_html_atag = season_html_tdtag.find("a")
        if season_html_atag is not None:
            match_urls.append(season_html_atag.get('href'))

これをJ1リーグ全17チーム分繰り返し実行することで全試合URLを収集します。

そのためには、各チームの試合日程ページのURLを取得する必要があります。

<ul class="simulationSelect"><!--
--><li><a href="/sapp/match/"><img alt="札幌" src="/img/team/SAPP_sdw.png"/><span>札幌</span></a></li><!--
--><li><a href="/kasm/match/"><img alt="鹿島" src="/img/team/KASM_sdw.png"/><span>鹿島</span></a></li><!--
--><li><a href="/uraw/match/"><img alt="浦和" src="/img/team/URAW_sdw.png"/><span>浦和</span></a></li><!--
--><li><a href="/kasw/match/"><img alt="柏" src="/img/team/KASW_sdw.png"/><span>柏</span></a></li><!--
--><li><a href="/fctk/match/"><img alt="FC東京" src="/img/team/FCTK_sdw.png"/><span>FC東京</span></a></li><!--
--><li><a href="/ka-f/match/"><img alt="川崎F" src="/img/team/KA-F_sdw.png"/><span>川崎F</span></a></li><!--
--><li><a href="/shon/match/"><img alt="湘南" src="/img/team/SHON_sdw.png"/><span>湘南</span></a></li><!--
--><li><a href="/shim/match/"><img alt="清水" src="/img/team/SHIM_sdw.png"/><span>清水</span></a></li><!--
--><li><a href="/iwat/match/"><img alt="磐田" src="/img/team/IWAT_sdw.png"/><span>磐田</span></a></li><!--
--><li><a href="/nago/match/"><img alt="名古屋" src="/img/team/NAGO_sdw.png"/><span>名古屋</span></a></li><!--
--><li><a href="/kyot/match/"><img alt="京都" src="/img/team/KYOT_sdw.png"/><span>京都</span></a></li><!--
--><li><a href="/g-os/match/"><img alt="G大阪" src="/img/team/G-OS_sdw.png"/><span>G大阪</span></a></li><!--
--><li><a href="/c-os/match/"><img alt="C大阪" src="/img/team/C-OS_sdw.png"/><span>C大阪</span></a></li><!--
--><li><a href="/kobe/match/"><img alt="神戸" src="/img/team/KOBE_sdw.png"/><span>神戸</span></a></li><!--
--><li><a href="/hiro/match/"><img alt="広島" src="/img/team/HIRO_sdw.png"/><span>広島</span></a></li><!--
--><li><a href="/fuku/match/"><img alt="福岡" src="/img/team/FUKU_sdw.png"/><span>福岡</span></a></li><!--
--><li><a href="/tosu/match/"><img alt="鳥栖" src="/img/team/TOSU_sdw.png"/><span>鳥栖</span></a></li><!--
--></ul>
#ulタグ部分を抽出する
season_html_ultag = season_html.find("ul", class_="simulationSelect")

#aタグ部分を抽出する
season_html_team_urls = season_html_ultag.find_all("a")

#URL部分を抽出する
team_urls =['/y-fm/match/'] #各チームの試合日程ページを格納する変数を用意する
for season_html_team_url in season_html_team_urls:
    team_url = season_html_team_url.get('href')
    team_urls.append(team_url)

正しく取得できたかコードを実行して確認します。

team_urls
['/y-fm/match/',
 '/sapp/match/',
 '/kasm/match/',
 '/uraw/match/',
 '/kasw/match/',
 '/fctk/match/',
 '/ka-f/match/',
 '/shon/match/',
 '/shim/match/',
 '/iwat/match/',
 '/nago/match/',
 '/kyot/match/',
 '/g-os/match/',
 '/c-os/match/',
 '/kobe/match/',
 '/hiro/match/',
 '/fuku/match/',
 '/tosu/match/']

各チームの試合日程ページから順番に試合URLを収集する関数を実行します。

match_urls = []  #URLを格納する変数を用意する

for team_url in team_urls:
    get_match_urls(team_url)
    sleep(3) #サイトに負荷をかけないようにスクレイピングを実行する毎にスリープさせる

上記コードを実行すると、早ければ約1分ほどで各チームの全試合URLの収集が完了します。

2. URLを引数に試合スタッツを収集するコードを関数化する

ここでは、前回の記事で書いたコードを関数化するだけなので簡単です。

def get_match_stats(match_url):

    #HTMLを取得して変数に格納する
    match_html = requests.get("https://www.football-lab.jp"+str(match_url)).text

    #HTMLデータの解析結果を変数に格納する
    match_soup = BeautifulSoup(match_html, 'html.parser')

    #チーム名の部分を抽出
    teams = match_soup.find_all("td", class_="tName")
    #ホームチームを抽出
    team_home = teams[0].text
    #アウェイチームを抽出
    team_away = teams[1].text
    #ゴール数の部分を抽出
    goals = match_soup.find_all("td", class_="numL")
    #ホームチームのゴール数を抽出
    goal_home = goals[0].text
    #アウェイチームのゴール数を抽出
    goal_away = goals[2].text

    #日時、場所の部分を抽出する
    info = match_soup.find_all("div", class_="boxHalfSP")

    #リストの1番目に格納されている「日付」を変数に格納する
    date = info[0].text

    #リストの2番目に格納されている「場所」を変数に格納する
    location = info[1].text

    #天気、気温、芝、観客数の部分を抽出する
    info2 = match_soup.find("div", class_="infoList")
    info2 = info2.find_all("dd")

    #リストの1番目に格納されている「天気」を変数に格納する
    weather = info2[0].text

    #リストの2番目に格納されている「気温」を変数に格納する
    temp = info2[1].text

    #リストの3番目に格納されている「芝」を変数に格納する
    grass = info2[2].text

    #リストの4番目に格納されている「観客数」を変数に格納する
    audience = info2[3].text

    #基本情報のデータフレームをホームとアウェイそれぞれ作成する
    df_info_home = pd.DataFrame(
                    data={'日付': [date], 
                          '場所': [location],
                          '天気': [weather],
                          '気温(℃)': [temp],
                          '芝': [grass],
                          '観客(人)': [audience],
                          'チーム名': [team_home],
                          '対戦相手': [team_away],
                          'Home/Away': "Home",
                          '得点': [goal_home],
                          '失点': [goal_away]}
    )

    df_info_away = pd.DataFrame(
                    data={'日付': [date], 
                          '場所': [location],
                          '天気': [weather],
                          '気温(℃)': [temp],
                          '芝': [grass],
                          '観客(人)': [audience],
                          'チーム名': [team_away],
                          '対戦相手': [team_home],
                          'Home/Away': "Away",
                          '得点': [goal_away],
                          '失点': [goal_home]}
    )

    #テーブルを特定し変数に格納する
    tables = match_soup.find_all("table", class_="statsTbl6")
    table = tables[1]

    #テーブル内に複数存在するtdタグを全て抽出する
    td_tags = table.find_all("td")

    #配列を用意して抽出したtdタグ内のテキストデータを格納する
    stats = []
    for td_tag in td_tags:
        stats.append(td_tag.text)

    stats_home_num = stats[3::8] #ホームチームの総数を格納する
    stats_home_per = stats[2::8] #ホームチームの成功率を格納する
    stats_away_num = stats[5::8] #アウェイチームの総数を格納する
    stats_away_per = stats[6::8] #ホームチームの成功率を格納する

    #スタッツの配列をホームとアウェイそれぞれ作成する
    stats_home = stats_home_num + stats_home_per + stats_away_num + stats_away_per
    stats_away = stats_away_num + stats_away_per + stats_home_num + stats_home_per

    #データフレームに変換する

    df_stats_home = pd.DataFrame([stats_home])
    df_stats_away = pd.DataFrame([stats_away])

    stats_columns = stats[0::8] #データ項目名を格納する

    #カラム名が格納された配列を作成する
    columns = [] + stats_columns
    for stats_column in stats_columns:
        column_name = str(stats_column) + "_成功率"
        columns.append(column_name)
    for stats_column in stats_columns:
        column_name = "(被)" + str(stats_column)
        columns.append(column_name)
    for stats_column in stats_columns:
        column_name = "(被)" + str(stats_column) + "_成功率"
        columns.append(column_name)

    #データフレームにカラム名を適用する
    df_stats_home.columns = columns
    df_stats_away.columns = columns

    #元の配列で値が入っていないインデックスを検索する
    home_null_index = [i for i, x in enumerate(stats_home) if x == '-']
    away_null_index = [i for i, x in enumerate(stats_away) if x == '-']

    #データフレームから値の入っていない列を削除する
    df_stats_home = df_stats_home.drop(df_stats_home.columns[home_null_index], axis=1)
    df_stats_away = df_stats_away.drop(df_stats_away.columns[away_null_index], axis=1)

    #df_info、df_statsを行方向に結合する
    df_match_home = pd.concat([df_info_home, df_stats_home], axis=1)
    df_match_away = pd.concat([df_info_away, df_stats_away], axis=1)

    #df_match_home、df_match_awayを列方向に結合する
    df_match = pd.concat([df_match_home, df_match_away], axis=0)

    df_match['日付'] = df_match['日付'].str[:-15] #後ろから15文字分を削除する
    df_match['日付'] = df_match['日付'].str.replace('.', '/') #ドットをスラッシュに変換する

    df_match['気温(℃)'] = df_match['気温(℃)'].str.rstrip('℃') #℃を削除する

    df_match['観客(人)'] = df_match['観客(人)'].str.rstrip('人') #人を削除する
    df_match['観客(人)'] = df_match['観客(人)'].str.replace(',', '') #カンマを削除する

    df_match['総走行距離'] = df_match['総走行距離'].str.rstrip('m') #mを削除する
    df_match['総走行距離'] = df_match['総走行距離'].str.replace(',', '') #カンマを削除する

    df_match['(被)総走行距離'] = df_match['(被)総走行距離'].str.rstrip('m') #mを削除する
    df_match['(被)総走行距離'] = df_match['(被)総走行距離'].str.replace(',', '') #カンマを削除する

    df_match = df_match.rename(columns={'総走行距離': '総走行距離(m)', '(被)総走行距離': '(被)総走行距離(m)'}) #カラム名を変更する

    #括弧で括られているカラムを特定する
    columns_per = df_match.filter(like='_成功率', axis=1).columns

    for column_per in columns_per:
        df_match[column_per] = df_match[column_per].str.replace('(', '') #前括弧を削除する
        df_match[column_per] = df_match[column_per].str.replace(')', '') #後括弧を削除する

    #%で表示されているカラムを特定する
    columns_per = df_match.filter(like='率', axis=1).columns.tolist()

    for column_per in columns_per:
        df_match[column_per] = df_match[column_per].str.replace('%', '') #%を削除する

    #勝ち点の列を追加する
    conditionlist = [
        (df_match['得点'] > df_match['失点']),
        (df_match['得点'] == df_match['失点']),
        (df_match['得点'] < df_match['失点'])]

    choicelist = ['3', '1', '0']
    df_match['勝ち点'] = np.select(conditionlist, choicelist, default='Not Specified')

    #1周目だけカラム名を取得して格納する
    if i == 0:
        columns = df_match.columns.tolist() #データフレームのカラムを取得してリスト化する
        for column in columns:
            match_columns.append(column) #予め用意した変数に格納する

    #データフレームをリスト化する
    df_match_array0 = df_match.iloc[0].tolist() #データフレーム1行目をリスト化する
    df_match_array1 = df_match.iloc[1].tolist() #データフレーム2行目をリスト化する
    match_stats.append(df_match_array0) #予め用意した変数に格納する
    match_stats.append(df_match_array1) #予め用意した変数に格納する

例えば、以下のように引数を渡すことで、1試合分のスタッツを収集することができます。

get_match_stats('/y-fm/report/?year=2022&month=02&date=19')

3. 1で収集したURLを2で作成した関数の引数に順番に渡す

各試合URLから順番にスタッツを収集する関数を実行します。

match_columns = [] #カラム名を格納するリストを用意する
match_stats = [] #データ項目を格納するリストを用意する

for i, match_url in enumerate(match_urls):
    get_match_stats(match_url)
    sleep(3) #サイトに負荷をかけないようにスクレイピングを実行する毎にスリープさせる

上記コードを実行すると、少し時間がかかりますが各チームの全試合URLの収集が開始します。

完了したら収集したデータを用いてPythonデータフレームを作成します。

df_stats = pd.DataFrame(data = match_stats, columns = match_columns)

今回の収集方法では、同じスタッツを両チームの試合URLから収集しているため、重複するデータを削除する必要があります。

#同じスタッツを両チームのページで収集しているため重複を削除する
df_stats = df_stats.drop_duplicates()

データフレームが正しく作成できているか確認します。

df_stats.head(10)

作成したデータフレームに対してデータ型を定義します。

#float型のカラム名を配列で用意する
columns_per = df_stats.filter(like='率', axis=1).columns.tolist()
column_float = ['気温(℃)','ゴール期待値','(被)ゴール期待値']
column_float = column_float + columns_per

#int型のカラム名を配列で用意する
column_int = ['観客(人)', '得点', '失点', 'シュート', '枠内シュート', 'PKによるシュート', 'パス', 'クロス',
              '直接FK', '間接FK', 'CK', 'スローイン', 'ドリブル', 'タックル', 'クリア', 'インターセプト',
              'オフサイド', '警告', '退場', '30mライン進入', 'ペナルティエリア進入', '総走行距離(m)', 'スプリント',
              '攻撃回数', '(被)シュート', '(被)枠内シュート', '(被)PKによるシュート', '(被)パス', '(被)クロス', '(被)直接FK', '(被)間接FK',
              '(被)CK', '(被)スローイン', '(被)ドリブル', '(被)タックル', '(被)クリア', '(被)インターセプト',
              '(被)オフサイド', '(被)警告', '(被)退場', '(被)30mライン進入', '(被)ペナルティエリア進入',
              '(被)総走行距離(m)', '(被)スプリント', '(被)攻撃回数', '勝ち点']

#データ型を変換する
df_stats['日付'] = pd.to_datetime(df_stats['日付'])
df_stats[column_float] = df_stats[column_float].astype('float')
df_stats[column_int] = df_stats[column_int].astype('int')

最後に作成したデータフレームをcsvで取得します。

#csvファイルに出力して確認する
df_stats.to_csv("df_stats.csv", index=False)

以上、今回は2022シーズンのJ1全306試合のスタッツデータをWebスクレイピングで収集する方法を紹介しました。
次回は、収集したデータを使ってPythonライブラリ「Pandas」の基本的な使い方を紹介します。