LoginSignup
12
10

More than 3 years have passed since last update.

Twitter API の Search API で"next_results"パラメータが使えない!?その原因と対処法

Last updated at Posted at 2020-04-17

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キーは環境変数に登録しています。

get_tweet.py
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文の中に、パラメータの置換処理を追加

get_tweet.py
    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

12
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
10