エンジニア データサイエンティスト

Python機械学習「ホームラン数予測②」データ収集「WEBスクレイピング」編!

2020年11月17日

Python機械学習

 

PythonのプログラミングでWEBサイトの情報を取ってきたいけど、
どうやってやればいいんだろう。。
CSVデータに出力したいんだけど。。

 

こんにちは、古賀です!

 

本記事は、

はてな

「Python機械学習でホームラン数を予測する」のデータ収集「WEBスクレイピング」編

です!

 

機械学習をするには、まず「データ」が必要です!

今回は「2020年のプロ野球データ」を、

「WEBスクレイピング」という方法を使って「データ収集」を行い、

それをCSVファイルに出力します!

 

「機械学習の導入部分」

「WEBスクレイピングの基本」

を学びましょう!

 

前回の続きになりますので、

前回の記事を読まれていない方はまずはこちらをご覧ください!

Python機械学習
Python機械学習「ホームラン数予測①」概要編!

  PythonってAIや機械学習ができるって言うけど、 誰にでもできるんだろうか。。 やっぱり難しいコードを書いたりするんだろうか。。 イメージを掴みたいな。。   こんにちは、 ...

続きを見る

 

また、実行環境は「JupyterLab」を使っていきますので、

「JupyterLab」の記事を読まれていない方は、こちらもご確認ください!

JupyterLab
JupyterLabとは?Python実行環境「JupyterLab」の概要と基本的な使い方!

  Pythonの実行環境「JupyterLab」が起動できるところまできたけど、 どういう風に使っていけばいいんだろう?? そもそも「JupyterLab」ってなんだ??   こ ...

続きを見る

 

自己紹介が遅れましたが、

わたしは大学卒業後、上場IT企業に就職し、プログラマー、システムエンジニアとして

約10年間働いておりまして、その後は様々な活動をしております。

プロフィールの詳細はこちらです。

野球
プロフィール

こんにちは、古賀正雄です。現在36歳です。 簡単ではありますが、こちらのページで自己紹介とこのブログについてお話します。 高校時代 学生時代は主に野球をしていました。 進学先の高校も野球で選びました。 ...

続きを見る

 

※YouTubeに同内容を公開しております。

Python機械学習「WEBスクレイピング」の概要

分析

機械学習をするためには、まず「データ」が必要です。

そのデータを今回は、

WEBサイトから情報を取ってくる「WEBスクレイピング」

という方法で取ってきます。

 

WEBサイトは、「プロ野球データFREAK」様のサイトのこのページをお借りしたいと思います。

プロ野球データFREAK

この表形式で表示されているデータを、CSVファイルに出力することが今回のゴールです!

手順としては、

  1. プロ野球データが表示されているURLにアクセス
  2. HTMLデータを取得
  3. HTMLデータから各項目の値を取得し、配列変数にセット
  4. 配列変数を表形式データに変換
  5. 表形式データをCSVファイルに出力

です!

Python機械学習「WEBスクレイピング」をしてみる!

仕事

今回のコード全体です。

今からこのコードを1から説明していきます!

#WEB上のデータ(HTML)にアクセス&取得するために使用
import urllib
#HTMLからデータを抽出するために使用
from bs4 import BeautifulSoup
#表形式でデータ格納するために使用
import pandas as pd

url = 'https://baseball-data.com/stats/hitter2-all/tpa-1.html'
html = urllib.request.urlopen(url)
soup = BeautifulSoup(html, 'html.parser')

#HTMLから表(tableタグ)の部分を全て取得する
table = soup.find_all('table')[0]
#テーブルから行データを取得
rows = table.find_all('tr')

#データ格納用
headData = []
playerData = []

for i, row in enumerate(rows):    
    #1行目
    if i == 0:
        #ヘッダ部取得
        for headerValue in row.find_all('th'):
            headData.append(headerValue.get_text())
    #2行目~最終行未満
    elif (i > 0) and (i + 1 < len(rows)):
        #明細部取得
        playerRow = []
        for playerValue in row.find_all('td'):
            playerRow.append(playerValue.get_text())
        playerData.append(playerRow)

