プロ野球の実況ツイートを全部取得する

# はじめに

プロ野球の実況ツイートを集めて，自然言語処理系の分析をしています。
ここでは，おもにスクレイピングのはなしと，tweepyを使った大量のツイート取得について書いています。

2019年度の記録と，2020年度の途中までの記録を載せています。
試合のデータをスクレイピングして，ツイート検索の時刻を決定しているのですが，2019年度と2020年度で，スクレイピングしていたページの構造が変わったため，2020年度の方に更新しました。

# 2019年プロ野球(NPB)の実況ツイート

2019年プロ野球(NPB)の実況ツイートをハッシュタグで毎日取得し続けた結果です。
Pythonファイルをcronで毎日実行して１年間取得しました。

![スクリーンショット 2019-10-16 15.21.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597078/d6a4a939-720b-c5f2-a2e7-34597cbcec08.png)

※ たまにバグで取得できていないところや，試合が中止になったため途切れているところもあるので，
　そこはご了承ください

|        セリーグ   |                   |    パリーグ         |                      |
|:----------------:|:------------------|:------------------:|:-------------------|
| 巨人             |   #kyojin, #giants |        日本ハム        |      #lovefighters       |
| 中日             |  # dragons         |       ソフトバンク       |      #sbhawks       |
| 広島             |  #carp            |        楽天        |      #rakuteneagles       |
| ヤクルト          | #swallows,#yakultswallows |         西武       |    #seibulions     |
| 阪神             | #hanshin,#tigers   |       ロッテ       |      #chibalotte       |
| ＤｅＮＡ          | #baystars      |      オリックス       |      #Orix_Buffaloes       |


# 2020年プロ野球(NPB)の実況ツイート
（2020年8月15日まで）
![スクリーンショット 2020-08-17 1.49.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/597078/5791f0c7-b3db-9d51-6ab7-cea5515d841d.png)

取得したツイートのハッシュタグ
2020年度内でも途中で変更を行いましたが，最終的に下の通りに落ち着きました。

|        セリーグ   |                   |    パリーグ         |                      |
|:----------------:|:------------------|:------------------:|:-------------------|
| 巨人             |   #kyojin, #giants |        日本ハム        |      #lovefighters, #日ハム       |
| 中日             |  #dragons, #中日ドラゴンズ         |       ソフトバンク       |      #sbhawks, #ホークス       |
| 広島             |  #carp, #広島カープ            |        楽天        |      #rakuteneagles, #楽天イーグルス       |
| ヤクルト          | #swallows, #ヤクルトスワローズ |         西武       |    #seibulions, #西武ライオンズ     |
| 阪神             | #hanshin, #阪神タイガース   |       ロッテ       |      #chibalotte, #千葉ロッテマリーンズ       |
| ＤｅＮＡ          | #baystars, #ベイスターズ      |      オリックス       |      #bs2020, #バファローズ       |


# 実況ツイート取得の概要

### 1. 対象の日の対戦カード・試合の時刻を取得

スポーツの速報などを提供しているWebサイトをスクレイピングすることによって取得する。

- スクレイピング先
    - SPORTS BULL（https://sportsbull.jp/stats/npb/）
    - スポーツナビ (by Yahoo!JAPAN)（https://baseball.yahoo.co.jp/npb/schedule/）

### 2. 試合の時刻を指定して，ハッシュタグから実況ツイートを取得

#### 各チームのIDとハッシュタグ（検索クエリ）

ハッシュタグの辞書型オブジェクト(tag_list)
key：自分が定めた team_id
item：ハッシュタグ（ツイート検索するときのクエリ）

```python
tag_list = {0: '#kyojin OR #giants', 1: '#dragons OR #中日ドラゴンズ',\
            2: '#carp OR #広島カープ', 3: '#swallows OR #ヤクルトスワローズ',\
            4: '#hanshin OR #阪神タイガース', 5: '#baystars OR #ベイスターズ',\
            6: '#lovefighters OR #日ハム ', 7: '#sbhawks OR #ホークス',\
            8: '#rakuteneagles OR #楽天イーグルス', 9: '#seibulions OR #西武ライオンズ',\
            10: '#chibalotte OR #千葉ロッテマリーンズ', 11: '#bs2020 OR #バファローズ'}
```


# 実装


- 仕様ライブラリ(Python)
今回使うライブラリは以下の通り。
適宜インストールしてください。

