分析目的のTwitterデータ収集方法（Python・TwitterAPI）

<font color="Red">※2019年3月時点の情報です</font>

Twitterデータを分析しようと思い、データ収集方法を調べました。
地味に色々なサイトを見て回ることになったので備忘録としてまとめておきます。

# 準備
Twitterは事前の承諾なしのスクレイピングを禁止しているので、APIを使う必要があります。

Twitter APIの登録については以下の記事がわかりやすいです。
[Twitter API 登録 (アカウント申請方法) から承認されるまでの手順まとめ](https://qiita.com/kngsym2018/items/2524d21455aac111cdee)

300文字の英作文では以下の記事が役に立ちました。
[TwitterAPIの申請に必要な英作文をユルく和訳してみた](https://qiita.com/tomon9086/items/7939ed83b40c9128826f)

私の場合、新規に作成したアカウントで申請から4時間程度で承認されました。

# 収集
## 使うライブラリ
- OAuth1Session
- twitter
- python-twitter
- tweepy

などからどれか好きなもの。どれを使っても大差はないと思います。
今回は一番使用例が多いように感じたOAuth1Sessionを使います。

## APIを投げるURL
https://api.twitter.com/1.1/search/tweets.json?tweet_mode=extended
末尾に?tweet_mode=extendedをつけない場合、ツイートが140文字を超えると省略されたものが取得されてしまいます。

## シンプルな取得コード例
```python
from requests_oauthlib import OAuth1Session

# トークン情報
CONSUMER_KEY = 'xxxxxxxxxx'
CONSUMER_SECRET = 'xxxxxxxxxx'
ACCESS_TOKEN = 'xxxxxxxxxx'
ACCESS_TOKEN_SECRET = 'xxxxxxxxxx'
# OAuth認証
api = OAuth1Session(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)

# 「hogehoge」が含まれるツイートを5つ取得
url = 'https://api.twitter.com/1.1/search/tweets.json?tweet_mode=extended'
params = {'q': 'hogehoge', 'count': 5}
req = api.get(url, params=params)
```
[Twitter開発者ドキュメント日本語訳](http://westplain.sakuraweb.com/translate/twitter/Documentation/REST-APIs/Public-API/GET-search-tweets.cgi)
公式ドキュメントの日本語訳があるので非常に参考になります。
ただしドキュメントに書かれていない検索術などがあるようです。

- params

paramsで検索条件を設定します。
クエリの具体例は以下がわかりやすいです。
https://gist.github.com/cucmberium/e687e88565b6a9ca7039

- 戻り値req
    - ステータス部　req.status_code
    - ヘッダ部　req.headers
    - テキスト部　req.text

正常に取得できている場合、取得ステータス（req.status_code）の値は200です。
取得ツイート情報はreq.textに指定数（count）分だけjson形式で入ります。
JSONの例は以下です。
https://github.com/twitterdev/tweet-updates/blob/master/samples/initial/extended_extended_14001.json

# サンプルコード
ハッシュタグhogehogeが含まれるツイートをリツイートを除外して新しいものから順に取得できるだけ取得。
取得したデータはPandasのDataFrameにしてPickleで保存。

```python
import os
import time
import json
import pandas as pd
from requests_oauthlib import OAuth1Session

CONSUMER_KEY = 'xxxxxxxxxx'
CONSUMER_SECRET = 'xxxxxxxxxx'
ACCESS_TOKEN = 'xxxxxxxxxx'
ACCESS_TOKEN_SECRET = 'xxxxxxxxxx'

class TwitterApi:
    
    def __init__(self, search_word, count):
    
        self.twitter_api = OAuth1Session(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_TOKEN_SECRET)               
        self.url = 'https://api.twitter.com/1.1/search/tweets.json?tweet_mode=extended'
        self.params = {'q': search_word, 'count': count, 'result_type': 'recent', 'exclude': 'retweets'}
        self.tweet_num = count
        
    def get_next_tweets(self):
        req = self.twitter_api.get(self.url, params=self.params)
        
        # 正常に取得できている場合
        if req.status_code == 200:
            
            self.x_rate_limit_remaining = req.headers['X-Rate-Limit-Remaining']
            self.x_rate_limit_reset = req.headers['X-Rate-Limit-Reset']
            
            self.tweets = json.loads(req.text)
            
            self.tweet_num = len(self.tweets['statuses'])
            if self.tweet_num == 0:
                return True
                
            self.max_id = self.tweets['statuses'][0]['id']
            self.min_id = self.tweets['statuses'][-1]['id']
            next_max_id = self.min_id - 1
            self.params['max_id'] = next_max_id
            
            return True
            
        else:
            return False

# 検索語
search_word = '#hogehoge'
count = 100

twitter_api = TwitterApi(search_word, count)

tweets_df = pd.DataFrame([])

while twitter_api.tweet_num > 0:
    ret = twitter_api.get_next_tweets()
    
    if twitter_api.tweet_num == 0:
            break
    
    if ret:
        # PandasのDataFrameに変換
        df = pd.io.json.json_normalize(twitter_api.tweets['statuses'])
        tweets_df = pd.concat([tweets_df, df], axis=0)
        
        print('アクセス可能回数:', twitter_api.x_rate_limit_remaining, ' リセット時間:', twitter_api.x_rate_limit_reset)
        
    else:
        # エラー時はとりあえず60秒待って再取得
        print('Error!, Wait 60s...')
        time.sleep(60)

# ツイート日時列をTimeStamp型にして降順に並び替え、Pickle保存
tweets_df['created_at'] = pd.to_datetime(tweets_df['created_at'])
tweets_df = tweets_df.sort_values('created_at').reset_index(drop=True)
tweets_df.to_pickle('saved_tweets_df.pickle')
```
本当はアクセス可能回数やリセット時間をちゃんと見て再取得するべきですが、それらが正常に取得できない場合があるみたいなのでひとまず適当に再取得しています。
ツイートIDは新しいものほど大きな値が振られているので、再取得時には前回取得した最小IDの値より小さいmax_id条件を設定します。

# その他参考
末尾に?tweet_mode=extendedをつけない場合のツイート取得
http://ailaby.com/twitter_api/#id7_2