#DataFrameに変換
df = pd.DataFrame(data = playerData, columns = headData)

#CSV出力
df.to_csv('playerData.csv', sep = ',', header = True, index = False)

WEBスクレイピングで使用するライブラリを宣言

はじめに「WEBスクレイピング」をするために使用するライブラリを宣言します。

※ライブラリをモジュールと呼んだりもしますが、「ライブラリ」で統一します。

#WEB上のデータ(HTML)にアクセス&取得するために使用
import urllib
#HTMLからデータを抽出するために使用
from bs4 import BeautifulSoup
#表形式でデータ格納するために使用
import pandas as pd

Pythonでは「import ライブラリ名」とすることで、ライブラリを使えるようになります。

「from ライブラリ名 import クラス名など」という書き方でライブラリ全体ではなく、

そのライブラリの一部分だけ使えるようにすることもできます。

また「as 名前」とすることで、別名を付けることもできます。

 

今回は、

  • WEB上のデータ(HTML)にアクセス&取得するために使用する「urllib」
  • HTMLからデータを抽出するために使用する「BeautifulSoup」
  • 表形式でデータ格納するために使用する「Pandas」

を宣言しておきます。

WEBサイトからHTMLデータの取得

次に、WEBサイトからHTMLデータを取得していきます。

難しいことは何もありません。

先程宣言したライブラリ「urllib」と「BeautifulSoup」を使うだけです。

url = 'https://baseball-data.com/stats/hitter2-all/tpa-1.html'
html = urllib.request.urlopen(url)
soup = BeautifulSoup(html, 'html.parser')

1行目は、変数に「取ってきたいデータが表示されているサイトのURL」をセットしています。

変数の説明はいらないと思いますが、変数は値を格納しておくためのものです。

ちなみにPythonでは「str url」というように、変数の前にデータ型の宣言を書く必要はありません。

「この変数は型は何だっけ?」

となりがちなので、「type(変数)」を使うことで確認できるので覚えておきましょう!

print(type(url))

<class 'str'>

「<class 'str'>」と表示され、文字型だということが確認できます。

 

2行目は、そのURLの変数を「urllibのHTMLデータを取得する関数」に渡して、

「html」という変数にセットしています。

※もちろん変数を使わず、関数に直接WEBサイトのURLを渡しても問題なしです。

 

3行目は、取得してきたHTMLデータを「BeautifulSoup」を使って、

HTMLデータから値を取得できるように変換を行っています。

いまいちピンとこないかもしれませんが、2~3行目は決まり事のような感じなので、

HTMLから値を取ってくる前の下準備と思っておいてください。

 

これでHTMLデータが取得できました!

表形式HTMLデータの構造

実際に手を動かす前に、データが表形式で表示されている「HTML」の構造を確認しておきましょう。

プロ野球データのサイトで「右クリック⇒検証」を押して、

「デベロッパーツール」を立ち上げます。

 

これでWEBサイトの「HTML」を確認できるのですが、

見て欲しいところが、今回データ取得対象である表形式となっている部分です。

HTMLデータ構造

表形式となっているところが、

「table」「thead」「tbody」「tr」という「タグ」が見えると思います。

 

HTMLの構造を簡略化して書くと、下のような構造になっています。

<table>
    <thead>
        <tr>
            <th>
                選手名
            </th>
            <th>
                チーム
            </th>
            ・・・・・略・・・・
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>
                鈴木 大地
            </td>
            <td>
                楽天
            </td>
            ・・・・・略・・・・
        </tr>
        <tr>
            <td>
                浅村 栄斗
            </td>
            <td>
                楽天
            </td>
            ・・・・・略・・・・
        </tr>
        ・・・・・略・・・・
    </tbody>
    <thead>
        ↑のtheadと同様
        ・・・・・略・・・・
    </thead>
</table>

