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

Pythonを利用したデータ分析を学ぶために、サッカーの試合データを題材にしたいと思います。

データを収集するための手段は様々ありますが、Webサイト上のデータを収集する場合には、Pythonを利用したWebスクレイピングがおススメです。

今回は、JavaScriptが利用されていない静的なWebページ上のデータ収集を行う方法を紹介していきます。

手順は次の通りです。

  1. Pythonライブラリ(requests、BeautifulSoup)をインストールする
  2. WebページのHTMLデータを取得、解析する
  3. HTMLの中から必要な情報を抽出する
  4. データフレーム化する
  5. データの前処理を行う
  6. データ型の定義を行う

早速コードを書いていきます。

#ライブラリをインストールする
import requests
from bs4 import BeautifulSoup

今回紹介する方法では、次の2つのPythonライブラリを利用します。

  • requests・・・指定したURLのWebページのHTMLデータを取得してくれる
  • BeautifulSoup・・・複雑な構造のHTMLデータを解析してくれる

今回は、データスタジアム株式会社様の運営するFootball LAB(https://www.football-lab.jp/)からJリーグの試合を1つ取り上げてデータ収集の手順を紹介していきます。

#URLを変数に格納する
match_url = "https://www.football-lab.jp/y-fm/report/?year=2022&month=02&date=19"

#HTMLを取得して変数に格納する
match_html = requests.get(match_url).text

実際にrequestsで取得したHTMLデータの中身を見てみましょう。

print(match_html) #HTMLの中身を見てみる
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:fb="http://www.facebook.com/2008/fbml">
<head>
<meta http-equiv="Content-Language" content="ja" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta http-equiv="Content-Style-Type" content="text/css" />
<meta http-equiv="Content-Script-Type" content="text/javascript" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="index,follow" /><title>横浜F・マリノス 2022マッチレポート | 2月19日 vs C大阪 | データによってサッカーはもっと輝く | Football LAB</title>
<meta name="keywords" content="サッカー,Jリーグ,データ,ランキング,フットボールラボ,Football LAB,チャンスビルディングポイント,CBP,J1,J2,J3,横浜F・マリノス,マッチレポート,ゲームレポート,レポート,試合" />
<meta name="description" content="フットボールラボ(Football LAB)はサッカーをデータで分析し、新しいサッカーの観戦方法を伝えるサッカー情報サイトです。選手のプレーを評価するチャンスビルディングポイントやプレースタイル指標、チームの戦術を評価するチームスタイル指標といった独自のデータを開発しています。データを活用してサッカーに新しい視点を提供するとともに、レポートやコラムを通してJリーグの試合を分析・解説します。" />
・・・

HTMLデータを取得できていることを確認できました。

続いて、取得したHTMLデータをBeautifulSoupで解析します。

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

ここまででデータを取得するための下準備が完了しました。

ここからは、実際に取得したいデータがHTML上でどこに位置しているかを確認し、BeautifulSoupの解析結果から抽出していきます。

まず、試合の基本データを抽出していきます。Webページ上の該当箇所を確認します。

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

まずは、上部の試合結果のHTMLを確認します

<tr>
<td class="tName r"><span><a href="/y-fm/">横浜F・マリノス</a></span></td>
<td class="numL c">2</td>
<td class="numL c">-</td>
<td class="numL c">2</td>
<td class="tName l"><span><a href="/c-os/">セレッソ大阪</a></span></td>
</tr>

この中から「横浜F・マリノス」「2」「2」「セレッソ大阪」の4箇所を抽出していきます。

#チーム名の部分を抽出
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

抽出結果を確認します。

print(team_home)
print(team_away)
print(goal_home)
print(goal_away)
横浜F・マリノス
セレッソ大阪
2
2

続いて、下部の基本情報のHTMLコードを確認します

<div class="boxHalfSP l">2022.2.19 14:00 Kick Off</div>
<!---->
<div class="boxHalfSP r">日産スタジアム</div>
<!-- 環境情報 -->
<div class="infoList">
<dl><dt>天気</dt><dd>曇</dd></dl>
<dl><dt>気温</dt><dd>9℃</dd></dl>
<dl><dt>芝</dt><dd>全面良芝</dd></dl>
<dl><dt>観客数</dt><dd>13,737<span class="s">人</span></dd></dl>
</div>

この中から「2022.2.19 14:00 Kick Off」「日産スタジアム」「曇」「9℃」「全面良芝」「13,737」の5箇所を取得していきます。

#日時、場所の部分を抽出する
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

抽出結果を確認します。

print(date)
print(location)
print(weather)
print(temp)
print(grass)
print(audience)
2022.2.19 14:00 Kick Off
日産スタジアム
曇
9℃
全面良芝
13,737人

ここまでの抽出結果をデータフレームに変換します。

#ライブラリをインストールする
import pandas as pd
import numpy as np
df_info = pd.DataFrame(
            data={'日付': [date], 
                  '場所': [location],
                  '天気': [weather],
                  '気温(℃)': [temp],
                  '芝': [grass],
                  '観客(人)': [audience],
                  'ホームチーム': [team_home],
                  'アウェイチーム': [team_away],
                  'ホーム得点': [goal_home],
                  'アウェイ得点': [goal_away]}
)

作成したデータフレームを確認します。

df_info

ここまでで試合の基本情報の抽出~データフレーム作成までが完了しました。

続いて、試合のスタッツを抽出していきます。Webページ上の該当箇所を確認します。

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

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

<table class="statsTbl6">
<thead>
<tr>
<th class="dsktp">今季平均</th>
<th class="">成功率</th>
<th class="">総数</th>
<th class="dsktp"></th>
<th class="">総数</th>
<th class="">成功率</th>
<th class="dsktp">今季平均</th>
</tr>
</thead>
<tbody>
<tr class="tr3 sp"><td colspan="4">ゴール期待値</td></tr>
<tr class="tr2">
<td class="dsktp numNA c">1.835</td>
<td class="c numNA">-</td>
<td class="c num1st">3.204</td>
<td class="dsktp c">ゴール期待値</td>
<td class="c">0.892</td>
<td class="c numNA">-</td>
<td class="dsktp numNA c">1.260</td>
</tr>
・・・

テーブル内のデータを抽出していきます。

#テーブルを特定し変数に格納する
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_all = stats_home_num + stats_home_per + stats_away_num + stats_away_per

#データフレームに変換する
df_stats = pd.DataFrame([stats_all])

作成したデータフレームを確認する。

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

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

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

データフレームにカラム名が適用されていることを確認する。

値の入っていない列を削除する。

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

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

値が入っていない列が削除されていることを確認する。

基本情報のデータフレーム「df_info」とスタッツのデータフレーム「df_stats」を行方向に結合する。

#df_info、df_statsを行方向に結合する
df_match = pd.concat([df_info, df_stats], axis=1)

作成したデータフレームを確認する。

df_match

1試合1レコードとするデータフレームの作成方法は以上です。

続いて、1試合について両チームの視点から2つのデータフレームを作成する方法を紹介します。

#基本情報のデータフレームをホームとアウェイそれぞれ作成する
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]}
)
#スタッツの配列をホームとアウェイそれぞれ作成する
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['日付'] = 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')

最後にデータ型の定義を行います。

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

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

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

以上、今回はPythonを使ってJリーグの試合を1つ取り上げてスタッツデータを収集する方法を紹介しました。
次回は、2022シーズンのJ1全306試合のスタッツデータを収集する方法を紹介します。