kta-basket's blog

バスケット好きによるなにか

Bリーグの選手情報を返すLINE Botを作った① - pythonでスクレイピングしてスプレッドシートに保存 -

経緯

Bリーグを生で観に行っていて不便だなと思うことが、コート上の選手が分からないことです。

Bリーグになってからマッチデープログラムが配られるようになって、NBLの時代なんかよりは親切になってはいるのですが、自チームはともかくAWAYチームの情報が質素な場合が多くてですね。。。

なので、スマホでそのチームのHPを開いて試合中にちょこちょこ選手を調べておりました。

めんどくさい!

というわけでよく使うLINEでBot作ったら便利じゃない??というモチベーションで作りました。

できたもの

こんな感じ。
チーム名+背番号でプロフィールと簡単なスタッツを返します。


エンジニアじゃないから中身興味ないけどBリーグ好きでそのBot使いたい!って方は友達追加して勝手にご利用ください。


Bリーグ選手情報Bot
友だち追加


念の為断っておくとログとかは取ってないし誰と友達かは私からは分からないので安心してご利用ください。

構成

  1. Bリーグ公式から選手情報取ってきてGoogleスプレッドシートに書き込むpythonプログラムをHerokuのschedulerに登録し毎日実行
  2. 文章解析してチーム名と背番号を抽出してスプレッドシートを参照して選手情報を返すGASプロジェクトをウェブアプリケーションとして公開し、LINE messaging APIのWebhook送信先に指定

以上。
本記事では1の中身を書きます。

pythonスクレイピング

一番泥臭いところです。
まず、B.LEAGUE(Bリーグ)公式サイト を眺めて選手情報を取って来れそうか調べます。
「クラブ・選手」タブを開くとチームが地図上に表示されていて、チームをクリックすると各チームの詳細ページに。
そして詳細ページの中に選手一覧があり、そこから更に各選手のプロフィールページに飛べる仕様です。

どこまで自動化するか、ですがこのあとのGASとのチーム名の一意性を保ちたかったこともあり、チームの詳細ページのURL(チーム毎にTeamIDが変わるだけ)までは手動で書いて、選手一覧を参照して各選手の情報を取ってくるところは自動化しました。


from bs4 import BeautifulSoup # スクレイピングに使える便利ライブラリ

# チームの詳細ページから選手のプロフィールページへのリンク一覧を取得
def list_roaster_url(teamid):
    url = BASE_URL + "club_detail/?TeamID=" + teamid
    res = requests.get(url)

    soup = BeautifulSoup(res.content, "html.parser")
    player_list_class = soup.find(class_="player-list")
    playerid_list = re.findall('PlayerID=[0-9]*', str(player_list_class))
    return playerid_list

# PlayerID=***を与えると選手のプロフィールページから情報を取得する
def get_player_detail(playerid):
    url = BASE_URL + "roster_detail/?" + playerid
    res = requests.get(url)

    soup = BeautifulSoup(res.content, "html.parser")
    player_detail = soup.find(class_="player-detail")
    outline = player_detail.header
    num = outline.find('h1').string
    name = outline.find('h2').string
    name_en = outline.find('p', class_='name-en').string
    position = outline.find('p', class_='position').string
    position = position.strip("ポジション / ")
    player = {
        'url': url,
        'name': name,
        'name_en': name_en,
        'num': num,
        'position': position
    }
    player_values = [url, name, name_en, num, position]

    tabledata = soup.findAll('table', class_="sub-player-table")
    for data in tabledata:
        keys = data.findAll('th')
        values = data.findAll('td')
        i = 0
        for key in keys:
            value = values[i].string
            if value is None:
                value = re.sub("<[a-z,/,=,\",\s]+>","",str(values[i]))
            player[key.string] = value
            player_values.append(value)
            i+=1
    
    return player, player_values


list_roaster_url(teamid)ではTeamIDを与えると選手のリンク(…正確に言えば選手プロフィールページの”PlayerID=”以降)のリストを返します。