頭に入れておいて欲しいことが、

  • 表形式の部分は<table>タグで全体を囲っている
  • 1行は<tr>タグで行全体を囲っている
  • ヘッダの値は<th>タグで値を囲っている
  • 明細の値は<td>タグで値を囲っている

この4つです。

<thead>と<tbody>は今回は使わないので、無視して大丈夫です。

 

これを踏まえて、続きのコードを書いていきます!

HTMLデータから値を取得

先程取得してきたHTMLデータから、表形式のデータを取り出していきます。

#HTMLから表(tableタグ)の部分を全て取得する
table = soup.find_all('table')[0]
#テーブルから行データを取得
rows = table.find_all('tr')

1行にまとめて書いても良いのですが、説明のためと、理解しやすいように2行で書いています。

 

1行目で先程取得してきたHTMLデータに対して、「find_all」関数を使っています。

「find_all」関数の引数に「'table'」を指定していますが、

これで「tableタグで囲われたデータを取ってきてください!」という意味になります。

そして最後に[0]を付けていますが、「soup.find_all('table')」だけだと「配列型」で値が返ってきます。

 

配列についても、簡単に説明しておきます。

先程変数について説明しましたが、配列型でない普通の変数は1つの値しか格納できません。

それに対して配列型だと、複数の値を格納することができます。

普通の変数が1件家なら、配列はマンションのような感じです。

以下が例です。

normal = 'a'
hairetu = ['a', 'b', 'c']

print(normal)
print(hairetu[0])
print(hairetu[1])
print(hairetu[2])

#実行結果
a
a
b
c

 

なので先程の[0]は、配列型で格納されているtableデータの1つ目という意味です。

※配列は0から始まるので注意です。

今回のWEBサイトは表形式のデータが1つしかないので、

配列型でデータが返ってきても結局1つなのですが、

もし表形式が複数あったら、取得したい方の配列番号を指定する必要があります。

 

そして2行目でtableデータに対して「find_all」関数を使って、

「trタグ」で囲われたデータを取ってきています。

今度は[0]を付けていないので、配列型として「rows」に格納しています。

 

 

次の処理に入る前に、配列型の変数を2つ宣言しておきます。

#データ格納用
headData = []
playerData = []

今からデータの値を取得してきますが、その値の格納する配列型変数です。

イメージとしては、

headData = ['選手名', 'チーム'・・・]
playerData = [['鈴木 大地', '楽天'・・・]
      , ['浅村 栄斗', '楽天'・・・]
      , [・・・]
      ]

となるように値をセットしていきます。

選手データは配列の中に配列があるので、少しややこしいかもしれませんが、

続きの処理を見るとなんとなく理解できると思うので、先に進みます!

 

それでは、行データから値を取り出していきます。

for i, row in enumerate(rows):    
    #1行目
    if i == 0:
    #ヘッダ部取得
        for headerValue in row.find_all('th'):
            headData.append(headerValue.get_text())
    #2行目~最終行未満
    elif (i > 0) and (i + 1 < len(rows)):
        #明細部取得
        playerRow = []
        for playerValue in row.find_all('td'):
            playerRow.append(playerValue.get_text())
        playerData.append(playerRow)

ちょっと盛沢山になってきましたが、1つずつ説明していきます。

 

まず「for」文です。

プログラミング経験者なら知っていると思いますが、

「for文は繰り返し処理」です。

「for 変数 in 配列型」と書くことで、配列の中身が一つずつ変数に格納され、

配列の中身がなくなるまで、繰り返し処理を行います。

※ちなみにPythonでは{}やBEGIN、ENDで処理をくくるのではなく、
インデントで処理の範囲が判断されます。

 

簡単な例だと、このようになります。

for i in ['a','b','c']:
    print(i)

#結果
a
b
c

 

ただし今回の場合は、

「for 変数1, 変数2 in enumerate(配列型)」

という書き方になっています。

enumerate」

を使うことで、「変数1」に配列番号が入り、「変数2」に配列の値が入ります。

配列番号と言うより、行番号と言った方が分かりやすいかもしれません。(0始まりですが)

今回は表形式データの行番号も欲しかったので、このような書き方になりました。

 

