データとバスケ

バスケをデータから楽しむブログです。

スクレイピングでBリーグ全選手の試合ごとスタッツを取得する

このブログはBリーグ観戦の肴になりそうなデータ分析の結果を主なコンテンツにしていますが、ときどきその裏でどういう技術的なことをやっているのかについても書きたいと思っています。これはその第1号です。よってこの記事の内容はBリーグやバスケットに直接の関係はありません。

はじめに

これまでの分析記事の多くは、Bリーグ公式のスタッツページにある各選手のシーズントータルのスタッツを取得し、それを使用して書きました。ちなみに具体的にどのように取得したかと言うと、例えばB1の2017-18シーズンのデータが欲しい場合はこうしました。

  1. スタッツのページに普通にブラウザでアクセス
  2. ページに表示される選手のスタッツ一覧表をすべて選択してコピー
  3. コピーしたものをExcelに張り付ける
  4. 張り付けたものをCSV(カンマ区切り)ファイルとして保存

CSVファイルを作ってしまえば、あとはRにしろPythonにしろ読み込んでしまえば、好きに加工したり分析したり何でもできます。Excel上で分析するのもいいでしょうし、データベースのテーブルにしてしまってSQLを駆使する人もいるかもしれません。私はRを使うか、もしくはExcelを使っています。

これからやりたいこと

最近はシーズントータルのスタッツを使った分析では段々と物足りなくなってきて、選手の試合ごとのスタッツを使った分析したくなってきました。ありがたいことに各選手の試合ごとのスタッツも公式ページで取得できます。例えばニック・ファジーカスの2017-18シーズン全試合のスタッツはこのページで取得できます。

数人の選手のデータが必要なだけであれば、上述のやり方で試合ごとスタッツの情報をコピーしてからExcelに貼り付けてデータを作ってもそこまで手間ではありません。しかしこれをB1、B2と合わせて何百人もいるすべての選手に対して行うのはいささか大変です。

そこで自動的にB1、B2全選手の試合ごとスタッツのページにアクセスし、必要な情報を読み取り、最後にそれをCSVファイルとして保存してくれるプログラムを書きたいと思います。このようにWebページから情報を読み取る行為を一般的にWebスクレイピングと呼びます(もしくは単にスクレイピング。)

この記事で必要なもの

Webスクレイピングをするプログラムを書くのに一番人気の言語はPythonではないかと推測しますが、この記事ではRを使います。単純に私が分析にRを使っているからです。Rのプログラムを書き実行するのにはR Studioというソフトウェアが便利なので、それも使用します。

またRはパッケージと呼ばれる拡張機能が充実しているのですが、その中からrvestというWebスクレイピングに便利なものをインストールしたいと思います。インストールの方法は次のセクションで説明します。

またGoogle Chromeも使用します。

まず試しに簡単なスクレイピングをやってみる

まずお試しで上述のファジーカスの2017-18シーズンの試合ごとスタッツの情報を取得してみましょう。R Studioの使い方はこの記事では説明しませんが、Windowsで言うところのCTRL + ENTERを押すと、カーソルがある行のコードがコンソールにて実行されることは覚えておくと便利です。

まずは簡単なことから始めてみます。ファジーカスの2017-18の情報のみスクレイピングして、それをCSVファイルにして保存しましょう。

if (!require(rvest)) {
  install.packages("rvest")
  library(rvest)
}

url <- "https://www.bleague.jp/roster_detail/?tab=1&year=2017&PlayerID=8490"
page <- read_html(url, encoding = "utf-8")
tables <- html_table(page)
games <- tables[[4]]

write.csv(games, "Fazekas.csv")

最初のif()の箇所は全体で「rvestパッケージがなかったらインストールしてからロードしてね。あったら単にロードしてね。」という意味です。

次のread_html()というのがページにアクセスしている部分です。ここでこのページにリクエストが投げられ、ページの内容(つまりHTMLのテキスト)を取得することができます。

続くhtml_table()というのはrvestの便利な関数で、いわゆる<table>、<td>、<tr>などのタグで書かれたHTMLのテーブルとその中身を抜き出してくれます。このページの場合、ページ内の4番目のテーブルが試合ごとのスタッツが入っているテーブルなので、4を指定して抜き出しています。ちなみにRのインデックスは0ではなく1から始まります。

