Twitter API の Search API を用いてツイート収集をしていた際にハマりました!
意外なところに落とし穴が・・・
Twitter の Search API について
今回は、ツイートを取得する Search API を利用しました。
Search API の仕様について簡単にまとめます。
Search APIには3種類のAPIがあります。
- Standard Search API (無料)
- Premium Search API (有料)
- Enterprise Search API (有料)
今回は、無料で使えるStandard Search APIを利用します。
Standard Search API の特徴
- 無料で利用可能
- リクエスト回数に制限あり
- user auth(OAuth1)で認証する場合 180リクエスト/15分
- app auth(OAuth2)で認証する場合 450リクエスト/15分
-
過去7日間のツイートまで取得可能
- 有料のAPIだと、さらに過去のツイートを取得可能
リクエストパラメータ
パラメータ | 説明 | 備考 |
---|---|---|
q | 検索クエリ(必須) | Twitterでのツイート検索と同様の検索が可能、文字列のみも可 |
geocode | ツイートした場所 | 緯度、経度、半径で指定 |
lang | ツイートの言語指定 | |
locale | クエリの言語指定 | 現在は日本語ja のみ有効 |
result_type | 取得ツイートの種類指定 |
recent なら最新のツイート、popular なら人気のツイート、mixed なら両方 |
count | 取得件数指定 | デフォルトは15件、最大で100件 |
until | ツイート時期の指定 | YYYY-MM-DD以前のツイートを取得(7日より前は取得不可) |
since_id | ID値指定 | 指定したID値より大きいID値のツイートを取得 |
max_id | ID値指定 | 指定したID値より小さいID値のツイートを取得 |
include_entities | entitiesの有無 |
false を指定するとentities情報を含まずにツイートを取得 |
レスポンスパラメータ
パラメータ | 説明 | 備考 |
---|---|---|
statuses | ツイートのリスト | ツイートオブジェクトがリストで格納されています |
search_metadata | 検索のメタデータ | 検索のメタデータが格納されています |
レスポンスの例
{
"statuses": [
(ツイートオブジェクトのため割愛),
...
],
"search_metadata": {
"max_id": 250126199840518145,
"since_id": 24012619984051000,
"refresh_url": "?since_id=250126199840518145&q=%23freebandnames&result_type=mixed&include_entities=1",
"next_results": "?max_id=249279667666817023&q=%23freebandnames&count=4&include_entities=1&result_type=mixed",
"count": 4,
"completed_in": 0.035,
"since_id_str": "24012619984051000",
"query": "%23freebandnames",
"max_id_str": "250126199840518145"
}
}
問題の発生
経緯
#Qiita
のハッシュタグが付いたツイートを大量に集めようとしていました。
しかし、Standard Search API では、1回のリクエストで最大100件のツイートしか取得できません。
そこで、リクエストパラメータnext_results
を利用することで再帰的にAPIを呼び出すことで1000件のツイート取得を試みました。
next_results
にはクエリが格納されており、このクエリを実行することで101件目以降のツイートを取得できるようになります。
つまり、
リクエスト→レスポンス→next_results
をパース→次のリクエストパラメータへ→リクエスト→・・・
という流れを1000件取得するまで行います。
(参考:Twitter API search/tweets で 100件以上のツイートを取得する(PHP))
しかし、リクエストは3回分しか実行されずしかも200件しかツイートが取れません!
(3回目のレスポンスはツイート取得数が0となっている)
明らかにツイートが200件を超えているのにも関わらずです・・・。
プログラム
コードはPythonで書きました。
また、各種APIキーは環境変数に登録しています。
from requests_oauthlib import OAuth1Session
import os
import json
#APIキーの設置
CONSUMER_KEY = XXXXXXXXXXXXXXXXXXXXXX #API key
CONSUMER_SECRET = XXXXXXXXXXXXXXXXXXXXXX #API secret
ACCESS_TOKEN = XXXXXXXXXXXXXXXXXXXXXX
ACCESS_SECRET = XXXXXXXXXXXXXXXXXXXXXX
# ツイート取得用のURL
SEARCH_URL = 'https://api.twitter.com/1.1/search/tweets.json'
def search(params):
twitter = OAuth1Session(CONSUMER_KEY, CONSUMER_SECRET, ACCESS_TOKEN, ACCESS_SECRET)
req = twitter.get(SEARCH_URL, params = params)
tweets = json.loads(req.text)
return tweets
# PHPにおけるparse_str関数の代わり
def parseToParam(parse_str, parse=None):
if parse is None:
parse = '&'
return_params = {}
parsed_str = parse_str.split(parse)
for param_string in parsed_str:
param, value = param_string.split('=', 1)
return_params[param] = value
return return_params
def main():
search_word = '#Qiita'
tweet_data = []
# Tweet Search
params = {
'q' : search_word,
'count' : 100,
}
tweet_count = 0
while tweet_count < 1000:
tweets = search(params)
for tweet in tweets['statuses']:
tweet_data.append(tweet)
# tweets['search_metadata']['next_results'] をパースしてparamへ
if 'next_results' in tweets['search_metadata'].keys():
next_results = tweets['search_metadata']['next_results']
next_results = next_results.lstrip('?') # 先頭の?を削除
params = parseToParam(next_results)
tweet_count += len(tweets['statuses'])
else:
break
if __name__=='__main__':
main()
原因の調査
レスポンスパラメータnext_results
を次回のリクエストパラメータに利用していることから、
- リクエストパラメータ
- レスポンスパラメータ
next_results
の2点を確認します。
リクエストパラメータとレスポンスパラメータの確認
1回目
リクエストパラメータ
{
'q' : '#Qiita',
'count': 100
}
レスポンスパラメータnext_results
?max_id=1250763045871079425&q=%23Qiita&count=100&include_entities=1
2回目
リクエストパラメータ
{
'max_id': '1250763045871079425',
'q' : '%23Qiita',
'count': 100,
'include_entities': '1'
}
レスポンスパラメータnext_results
?max_id=1250673475351572480&q=%2523Qiita&count=100&include_entities=1
3回目
リクエストパラメータ
{
'max_id': '1250673475351572480',
'q' : '%2523Qiita',
'count': 100,
'include_entities': '1'
}
レスポンスパラメータnext_results
None
調査結果
どうやら、本来は同じクエリが引き継がれていくところを
#Qiita
→ %23Qiita
→ %2523Qiita
とクエリが変遷しているようです。
#Qiita
と %23Qiita
はURLエンコーディングによって互換が効いていますが、%2523Qiita
に至っては完全に別のクエリとなっています。
(こちらでエンコード・デコードを試すと確認できます。)
すなわち、
%23Qiita
→ %2523Qiita
の過程で**%がデコードされている**ことが問題の原因なようです。
解決策
レスポンスパラメータnext_results
をパースした後、リクエストパラメータ中の**%25を%に置換**します。
プログラムの修正
while文の中に、パラメータの置換処理を追加
while tweet_count < 1000:
tweets = search(params)
for tweet in tweets['statuses']:
tweet_data.append(tweet)
# tweets['search_metadata']['next_results'] をパースしてparamへ
if 'next_results' in tweets['search_metadata'].keys():
next_results = tweets['search_metadata']['next_results']
next_results = next_results.lstrip('?') # 先頭の?を削除
params = parseToParam(next_results)
# %25の置換処理を追加
params['q'] = params['q'].replace('%25', '%')
tweet_count += len(tweets['statuses'])
else:
break
まとめ
レスポンスパラメータnext_results
中に含まれるクエリq
にて、URLエンコーディング後の%が余分にエンコードされていました。
それにより、クエリの引き継ぎがうまくいかずにツイートの取得で問題が発生していました。
解決策として、next_results
で余分にエンコードされてしまった%を文字列置換で復元しました。
## 参考
Twitter API公式ドキュメント
https://developer.twitter.com/en/docs/tweets/search/api-reference/get-search-tweets
Twitter 開発者ドキュメント日本語訳
http://westplain.sakuraweb.com/translate/twitter/Documentation/REST-APIs/Public-API/GET-search-tweets.cgi
Twitter API search/tweets で 100件以上のツイートを取得する(PHP)
https://blog.apar.jp/php/3007/
URLエンコード・デコード
https://tech-unlimited.com/urlencode.html