ループ処理の中身に入りますが、今度は「if」文です。

こちらもプログラミング経験者なら当たり前の処理ですが、

if 条件文:
    条件を満たすと処理実行
elif 条件文:
    ifの条件を満たさないかつ、elif条件を満たすと処理実行
else:
    上記の条件をどれも満たさないと処理実行

というやつです。

 

そのif文の条件に「i == 0」としています。

「==」「等しい」という意味です。「=」だと「代入」という意味になります。

    #1行目
    if i == 0:
        #ヘッダ部取得
        for headerValue in row.find_all('th'):
            headData.append(headerValue.get_text())

「for文」の説明の時に「enumerate」を使うことで、

「変数1」である「i」に「行番号」がセットされると説明しました。

なので「i == 0」で、「先頭行だったら」ということになります。

 

1回目のループでしたいことは、

データの項目名「選手名」、「チーム」という部分の取得です。

それを「for文」で処理しています。

行データからヘッダの値がセットされている「thタグ」を取得して、1つずつ処理しています。

「thデータ」に対して「get_text()」を使う事で値を取り出すことができます。

それをヘッダ部格納用配列変数「headData」に「append」をしています。

「append」は配列要素の追加です。

for文が処理されるごとに、「選手名」、「チーム」、「打率」という項目名がセットされていきます。

取得してきた結果を確認すると、こんな感じです。

print(headData)

結果
['順位', '選手名', 'チーム', '打率', '試合', '打席数', '打数', '得点', '安打', '二塁打', '三塁打', '本塁打', '塁打', '打点', '盗塁', '盗塁刺', '犠打', '犠飛', '四球', '敬遠', '死球', '三振', '併殺打', '出塁率', '長打率']

これで目的の1つ、ヘッダの項目名を取ってこれました!

 

続いて選手データ部分ですが、for文の中の2つ目の「elif文」が、

    #2行目~最終行未満
    elif (i > 0) and (i + 1 < len(rows)):
        #明細部取得
        playerRow = []
        for playerValue in row.find_all('td'):
            playerRow.append(playerValue.get_text())
        playerData.append(playerRow)

このようになっています。

条件文に「and」を付けると、「両方の条件を満たしたときだけ」処理を実行するようになります。

「どちらか一方」「or」です。

1つ目の条件「i > 0」は「i」にループ回数が入っているので、

「行データの2行目以降」という条件になります。

2つ目の条件「i + 1 < len(rows)」「最終行未満」という条件です。

「len」を使うと配列の「要素数」、つまり「データの行数」を取得することができます。

最終行の処理だと「行番号」=「データの行数」となるので、

「i + 1 < len(rows)」とすることで「最終行未満」とすることができます。

※「+1」は「i」が「0」から始まるためです。

 

なぜ最終行を省いているかと言うと、

WEBサイトを見てみると、最終行にまた「項目名」の行があるからですね。

これを省きたいがために「i + 1 < len(rows)」の条件を追加しています。

最終行に項目名

 

後の処理はヘッダの項目名取得と同じです。

空の配列変数「playerRow」を宣言してますが、これに先程と同様に「for文」で値を取ってきます。

明細データの値は「tdタグ」で囲ってるので、「tdタグ」を指定して値を取得します。

「playerRow」に「鈴木 大地」、「楽天」、・・・とセットされるので、

その処理が終わった後に、明細データ格納用配列変数「playerData」に追加します。

そして次のfor文の処理に移っていくと、最終的に「playerData」には、

print(playerData)