変数gamesはRでdata.frameと呼ばれている型なのですが、いわゆるテーブル形式でデータを保持するのに使用される型です。上記のプログラムを実行すると、gamesの最初の3行は以下のようになっています。見事にファジーカスの試合ごとスタッツが取得されています。

DAY VS S MIN PTS FGM FGA FG% 3FGM 3FGA 3FG% FTM FTA FT% OR DR TR AS TO ST BS BSR F FD DUNK EFF
2018.01.14 B.BLACK VS B.WHITE 17:58 6 3 11 27.3% 0 5 0.0% 0 0 0.0% 2 8 10 1 2 1 0 0 1 0 0 8
2018.01.26 川崎 VS 千葉 28:44 22 9 17 52.9% 0 1 0.0% 4 4 100.0% 3 13 16 4 3 1 1 0 3 5 0 33
2018.01.27 川崎 VS 千葉 24:37 26 11 19 57.9% 2 6 33.3% 2 2 100.0% 0 8 8 2 5 0 1 0 4 3 0 24

最後にgamesに名前を付けて保存すればCSVファイルの完成です。

全選手のデータを取得するためにプログラムを改造する

では上記のプログラムを全選手のデータを取得する為に改造してみましょう。大まかな方針は以下のようになります。

  1. 各選手の情報が載っているページのURLを手に入れる
  2. 各選手のページに順番にアクセスし、html_table()を使って試合ごとのスタッツを抜き出す
  3. 各選手の結果をすべて結合し、ひとつのdata.frameにまとめる
  4. まとめた結果のdata.frameをCSVとして保存する

こうして見ると、そんなに手間ではなさそうです。実際にこの中で少し難しいのは手順1のみです。

もちろん実際にデータを使う段階になると、スクレイピングで取得したデータそのものをそのまま使うのではなく、少し加工してから使う必要がある場合がほとんどです。なので手順4の後にやらなければならないこともたくさんあります。この記事ではそこには触れません。

各選手のページのURLを手に入れるためのスクレイピング

上の例でファジーカスのスタッツを手に入れる為のURLが出てきました。

https://www.bleague.jp/roster_detail/?tab=1&year=2017&PlayerID=8490

どうやらtabというのがB1/B2、yearがシーズン、PlayerIDが各選手を表しているようです。よって各選手のPlayerIDさえ分からればB1、B2全選手の2016-17、2017-18両シーズンの試合ごとスタッツが手に入りそうです。

この各選手のPlayerIDですが、これを手に入れるためにはスクレイピングが必要です(Bリーグのシステム担当者に知り合いでもいない限り。)つまりスクレイピングをする為に、別のスクレイピングが必要ということになります。私はスクレイピングをやるのがこれが初めてですが、おそらくよくあることでしょう。

スクレイピングをするべきは、Bリーグのスタッツトップページの以下の赤枠内のリンク部分です。ちなみにトップページURLのパラメータはどうやら各選手のページと同様のルールに従っているようです。

https://www.bleague.jp/stats/?tab=1&year=2017

f:id:rintaromasuda:20180713082933p:plain

テーブルに記載されている情報なので上述のrvestのhtml_table()を使えばいいかと一瞬思いましたが、それだとリンクではなく文字列(つまり選手の名前)の方が取得されてしまうので上手く行きません。ではどうすればいいでしょう。

CSSセレクタを利用して一連のルールに従う要素を抜き出す

説明はあと回しにして、このようにRを書くとスクレイピングで該当のリンクの情報を抜き出すことができます。

if (!require(rvest)) {
  install.packages("rvest")
  library(rvest)
}

url_top <- "https://www.bleague.jp/stats/?tab=1&year=2017"
html_top <- read_html(url_top, encoding = "utf-8")

urls_player <- html_top %>%
  html_nodes("#tbl-player > tbody > tr > td:nth-child(3) > a") %>%
  html_attr("href")

urls_player <- paste(urls_player, "&year=2017", sep = "")

試しに出力してみると、URLが取得できているのがわかります。ちなみにyear=2017は自分で付与しないといけないことが判明したので、paste()というRの関数を用いて取得したURL文字列にくっつけています。

> head(urls_player)
[1] "https://www.bleague.jp/roster_detail/?PlayerID=9040&year=2017" 
[2] "https://www.bleague.jp/roster_detail/?PlayerID=8490&year=2017" 
[3] "https://www.bleague.jp/roster_detail/?PlayerID=8593&year=2017" 
[4] "https://www.bleague.jp/roster_detail/?PlayerID=12595&year=2017"
[5] "https://www.bleague.jp/roster_detail/?PlayerID=8503&year=2017" 
[6] "https://www.bleague.jp/roster_detail/?PlayerID=10819&year=2017"

