見出し画像

プロ野球のデータが入りにくいので10年分のデータをスクレイピングした

更新日  :2020/08/02
更新内容 :プログラムの修正
最終更新日:2020/11/25
更新内容 :プログラムの修正

プロ野球のデータは手に入りにくいです。
公式のデータベースもなければ、NPBのサイトで掲載されている程度です。
そのため、データを取るにはスクレイピングが必要です。
この記事を書いていく中でデータを取るのにも、いちいちデータをスクレイピングしてセイバーメトリクス指標を計算してとなかなかの手間です。
もういっそのことデータを取ったほうがいいんじゃない!?
と、思ったので今回はプロ野球Freakさんから10年分の成績データと年収データをスクレイピングしてcsvデートとして保存していきます。


スクレイピングさせていただくサイト

今回スクレイピングさせていただくサイトはおなじみのこちらです。

テーブル構造で綺麗にデータが2009年から2020年までありますのでこちらのサイトからスクレイピングをさせていただこうと思います。
2020年7月25日現在robot.txtはなく、スクレイピングの制限に関する記載はないので、不正アクセスや

全体コード

全体コードは以下になります。
データをスクレイピングする関数とスクレイピングするための処理をしていくメイン関数の2種類です。
メイン関数はデータの初期化、スクレイピングの繰り返し処理、データの保存のパートになります。
下のコードを実行すると2009年から2020年のプロ野球の成績データの投手、野手のデータをスクレイピングしてcsvファイルとして保存してくれます。
csvファイルは、投手は基本成績とセイバーメトリクスの指標が入っているデータの2セットできます。
野手は基本成績とセイバーメトリクスの指標とその他のデータセットの3種類のcsvファイルが作られます。
高速でアクセスするとサイトに負荷をかけてしまうので、ところどころ処理を止める関数を入れています。
そのため、データをすべて取り終わるまでに1時間程度かかります。

csvファイル間では重なっている列も多くあります。
重なっている部分はcsvファイルをマージして1つのテーブル形式のファイルにしたときに除去します。
これらの加工はまた別の記事で紹介します。

import time
import pandas as pd

def ScrapingData(url):
   data=pd.read_html(url)
   data=data[0]
   data.columns=data.columns.droplevel(0)
   data=data.replace('-','-1')
   data=data.replace('∞','9999')
   data_player=data2['選手名']
   data.drop('選手名',inplace=True,axis=1)
   data=data.astype('float')
   data['選手名']=data_player
   data['チーム']=team
   data['年']=year
   return data

if __name__ == "__main__":
   start_time = time.time()
   base_url = 'https://baseball-data.com/'
   #19 /stats/hitter2-t/tpa-1.html'
   pitcher_dataset_normal = pd.DataFrame()
   pitcher_dataset_total = pd.DataFrame()
   
   hitter_dataset_normal=pd.DataFrame()
   hitter_dataset_total=pd.DataFrame()
   hitter_dataset_other=pd.DataFrame()
   
   name_list=['g','yb','t','c','d','s','l','h','e','m','f','bs']
   year_list=['09','10','11','12','13','14','15','16','17','18','19','']
   player_type=['pitcher','hitter']
   print('now scraping')
   try:
       for name in name_list:
           for year in year_list:
               for player in player_type:
                   if player == 'pitcher':      
                       time.sleep(2)                        
                       p_normal_url=base_url + year + '/stats/'+str(player)+ '2' + '-' + str(name) + '/ip3-1.html'
                       p_total_url=base_url + year + '/stats/'+str(player)+ '3' + '-' + str(name) + '/ip3-1.html'
                       
                       print(p_normal_url)
                       p_normal_data=ScrapingData(p_normal_url,team,year)
                       time.sleep(5)
                       print(p_total_url)
                       p_total_data=ScrapingData(p_total_url,team,year)
                   
                       pitcher_dataset_normal=pd.concat([pitcher_dataset_normal,p_normal_data],axis=0)
                       pitcher_dataset_total=pd.concat([pitcher_dataset_total,p_total_data],axis=0)
                       
                   if player == 'hitter':
                       time.sleep(2)
                       h_normal_url=base_url + year + '/stats/'+str(player)+ '2' + '-' + str(name) + '/tpa-1.html'
                       h_total_url=base_url + year + '/stats/'+str(player)+ '3' + '-' + str(name) + '/tpa-1.html'
                       h_other_url=base_url + year + '/stats/'+str(player)+ '4' + '-' + str(name) + '/tpa-1.html'
                       
                       print(h_normal_url)
                       h_normal_data = ScrapingData(h_normal_url,team,year)
                       time.sleep(5)
                       print(h_total_url)
                       h_total_data = ScrapingData(h_total_url,team,year)
                       time.sleep(5)
                       print(h_other_url)
                       h_other_data = ScrapingData(h_other_url,team,year)

                       hitter_dataset_normal=pd.concat([hitter_dataset_normal,h_normal_data],axis=0)
                       hitter_dataset_total=pd.concat([hitter_dataset_total,h_total_data],axis=0)
                       hitter_dataset_other=pd.concat([hitter_dataset_other,h_other_data],axis=0)
                       
   except:
       print('exception')
       
   hitter_dataset_normal.to_csv('hitter_dataset_normal.csv',index=False)
   hitter_dataset_total.to_csv('hitter_dataset_total.csv',index=False)
   hitter_dataset_other.to_csv('hitter_dataset_other.csv',index=False)
   pitcher_dataset_normal.to_csv('pitcher_dataset_normal.csv',index=False)
   pitcher_dataset_total.to_csv('pitcher_dataset_total.csv',index=False)
   print('elapsed time[sec]:',time.time()-start_time)


