DataTennis.NET

データテニスドットネット

【Webスクレイピング】錦織圭実況掲示板のデータ記録(サーブコースや速度など)を抽出してまとめてみました

公開日:
最終更新日:2019/01/20

      2019/01/20

きっかけはツイッターでの下記やりとりから。
(私=おたこ@otakoma)

掲示板を確認してみると、サーブコースや速度やポイントの勝ち負け(〇×)がほとんどのポイントで記録されていて。

素晴らしい。是非データベースなどデータ分析をしやすい形にしてみたいなと。

ということでスクレイピングでデータを自動収集してスプレッドシートにまとめてみました。

そのやり方について簡単に説明したいと思います。

便利なWebスクレイピングだが、注意も必要

Webスクレイピングとは、Web上の情報を自動で収集する技術のことを言います。

一度プログラムコードを書けば、ボタン一つでページを巡回してデータを決めたルールに則って自動抽出してくれるので非常に便利です。

ただし、サーバに負荷をかけない、著作権に気を付けるなど注意も必要です。

私は以下を読みました。
Webスクレイピングの注意事項一覧

抽出したデータはcsvファイル(スプレッドシート)で保存

↓は保存したスプレッドシートのスクリーンショットです。

DcOxlb5VAAAT0Zf

データ数は、2016/4バルセロナ以降の70試合5734ポイント分です。
下記項目をデータとして抽出しています。

・AceDbF エース、ダブルフォルトなど
・Cource サーブコース(wide center body)
・FirstSecond 1stサーブか2ndサーブか
・OpponentPlayer 対戦相手
・Set 何セット目か
・Speed サーブ速度
・TotalGame 何ゲーム目か
・Tournament 大会名
・WinLose 〇⇒Won ×⇒Lost
・ScoreServer ScoreReturner データ記録時のスコア
・Side DeuceサイドかAdサイドか

データ抽出方法について

データ抽出ですが、
ページを巡回して、下記ルールをコードに書いてサーブデータを抽出しました。
(プログラムコードは最後に記載してます)

・赤黄色さんのコメントだけを抽出
・抽出したコメントを各セット毎に分類
・正規表現を用いて文章をゲーム、ポイント毎に区切る

赤黄色さんのコメントだけを抽出

↓は掲示板のスクリーンショットになり、赤黄色さん以外にも多くの方がコメントを書いてます。
基本的には赤黄色さんがサーブデータを記録してくれているので、赤黄色さんのコメントだけを抽出するようにしました。

hanaji

抽出したコメントを各セット毎に分類

コメントの冒頭にコメントが第何セットの内容なのかわかるような書き方をしてくれているので、何セット目のコメントかでデータを分類しました。

(例)
set1 2-6
set2

と書いてあれば、set2の2の数字を抽出して、コメント内のデータ全てのデータで、setに2を格納するようにします。

正規表現を用いて文章をゲーム、ポイント毎に区切る

各ゲーム毎の情報が、↓のように1行でまとめて書かれています。

G1圭〇×〇〇〇1-0(1w1662c1522c1482c1481c)

この例でいくと、
G1:1ゲーム目
圭:サーバー
〇×〇〇〇〇:勝ち負けが順番に書かれてます。
1-0:ゲーム後のゲームカウント
(1w1662c1522c1482c1481c):サーブの情報が順番に書かれています。
1w166 2c152 2c148 2c148 1c と分割でき、1w166は1stサーブ、コースはワイド、166km/hとなります。

これらを、正規表現という手法でゲーム、サーバー、etc、それぞれの項目を分割して抽出し、データとして格納していきました。

↓のような正規表現を使ったコードです。

Image

正規表現の基本的な考え方については下記をみるといいと思います。
サルにもわかる正規表現
リンク先では正規表現について、
「なぜこの表現方法が有名なのかといえば、この表現方法を利用すれば、たくさんの文章の中から容易に見つけたい文字列を検索することができるためです」
と書かれています。

これ以上の細かい説明は省略しますがデータの抽出ルールをコードに書いたら、あとはページを巡回して自動収集させてデータを格納させました。

他にも空白削除など細かい処理はいくつかやってますが、最後に記載したプログラムコードをみていただければわかるかと思います。

全体の傾向をATPデータと比較してみた

データ収集したら、
まずはATPサイトのServe&Return Trackerの錦織選手のデータと比較してみました。
全体の傾向としてはだいたい合っていそうです。

データのビジュアライズとか

収集したデータを使って、BIツール(redashとかtableau)などでビジュアライズさせるようなことできないかなあと、いろいろ試したりしています。
せっかくのデータなので良い形で公開できないかなと。

やるとしたらデータをAWSなどクラウド上に置く必要があるんですけど、従量課金なのでアクセス多いと大金を払う羽目になりそうで怖く。

いいやり方ないかなあ。

コードについて(Python)

githubで公開してます。

# coding: UTF-8
import re
import unicodedata 
import pandas as pd
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.select import Select
import lxml.html
import time

def createIndex(temp):
    for i,r in enumerate(temp):
        temp2=re.findall('G([0-9]+)(圭)(.*)([0-9])-([0-9])\((.*)\)',r)
        if(temp2[0][0] not in index):
            index.append(temp2[0][0])
            index2.append(i)
        index2[index.index(temp2[0][0])]=i
    return index,index2