```python:getLiveTweet_NPB.py
from urllib.request import urlopen
import tweepy
from datetime import timedelta
import time
import sqlite3
from contextlib import closing
import datetime
from bs4 import BeautifulSoup
import urllib.request as req
```

## 対戦カードを取得

SPORTS BULL（https://sportsbull.jp/stats/npb/）
から指定した日時に行われた試合をスクレイピングする。
スポナビから取得しても良いが，こちらの方がHTMLの構造が簡単。

```python:getLiveTweet_NPB.py

def get_gameteamId(gamedate):
    url = 'https://sportsbull.jp/stats/npb/home/index/' + gamedate
    print(url)
    res = req.urlopen(url)
    soup = BeautifulSoup(res, 'html.parser')
    q = soup.select('.game-block a')
    gameId_list = []
    flag_list = [1 for i in range(12)]
    i = 0
    for p in q:
        # print(p)
        urls = p.get('href')
        # print(urls)
        p_ = p.select('.st-03')
        #print(p_)
        for p__ in p_:
            if '中止' in str(p__.text) or 'ノーゲーム' in str(p__.text):
                print('中止')
                flag_list[i] = 0
                flag_list[i+1] = 0
        if flag_list[i] == 1:
            print(urls[-10:])
            gameId_list.append(urls[-10:])
        i += 2
    print('flag_list: ',flag_list)
    q = soup.select('.game-block .play-box01 dt')
    teamId_list = []
    # 研究共通
    teamId_dic = {'巨人': 0, '中日': 1, '広島': 2, 'ヤクルト': 3, '阪神': 4, 'ＤｅＮＡ': 5,
                  '日本ハム': 6, 'ソフトバンク': 7, '楽天': 8, '西武': 9, 'ロッテ': 10, 'オリックス': 11}
    i = 0
    for p in q:
        print(p.text)
        if flag_list[i] == 1:
            teamId_list.append(teamId_dic[p.text])
        i += 1
    return gameId_list, teamId_list


#日付
def get_date(days_ago):
	date = datetime.date.today()
	date -= datetime.timedelta(days=days_ago)
	date_str = str(date)
	date_str = date_str[:4]+date_str[5:7]+date_str[8:10]
	return date_str


# 例 --------------------------
n = 1
game_date = get_date(n) # 自動(n日前のデータを取得)
game_date = '20200401' # 手入力
print(game_date,'のデータを取得,')
# -----------------------------

#ゲームID，チームIDのリスト
gameteamId_list = get_gameteamId(game_date)
gameId_list = gameteamId_list[0]
teamId_list = gameteamId_list[1]
print('gameId_list:',gameId_list)
print('teamId_list:',teamId_list)
```

実行結果の例

```
20200401 のデータを取得
https://sportsbull.jp/stats/npb/home/index/20200401
flag_list: [1,1,1,1,0,0,0,0,0,0,0,0]
gameId_list: [2020040101,2020040102]
teamId_list: [0,1,2,3]
```

この場合，
gameId = 2019040101 で 巨人(Home) vs 中日(away)
gameId = 2019040102 で 広島(Home) vs ヤクルト(away)
の試合が行われたとなる

## 試合の開始時間と終了時間を取得

Yahoo!のスポナビ（https://baseball.yahoo.co.jp/npb/schedule/）
の各試合のページ
https://baseball.yahoo.co.jp/npb/game/[game_id]/top
から開始時間と試合時間がとれるため，足し算して開始時間と終了時間を取得する

```python:getLiveTweet_NPB.py

# 試合の開始時間と終了時間をスクレイピングで取得
def gametime(game_id):
    url = 'https://baseball.yahoo.co.jp/npb/game/' + game_id + '/top'
    print(url)
    res = req.urlopen(url)
    soup = BeautifulSoup(res, 'html.parser')
    time = []

    # 開始時間
    css_select = '#contentMain .bb-gameDescription time'
    q = soup.select(css_select)
    print(q[0].text)
    time.append(q[0].text[-5:-3])
    time.append(q[0].text[-2:])

    # 終了時間
    minutes = []
    css_select = '.bb-tableLeft .bb-tableLeft__data'
    q = soup.select(css_select)
    for q_ in q:
        # print(q_.text)
        if '時間' in q_.text:
            minutes = q_.text.split('時間')
    minutes[1] = minutes[1][:-1]
    print(minutes)

    time = time + minutes
    return time
```