コード解説

コードの詳細を説明していきます。
データの初期化、スクレイピングの繰り返し処理、データの保存、スクレイピング関数の4パートに分けて説明します。

データの初期化

データの初期化部分の処理になります。
データ格納用の変数とURLを作るためのリストを作成します。

時間関数で現時刻を格納します。

   start_time = time.time()​

まずはサイトにアクセスできないとスクレイピングができませんので、ベースとなるURLを用意します。

base_url = 'https://baseball-data.com/'

スクレイピングしたデータを格納しておく変数を用意します。
今回は投手で2つのデータセット、野手で3つのデータセットを取ってくる形になりますので、以下のように合計5つのデータフレーム型を初期化しておきます。

   pitcher_dataset_normal = pd.DataFrame()
   pitcher_dataset_total = pd.DataFrame()
   
   hitter_dataset_normal=pd.DataFrame()
   hitter_dataset_total=pd.DataFrame()
   hitter_dataset_other=pd.DataFrame()

次にURLを作成するための文字列をリストに格納します。
リストは3種類用意します。
球団別にURLが分かれていますので、球団別のURLの文字列をname_listに用意します。
年度のデータは09、10、11・・・とURLが分かれています。
分かりやすいですね。
スクレイピングしやすくてマジ泣きそうですw
年度のURLを格納するリストがyear_listです。
最後に野手と投手でまたURLが分かれます。
野手はhitter、投手はpitcherとURLになっていますので、それら2つの文字列をplayer_typeリストに格納します。
以上で取りたいデータがあるサイトすべてにアクセスできる準備が整いました。

   
   name_list=['g','yb','t','c','d','s','l','h','e','m','f','bs']
   year_list=['09','10','11','12','13','14','15','16','17','18','19','']
   player_type=['pitcher','hitter']

print関数で開始の合図です。

   print('now scraping')

スクレイピングの繰り返し処理

アクセスするURLをfor文の繰り返し処理で作成し、スクレイピングして取ったデータを格納用に用意したデータフレームにつなげていきます。
繰り返し処理には、例外処理を入れています。
エラーをしたらexceptionと表示されて処理が終わります。
それでは処理を見ていきましょう。

try:で例外処理にを開始します。
データの初期化で格納したリストをfor文で取り出してURLを作っていきます。
そのためのfor文部分が以下になります。

   try:
       for name in name_list:
           for year in year_list:
               for player in player_type:

for文で取り出した文字列をつなげていきURLを作成します。

p_normal_url=base_url + year + '/stats/'+str(player)+ '2' + '-' + str(name) + '/ip3-1.html'
p_total_url=base_url + year + '/stats/'+str(player)+ '3' + '-' + str(name) + '/ip3-1.html'

投手のデータからスクレイピングしていくためのif文になります。
pitcherの文字列のときは投手のスクレイピングを行っていくための分岐処理をしています。
time.sleep(2)で2秒ここで処理を止めます。
スクレイピング関数前にはこまめにあえて処理を止める関数を今回は入れています。

if player == 'pitcher':      
    time.sleep(2)

URLをprintで表示してからScrapingData関数にデータを取りたいURLを引数にして、結果をp_normal_dataに渡します。
time.sleepで5秒待ち、同様にp_total_dataにScrapingDataの結果を格納します。

print(p_normal_url)
p_normal_data=ScrapingData(p_normal_url)
time.sleep(5)
print(p_total_url)
p_total_data=ScrapingData(p_total_url)

取ってきたデータを縦にくっつけていく処理になります。
投手は2種類のデータセットを取るので、それぞれのデータフレームにスクレイピングしてきたデータをつなげていきます。

pitcher_dataset_normal=pd.concat([pitcher_dataset_normal,p_normal_data],axis=0)
pitcher_dataset_total=pd.concat([pitcher_dataset_total,p_total_data],axis=0)

野手側も処理は同じ流れになります。
野手の場合はhitterという文字列により分岐するように処理をしています。
野手は3種類のデータセットを取りますので、同じ処理が3個ずつあります。