さて、プログラムの中になんだか「#tbl-player > tbody ...」というおまじないみたいな文字列が出てきましたね。これはCSSセレクタと呼ばれるものです。スクレイピングをするときには絶対に外せない部分だと思います。

CSSについて詳しくなる必要はまったくありません(私も詳しくありません。)簡単に言うとCSSとはWebページの見た目を整えるためのものであり、デザイナーさんなどが「このルールに従う一連の文字は赤色にしてね!」みたいな処理をするために使うものです。そしてその「一連の~」をが指示するときにCSSセレクタを使います。これをスクレイピングでも流用して「このルールに従う一連のHTML要素を抜き出してください!」とやるわけです。

ちなみにCSSセレクタと同じ目的でXPathという記述方法もあり、rvestではXPathも使用できますが、今回はCSSセレクタのみ使います。おそらくCSSセレクタだけで事足りる場合がほとんどだと思います。

ではあなたが欲しいページの要素のCSSセレクタをどのように知ればいいのでしょうか?これにはGoogle Chrome(や他のブラウザの)開発者ツールを使用すると便利です。

BリーグのスタッツトップページGoogle Chromeで開き、以下のようにします。

  1. F12ボタンを押す。すると開発者ツールが開きます。
  2. 開発者ツール左上にある矢印ボタンを押します
  3. あなたが気になっているページ内要素をクリックします(右クリックで「検証」を選んでも可)
  4. そうするとツール内で対応するHTML要素がハイライトされるので、それを右クリックをします。
  5. Copy -> Copy selectorを選びます

f:id:rintaromasuda:20180713143853p:plain

これでクリップボードCSSセレクタがコピーされました。ファジーカスのリンク(つまり<a>要素)でCopy selectorしたときの値はこのようになりました。

#tbl-player > tbody > tr:nth-child(2) > td:nth-child(3) > a

ただこのままだと、ファジーカスの行のリンクだけを指し示しているCSSセレクタになってしまいます。この列にある<a>要素をすべてを指すためには、すべての行を指すようにしなければならないため、tr:nth-child(2)を少しいじって以下のように変更します。

#tbl-player > tbody > tr > td:nth-child(3) > a

これですべての<a>要素が抜き出せました。あとはこの要素からhref属性を抜き出すことでURLが文字列として手に入ります。上記のプログラムの中に%>%という文字列が出てきましたが、これは「パイプ」と呼ばれるもので「まずAをやって、そして次にBをやって、最後にCをやって」みたいな処理の連鎖を「A %>% B %>% C」のように記述するやり方です。上記の例では「まずはこのCSSセレクタでa要素を取得して、その後にhref属性の値を取得してね!」という処理をパイプを使って書いています。

複数のdata.frameを結合する

私はすべての選手のすべての試合のデータをひとつのファイルにまとめたいので、その前段階としてすべてのデータをひとつのdata.frameにまとめます。これにはrbind()という関数を使います。Row bind、つまり行ベースで結合するということです。

改造版のコード

ではこれですべてのピースが揃ったはずなので、がっちゃんこしたプログラムを書いてみましょう。

if (!require(rvest)) {
  install.packages("rvest")
  library(rvest)
}

result <- data.frame()

for (year in c(2016, 2017)) {
  for (league in c(1, 2)) {
    url_top <- paste("https://www.bleague.jp/stats/",
                     "?tab=",
                     as.character(league),
                     "&year=",
                     as.character(year),
                     sep = "")
    print(url_top)
    html_top <- read_html(url_top, encoding = "utf-8")
    
    urls_player <- html_top %>%
      html_nodes("#tbl-player > tbody > tr > td:nth-child(3) > a") %>%
      html_attr("href")
    
    urls_player <- paste(urls_player, "&year=", as.character(year), sep = "")
    
    for (url in urls_player) {
      html_player <- read_html(url, encoding = "utf-8")
      tables_player <- html_table(html_player)
      df_game <- tables_player[[4]]
      result <- rbind(result, df_game)
    }
  }
}

write.csv(result, file = "player_games.csv")

if (!require(readr)) {
  install.packages("readr")
  library(readr)
}
readr::write_excel_csv(result, "player_games_excel.csv")