BeautifulSoupはとっても便利。


get_player_detail(playerid)では取得したPlayerID=***の文字列を渡すと選手のプロフィールにまとめて返します。


BeautifulSoupはとっても便利。


名前・ポジション・背番号はそれぞれタグが別れていたので取ってくるのは簡単だったのですが、身長体重や出身校はtableになっていたのでvalueだけ抜き取っています。
ついでに平均スタッツも同じ名前の別tableだったのでsoup.findAllして一緒に取ってきました。
なお、dict型とlist型両方返してますが結局list型しかこのあと使ってません。

Googleスプレッドシートに書き込む

このへんを参照しつつスプレッドシートに書き込めるように設定します。

qiita.com


さっき作った関数を使って得た選手情報のlistをかきこみます。

# sheet: 書き込み先シート、data: 選手の情報が入ったlist、row: 書き込み行
def write_to_sheet(sheet, data, row):
    i = 0
    cell_list = sheet.range('A%s:T%s' % (row, row))
    for cell in cell_list:
        cell.value = data[i]
        i += 1
    sheet.update_cells(cell_list)


gspreadのドキュメントはあるにはあるのですが、わかりづらくて少し苦戦しました笑。

詰まりポイント

始め、worksheet.update_cell(row, column, value)という形で一セルずつ書き込んでいっていたら、1チーム目の途中でエラーになりまして…
どうやら100秒間に100リクエストを超えてはいけないという制限に引っかかっていたようです。
そのため行単位で更新することにしました。


cell_list = sheet.range('A%s:T%s' % (row, row))

である行のA列からT列のセルを取得。
(セルオブジェクトのリストが返ってきます)


i = 0
for cell in cell_list:
    cell.value = data[i]
    i += 1

で一セルずつ値を書き込んでいき(この時点ではローカルの値のみ変更される)


sheet.update_cells(cell_list)

で一気にアップデート!


range()で取得したリストは左上のセルから順になっていて、対象のセルのvalueに書き込んでupdate_cells()することで1回のリクエストで複数セルを更新することができます。
セルオブジェクトは実際にシート上で見える値がvalueに入っていて、cell.row, cell.colでどの列と行のセルなのかを取得できます。

完成

これらの関数を使って以下のmain関数で実行しています。

if __name__ == "__main__":
    player_dict = []
    for team in TEAM:
        time.sleep(10)
        roasters_list = list_roaster_url(team['teamid'])
        team_name = team['name']
        sheets = G_CLIENT.open_by_key(SHEET_ID)
        try:
            del_sheet = sheets.worksheet(team_name)
            sheets.del_worksheet(del_sheet)
        except:
            print('new '+team_name)
        team_sheet = sheets.add_worksheet(title=team_name, rows="30", cols="30")
        row = 1

        for player in roasters_list:
            player_info, data = get_player_detail(player)
            player_info['team'] = team['name']
            player_dict.append(player_info)
            data.insert(0,team['name'])
            write_to_sheet(team_sheet, data, row)
            row += 1


1行ずつとはいえ100秒間100リクエストを超えないように各チームごとに10秒空けてます。
選手の増減を確認するのが面倒だったので一々シートを消して作り直しているけど更新時のエラーとか更新中のデータ欠損とか考えるとちょっと直したい・・・
というかこのブログを書きながら選手毎じゃなくて1チーム分まるっと書き込めるじゃない、と思ったので直してきます。 → 直した

Herokuにアップ

このへんをみてschedulerに登録。

qiita.com

週1~2くらい更新でいいかなと思ってたんですが、最長で一日置きでした。

最初ちょこっとエラーになったのはruntime.txtrequirements.txtがなかったからでした。

runtime.txt

python-3.6.4

requirements.txtpip freeze > requirements.txtでok

まとめ

BeautifulSoupはとっても便利。


次回、GASでLINE Botを作ろう編です。