if player == 'hitter':
   time.sleep(2)
   h_normal_url=base_url + year + '/stats/'+str(player)+ '2' + '-' + str(name) + '/tpa-1.html'
   h_total_url=base_url + year + '/stats/'+str(player)+ '3' + '-' + str(name) + '/tpa-1.html'
   h_other_url=base_url + year + '/stats/'+str(player)+ '4' + '-' + str(name) + '/tpa-1.html'
                      
   print(h_normal_url)
   h_normal_data = ScrapingData(h_normal_url,team,year)
   time.sleep(5)
   print(h_total_url)
   h_total_data = ScrapingData(h_total_url,team,year)
   time.sleep(5)
   print(h_other_url)
   h_other_data = ScrapingData(h_other_url,team,year)

   hitter_dataset_normal=pd.concat([hitter_dataset_normal,h_normal_data],axis=0)
   hitter_dataset_total=pd.concat([hitter_dataset_total,h_total_data],axis=0)
   hitter_dataset_other=pd.concat([hitter_dataset_other,h_other_data],axis=0)
                       

処理を数十回レベルで回すので、一度のエラーで処理が終わってしまうと被効率的です。
URLですので規則的に基本的に書かれていますが、万が一連番が1個崩れているということもあり得るかもしれません。
そういったちょっとの違いで処理が取れないよりも1年分URLが合わなくて取れなかったとしてもほかの年度が取れたほうが効率的にデータセットが取れます。
そのため、例外処理のtryをいれて、exceptionでスクレイピングできなかったとメッセージとしてerrorと出力して次の処理に進めるようにさせています。
この例外処理のexceptionに当たる部分が以下になります。

   except:
       print('exception')

最後にデータの保存をして、トータルの処理時間を計算して表示して終わります。

   hitter_dataset_normal.to_csv('hitter_dataset_normal.csv',index=False)
   hitter_dataset_total.to_csv('hitter_dataset_total.csv',index=False)
   hitter_dataset_other.to_csv('hitter_dataset_other.csv',index=False)
   pitcher_dataset_normal.to_csv('pitcher_dataset_normal.csv',index=False)
   pitcher_dataset_total.to_csv('pitcher_dataset_total.csv',index=False)
   print('elapsed time[sec]:',time.time()-start_time)

スクレイピング関数

スクレイピング関数の内容を説明していきます。
関数内は10行でできています。

def ScrapingData(url,team,year):
   data=pd.read_html(url)
   data=data[0]
   data.columns=data.columns.droplevel(0)
   data=data.replace('-','-1')
   data=data.replace('∞','9999')
   data_player=data['選手名']
   data.drop('選手名',inplace=True,axis=1)
   data=data.astype('float')
   data['選手名']=data_player
   data['チーム']=team
   data['年']=year
   return data

urlを引数にして、受け取ったURLを基にスクレイピングをしていきます。

def ScrapingData(url):

表形式になっているデータを取ります。

data=pd.read_html(url)

取ってきたデータはリスト形式になっていますので、データフレームの形式として扱うためにリスト内の要素を取り出します。
リストの要素には、テーブル構造になっている部分一式が格納されていて、形式がデータフレーム型のため、要素を取り出す形でデータフレーム型になります。

data=data[0]

このデータセットはマルチカラムになっており2重で同じカラム名が設定されています。
1段のカラムにするためにカラムを1段捨てる処理をします。

data.columns=data.columns.droplevel(0)

データは出場していない選手などデータがない項目は’-’のハイフンで埋められているので、ハイフンをデータがなかったという意味も込めて-1と欠損値処理をしておきます。

data=data.replace('-','-1')

出場が少ない選手の場合、K/BBの指標などは奪三振数回、四死球0の投手がいたりして、∞の無限大と記録されています。
∞ですと、数値としてこの項目を扱えないので、9999と大きい数字で埋めます。

data=data.replace('∞','9999')

数値の型変換をするため、文字列の選手名項目を別の変数に格納します。

data_player=data['選手名']

データフレームから選手名列を外します。

data_player=data2['選手名']

数値にする列をすべてfloat型に変えます。

data=data.astype('float')

一度外していた選手名列を付けなおします。

data['選手名']=data_player

URLの識別で使ったチームと年度を用いて、チーム列と年列を作成してデータを格納します。
何年のデータでどこのチーム所属しているかが分かるようになります。

data['チーム']=team
data['年']=year

最後にreturnでスクレイピングしたデータフレームを返します。

return data

ネストが深いコードになっていますが、100行以内のプログラムになりますのでコードも追える処理ではないでしょうか。

今回のデータとコードファイルはこちら!(有料)

興味ある方や上のコードを欲しいという方、データが欲しい方は以下からダウンロードできます。
今回のスクレイピングコードになります。

ここから先は

116字 / 6ファイル
この記事のみ ¥ 400
期間限定 PayPay支払いすると抽選でお得に!

よろしければサポートをよろしくお願いします。サポートいただいた資金は活動費に使わせていただきます。