def devideText(text1):#大会の中で何試合あるかをカウントする
    opponentPlayers=list(set(re.findall('.*?vs.*?\n',text1)))
    text1=text1.replace(" ","")
    text1=text1.replace(" ","")
    a=text1.split('\n\n')#コメント毎に抽出
    b=[s for s in a if '赤黄色' in s]#赤黄色が含まれるコメントだけ抽出
    c=[s for s in b if 'set' in s]#set
    d=[s for s in c if 'vs' in s]#vs

    array=[]#1⇒選手 2⇒set
    for k,kk in enumerate(opponentPlayers):
        array_temp=[]
        for i in range(9):
            array_temp.append("")
        array.append(array_temp)

    print(opponentPlayers)
    for i,ii in enumerate(opponentPlayers):
        e=[s for s in d if ii in s]#相手プレイヤー毎に抽出
        match = [s for s in e if re.findall('set[0-9][0-9]-[0-9](\([0-9]\))?\n', s)]#set2以降を抽出
        #print(match)
        for j,jj in enumerate(match):
            f=re.findall('set([0-9])[0-9]-[0-9](\([0-9]\))?\n', jj)
            print(f)
            print(f[0][0])
            g=array[i][int(f[0][0])]
            g+=jj
            array[i][int(f[0][0])]=g
        
        match = [s for s in e if re.findall('set1\n', s)]#set1を抽出
        for j,jj in enumerate(match):
            g=array[i][0]
            g+=jj
            array[i][0]=g
    return opponentPlayers,array

def initArray():
    totalGame=[]
    server=[]
    winLose=[]
    firstSecond=[]
    cource=[]
    speed=[]
    ad=[]
    row=[]
    index=[]
    index2=[]
    opponentPlayer=[]
    setArray=[]
    tournament=[]
    return totalGame,server,winLose,firstSecond,cource,speed,ad,row,index,index2,opponentPlayer,setArray,tournament

url = "サイトのURL"
html = urllib2.urlopen(url)

soup = BeautifulSoup(html, "html.parser")
table= soup.findAll("ul",{"class":"thread-list"})[0]
links = [a.get("href") for a in table.find_all("a")]

df = pd.DataFrame({'Tournament':[],'OpponentPlayer':[],'Set':[],'TotalGame':[], 'Server':[], 'WinLose':[],'FirstSecond':[], 'Cource':[], 'Speed':[], 'AceDbF':[]})
options = Options()
options.set_headless(True)# Headlessモードを有効にする(コメントアウトするとブラウザが実際に立ち上がります)
driver = webdriver.Chrome(chrome_options=options)# ブラウザを起動する

for i,l in enumerate(links):    
    url=l.replace("/l50","/")
    print(i,url)
    driver.get(url)# ブラウザでアクセスする

    time.sleep(1)
    title=driver.find_element_by_class_name('thread-title').text#全体をテクスト情報として入手    
    text=driver.find_element_by_id('thread-body').text#全体をテクスト情報として入手    

    text1=unicodedata.normalize('NFKC',text)#全角を半角に変換
    text2=re.sub("([0-9])(\.)",r"\1",text1)
    text2=re.sub("(Ac)(\.)",r"\1",text2)
    text2=re.sub("(Aw)(\.)",r"\1",text2)
    text2=re.sub("(Do)(\.)",r"\1",text2)
    text2=re.sub("(Dn)(\.)",r"\1",text2)
    text2=text2.replace(" ","")
    text2=text2.replace(" ","")
    opponentPlayers,array=devideText(text2)

    for p in range(len(array)):
        for s in range(len(array[p])):
            totalGame,server,winLose,firstSecond,cource,speed,ad,row,index,index2,opponentPlayer,setArray,tournament=initArray()
            temp=re.findall('G[0-9]+圭.*[0-9]-[0-9]\(.*\)',array[p][s])#サーブ記載行をすべて抽出
            index,index2=createIndex(temp)
            for iii,ii in enumerate(index2):
                r=temp[ii] 
                temp2=re.findall('G([0-9]+)(圭)(.*)([0-9])-([0-9])\((.*)\)',r)
                temp4=re.sub(r'([0-9][a-z])',r',\1',temp2[0][5].replace(" ",""))
                temp5=re.sub('^,',"",temp4)
                temp6=temp5.split(",")
                for i,t in enumerate(temp6):
                    temp7=re.search('[0-9][a-z]',t)    
                    temp8=re.search('[0-9]{3}',t)
                    temp9=re.search('[A-Z][a-z]',t)

                    tournament.append(title)
                    temp10=re.search('vs.(.*?)\n',opponentPlayers[p])
                    opponentPlayer.append(temp10.group(1))
                    setArray.append(s+1)

                    totalGame.append(temp2[0][0])
                    server.append(temp2[0][1])

                    if((temp2!=None) & (len(temp2[0][2])>i)):
                        winLose.append(temp2[0][2][i])
                    else:
                        winLose.append("")

                    if(t!=None):
                        firstSecond.append(t[0])
                    else:
                        firstSecond.append("")

                    if(len(t)>1):
                        cource.append(t[1])
                    else:
                        cource.append("")

                    if(temp8!=None):
                        speed.append(temp8.group(0))
                    else:
                        speed.append("")
                    if(temp9!=None):
                        ad.append(temp9.group(0))
                    else:
                        ad.append("")
            df_add = pd.DataFrame({'Tournament':tournament,'OpponentPlayer':opponentPlayer,'Set':setArray,'TotalGame':totalGame, 'Server':server, 'WinLose':winLose,'FirstSecond':firstSecond, 'Cource':cource, 'Speed':speed, 'AceDbF':ad})
            df=df.append(df_add)
df.to_csv("nosebleed-data-serve.csv")
driver.close()
driver.quit()
print("end")

 - blog, データ分析, 錦織圭