結果
[['1', '鈴木\u3000大地', '楽天', '.295', '120', '546', '478', '71', '141', '27', '1', '4', '182', '55', '1', '3', '12', '3', '46', '0', '7', '58', '18', '.363', '.381'], ['2', '浅村\u3000栄斗', '楽天', '.280', '120', '529', '432', '72', '121', '25', '0', '32', '242', '104', '1', '1', '0', '2', '91', '2', '4', '111', '15', '.408', '.560'], ['3', '大島\u3000洋平', '中日', '.316', '118', '525', '462', '58', '146', '21', '3', '1', '176', '30', '16', '8', '9', '3', '47', '3', '4', '51', '5', '.382', '.381'],

このように欲しいデータがセットされています。

これで後は表形式データに変換して、CSV出力するだけです!

配列変数を表形式データに変換

「ヘッダ項目名の配列」と、「選手データの配列」を取得できましたが、

これを表形式データに変換します!

#DataFrameに変換
df = pd.DataFrame(data = playerData, columns = headData)

これで表形式に変換できます。

表形式データをPythonで扱うためには、最初に宣言したライブラリ「Pandas」を使います。

そして、その表形式データのことを「DataFrame」と言います。

「data」の引数に「選手データの配列」、「columns」の引数に「ヘッダ項目名の配列」を渡します。

その結果を「df」変数にセットしています。

結果を確認してみると、

print(df)

結果
      順位    選手名    チーム    打率   試合  打席数   打数  得点   安打 二塁打  ... 盗塁刺  犠打 犠飛  四球  \
0      1  鈴木 大地     楽天  .295  120  546  478  71  141  27  ...   3  12  3  46   
1      2  浅村 栄斗     楽天  .280  120  529  432  72  121  25  ...   1   0  2  91   
2      3  大島 洋平     中日  .316  118  525  462  58  146  21  ...   8   9  3  47   
3      4  西川 遥輝   日本ハム  .306  115  523  422  82  129  17  ...   7   4  3  92   
4      5  近本 光司     阪神  .293  120  519  474  81  139  21  ...   8   7  1  30   
..   ...    ...    ...   ...  ...  ...  ...  ..  ...  ..  ...  ..  .. ..  ..   
324  322  吉田 裕太    ロッテ  .000    1    1    0   0    0   0  ...   0   1  0   0   
325  322   細川 亨    ロッテ  .000    1    1    1   0    0   0  ...   0   0  0   0   
326  322  加藤 脩平     巨人  .000    1    1    1   0    0   0  ...   0   0  0   0   
327  328  白崎 浩之  オリックス  .000    3    0    0   1    0   0  ...   0   0  0   0   
328  328   細谷 圭    ロッテ  .000    1    0    0   0    0   0  ...   0   0  0   0   

    敬遠 死球   三振 併殺打   出塁率   長打率  
0    0  7   58  18  .363  .381  
1    2  4  111  15  .408  .560  
2    3  4   51   5  .382  .381  
3    1  2   84   5  .430  .396  
4    2  7   61   4  .344  .416  
..  .. ..  ...  ..   ...   ...  
324  0  0    0   0  .000  .000  
325  0  0    1   0  .000  .000  
326  0  0    1   0  .000  .000  
327  0  0    0   0  .000  .000  
328  0  0    0   0  .000  .000

良い感じです!

表形式データをCSVファイルに出力

最後にCSVファイルに出力します。

PythonでCSVファイルを出力方法はいくつかありますが、

今回は「pandas」を使ったCSVファイルの出力です。

#CSV出力
df.to_csv('playerData.csv', sep = ',', header = True, index = False)

DataFrame変数「df」に対して「to_csv」を使うことでCSV出力ができます。

引数は順番に、

「ファイル名」、

「区切り文字」今回はCSVなのでカンマを指定してます、

「ヘッダの項目名を付けるか?」付けます、

「一番左の列に連番を付けるか?」特に使う予定はないので、付けません。

 

実行して出力したファイルを見てみましょう!

CSVファイルの中身

ちゃんとできています!

これで今回のやりたいことは完了です!

全体をもう一度見てみましょう!

#WEB上のデータ(HTML)にアクセス&取得するために使用
import urllib
#HTMLからデータを抽出するために使用
from bs4 import BeautifulSoup
#表形式でデータ格納するために使用
import pandas as pd

url = 'https://baseball-data.com/stats/hitter2-all/tpa-1.html'
html = urllib.request.urlopen(url)
soup = BeautifulSoup(html, 'html.parser')

#HTMLから表(tableタグ)の部分を全て取得する
table = soup.find_all('table')[0]
#テーブルから行データを取得
rows = table.find_all('tr')

#データ格納用
headData = []
playerData = []

for i, row in enumerate(rows):    
    #1行目
    if i == 0:
        #ヘッダ部取得
        for headerValue in row.find_all('th'):
            headData.append(headerValue.get_text())
    #2行目~最終行未満
    elif (i > 0) and (i + 1 < len(rows)):
        #明細部取得
        playerRow = []
        for playerValue in row.find_all('td'):
            playerRow.append(playerValue.get_text())
        playerData.append(playerRow)

#DataFrameに変換
df = pd.DataFrame(data = playerData, columns = headData)

#CSV出力
df.to_csv('playerData.csv', sep = ',', header = True, index = False)

まとめ:Python機械学習「WEBスクレイピング」

ここまでの話をまとめます。

まとめ

WEBサイトから情報を取ってくる「WEBスクレイピング」と言い、

今回やったことは、

  1. 「urllib」を使って、プロ野球データが表示されているURLにアクセス
  2. 「urllib」を使って、HTMLデータを取得
  3. 「BeautifulSoup」を使って、HTMLデータから各項目の値を取得し、配列変数にセット
    その中で繰り返し処理の「for文」、条件分岐の「if文」を使用した
  4. 「Pandas」を使って、配列変数を表形式データに変換
  5. 表形式データをCSVファイルに出力

2020年のプロ野球選手データを取得することができた!

 

今回のWEBスクレイピングは表形式となっている部分を取得してきましたが、

表形式でなくてもWEBサイトの情報は取ってこれます。

今回は単純な例でしたが、

WEBサイトのHTML構造を分析して、

うまく「タグ」を指定したり、

今回はやりませんでしたが「クラス名」を指定したりして、

欲しい情報を取ってくるようになりましょう!

 

うまく使いこなせば、

これだけで仕事を受けることができますし、

仕事の手作業を減らせることができるかもしれません。

 

また今回の処理で、

プログラミングの基本である「配列」や「for文」、「if文」、

Pythonの基本構文が多く学べました。

人によっては丁寧過ぎたかもしれませんが、

今回で基本部分を大分説明できたので、

次回以降はペースアップしてお話していきます!

 

次回は「データ分析」編です!

今回取得してきたデータを使って、「ホームラン数」と関連性が高いデータを洗い出します!

 

※注意事項

WEBスクレイピングはWEBサイトにアクセスするため、

少なからずWEBサーバーに負担が掛かります。

大量データを取得したり、何度も実行したりしないようにしましょう!

 

※次の記事へ

Python機械学習
Python機械学習「ホームラン数予測③」データ分析編!

  ホームラン数を予測できるように機械学習させたいけど、 どんな情報を渡せば良いんだろう。。 片っ端から渡して色々と試せばいいんだろうか。。   こんにちは、古賀です! &nbsp ...

続きを見る

おすすめ記事

eSportsプラットフォームSPFans 1

  こんにちは、古賀です!   本記事では、わたしが個人開発したWEBサービス はてな eSportsプラットフォーム「SPFans」 の紹介をします。   以前に紹介し ...

成長するエンジニア 2

  エンジニアになったけど、いまいち成長を感じられないなぁ。。 作業スピードも品質も平凡。 この先成長していけるんだろうか。。   こんにちは、古賀です!   エンジニア ...

道のり 3

※本記事は、各記事のまとめ記事です。   こんにちは、古賀です!   本記事では、 「プログラミング未経験者がエンジニアとして働き、 年収1000万に到達するまでの道のり」 をご説 ...

プログラミングスクール 4

  プログラミングを本格的に勉強して仕事に繋げていきたいけど、 プログラミングスクール多すぎる。。 どういう目線で選べばいいんだ。。   こんにちは、古賀です。   プロ ...

-エンジニア, データサイエンティスト
-, ,

Copyright© Koga Masao's LifeBlog , 2024 All Rights Reserved.