実行するとかなり長い時間がかかりますが、すべての選手ページにアクセスし、試合ごとのスタッツを読み取り、最後にひとつのファイルが作成されるのが確認できます。write_excel_csv()というのはおまけで、作成したCSVExcelで開きたいときはこちらの方法で保存するのがおすすめです。

最後の問題点

せっかくデータができたのですが、これだとどの行がどの選手のデータなのか分かりません。なので各行に選手の名前と、それから上で出てきたPlayerIDを追加しておきましょう。多分このIDはそのうち役に立つと思います。

またフィルタするのに役立ちそうなので、どちらのシーズンのデータなのか、B1なのかB2なのかもデータとして足しておきましょう。

以上を踏まえて以下のようにプログラムを変更しました。これがこの記事での最終形です。

if (!require(rvest)) {
  install.packages("rvest")
  library(rvest)
}

result <- data.frame()

for (year in c(2016, 2017)) {
  for (league in c(1, 2)) {
    url_top <- paste("https://www.bleague.jp/stats/",
                     "?tab=",
                     as.character(league),
                     "&year=",
                     as.character(year),
                     sep = "")
    print(url_top)
    html_top <- read_html(url_top, encoding = "utf-8")
    
    urls_player <- html_top %>%
      html_nodes("#tbl-player > tbody > tr > td:nth-child(3) > a") %>%
      html_attr("href")
    urls_player <- paste(urls_player, "&year=", as.character(year), sep = "")
    
    names_player <- html_top %>%
      html_nodes("#tbl-player > tbody > tr > td:nth-child(3) > a") %>%
      html_text(trim = TRUE)

    startStr <- "PlayerID="
    endStr <- "&"
    ids_player <- substring(urls_player,
                            regexpr(startStr, urls_player) + nchar(startStr),
                            regexpr(endStr, urls_player) - nchar(endStr))

    index <- 0
    for (url in urls_player) {
      index = index + 1
      html_player <- read_html(url, encoding = "utf-8")
      tables_player <- html_table(html_player)
      df_game <- tables_player[[4]]
      df_game$PLAYER <- names_player[index]
      df_game$PID <- ids_player[index]
      df_game$YEAR <- year
      df_game$LEAGUE <- league
      result <- rbind(result, df_game)
    }
  }
}

write.csv(result, file = "player_games.csv")

if (!require(readr)) {
  install.packages("readr")
  library(readr)
}
readr::write_excel_csv(result, "player_games_excel.csv")

名前の取得のところはhref属性を取得している部分とほぼ同じ要領で$lt;a>のテキストを取得しています。選手のIDの取得は若干複雑になってしまいましたが、要はURLの中にある"PlayerID=xxxxxx&"のxxxxxxの部分を抜き出しているだけです。

まとめ

Webスクレイピングを使用し、Bリーグの全選手の試合ごとデータを取得しました。スクレイピングをする方法は色々あると思いますが、ひとつの参考になれば幸いです。

前述のとおりもう少し後処理をしないとデータ分析で使うためには不十分ですが、それが終わりましたらバシバシまたこれでバスケ観戦がより楽しくなるような分析結果がお届けできると思います。

技術的なことでもなんでも、質問がありましたらお気軽にどうぞ。もしスクレイピングをする気はないけど私が作ったデータが欲しい!分析したい!という人がいたら、何かしらの方法(おそらくgithubかなんかに上げます)でお渡ししますのでそれもご遠慮なく。

参考書籍

rvestパッケージのことやR全般のことは概ね以下の書籍を参考にしました。

Rプログラミング本格入門: 達人データサイエンティストへの道

Rプログラミング本格入門: 達人データサイエンティストへの道

追記1(2018年7月14日)

今回作成したプログラムとそこから出力したふたつのCSVですが、githubに上げておきました。

bleaguebydata/blog/2018071301 at master · rintaromasuda/bleaguebydata · GitHub

当然ですがBリーグが公式にサポートしているファイルでもないですし、私のプログラムやその他の原因で間違いが含まれている可能性もあります。ご了承ください。

またデータの後処理はしていません。データの処理は分析者の都合によって変わってくるので敢えてそのままにしている部分もありますが、明らかなもの(例えば選手の名前に半角スペースが含まれている場合と全角スペースが含まれている場合がある)も直していませんので、そちらも使用時にはご注意ください。