↑この関数の出力

```python
# 開始時間が18:00，試合時間が3時間15分の場合
[18,0,21,15]
```


## TwitterAPIで検索

TwitterAPIのsearchを使って時間内の全ツイートを取得する。
１度のリクエストで取得できるツイートは100件なので，それを繰り返す。
API制限に引っかかったら，一時停止して15待つ。

試合開始から試合終了５分後までのツイートを対象にしている。


```python:getLiveTweet_NPB.py

# TwitterAPI
APIK = 'consumer_key'
APIS = 'consumer_secret'
AT = 'access_token'
AS = 'access_token_secret'
auth = tweepy.OAuthHandler(APIK, APIS)
auth.set_access_token(AT, AS)
api = tweepy.API(auth)

# TwitterAPI検索
def search_livetweet(team_num, api, game_id, query):
    print(query)    # 最新のつぶやきから取得
    print('検索ページ：1')
    try:
        tweet_data = api.search(q=query, count=1)
    except tweepy.TweepError as e:
        print('エラー：15ふん待ちます')
        time.sleep(60 * 15)

    tweet_data = api.search(q=query, count=100)
    table_name = 'team' + str(team_num)
    # この関数はデータベースに保存する用
    saveDB_tweet(table_name, 0, tweet_data, game_id)
    print('************************************************\n')
    next_max_id = tweet_data[-1].id

    page = 1
    while True:
        page += 1
        print('検索ページ：' + str(page))
        try:
            tweet_data = api.search(q=query, count=100, max_id=next_max_id - 1)
            if len(tweet_data) == 0:
                break
            else:
                next_max_id = tweet_data[-1].id
                # この関数はデータベースに保存する用
                saveDB_tweet(table_name, page - 1, tweet_data, game_id)
        except tweepy.TweepError as e:
            print('エラー：15ふん待ちます')
            print(datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"))
            print(e.reason)
            time.sleep(60 * 15)
            continue
        print('*'*40 + '\n')


# 時間指定 → クエリを作成 → ツイート検索関数（search_livetweet()）を実行
def get_livetweet(team_id, game_id):
    date = game_id[:4] + '-' + game_id[4:6] + '-' + game_id[6:8]
    time = gametime(game_id)
    sh, sm = time[0], time[1]
    eh = int(time[0]) + int(time[2])
    em = int(time[1]) + int(time[3]) + 5  # 終了後5分
    if em >= 60:
        em -= 60
        eh += 1
    eh = '{0:02d}'.format(eh)
    em = '{0:02d}'.format(em)
    
    print(date, sh, sm, eh, em)
    tag_list = {0: '#kyojin OR #giants', 1: '#dragons OR #中日ドラゴンズ',\
            2: '#carp OR #広島カープ', 3: '#swallows OR #ヤクルトスワローズ',\
            4: '#hanshin OR #阪神タイガース', 5: '#baystars OR #ベイスターズ',\
            6: '#lovefighters OR #日ハム ', 7: '#sbhawks OR #ホークス',\
            8: '#rakuteneagles OR #楽天イーグルス', 9: '#seibulions OR #西武ライオンズ',\
            10: '#chibalotte OR #千葉ロッテマリーンズ', 11: '#bs2020 OR #バファローズ'}
    tag = tag_list[team_num]
    query = tag + ' exclude:retweets exclude:replies\
            since:' + date + '_' + sh + ':' + sm + ':00_JST \
            until:' + date + '_' + eh + ':' + em + ':59_JST lang:ja'
    search_livetweet(team_id, api, game_id, query)
```

## ここで上記の関数を実行
上で作られた gameId_list，teamId_list  から，一つの試合ごとに２つのチームのツイートを取得する

```python:getLiveTweet_NPB.py

for i in range(len(gameId_list)):
    game_id = gameId_list[i]

    #away
    team_id = teamId_list[2*i+1]
    get_livetweet(team_id, game_id)
    print('='*60 + '\n')

    #home
    team_id = teamId_list[2*i]
    get_livetweet(team_id, game_id)
    print('='*60 + '\n')
```


# おわりに

試合が雨で中断したりなどすると，もしかしたらツイートを全て取得できていない場合もあるかもしれないで，
そのあたりの改善は必要そうです。

ツイートを時間指定して取得する部分は，どのドメインでも使えることなので，参考になればと思います。