Tweepyをアップデートしたタイミングで、使用するTwitter APIを v1.1 から v2 に切り替えてみた

以前、個人アプリを Python 3.10 & Django 4.1 へとアップデートしました。
Python3.7 & Django 2.1 な個人アプリを Python 3.10 & Django 4.1 へとアップデートした - メモ的な思考的な

その際、 pip-review で他のライブラリのバージョンも最新に上げたため、Tweepyも 3.8 から 4.10.1 になりました。

ただ、その時はTweepyまわりのソースコードを修正しなかったため、Tweepyの中で使用しているTwitter APIは1.1のままでした。

せっかくなので、Tweepyの中で使っているTwitter APIv1.1 から v2 へ切り替えようと考えました。

そこで、実際に切り替えてみた時のメモを残します。

 
目次

 

環境

 

切り替え作業

Twitter Developer account を申請

しばらく放置していたこともあり、Twitterの Developer Portal を開いたところ申請が必要な状態になっていました。
https://developer.twitter.com/en/portal/dashboard

 
そこで、画面の内容に従い、必要な項目を入力して申請を行いました。

「審査を開始する」メールが届いたものの、すぐには審査完了になりませんでした。

自分の場合は、約9時間後に無事審査が通りました。

 

Tweepyのドキュメントから、API v2で使えそうなメソッドを探す

Tweepy切り替え前は

tweepy.Cursor(api.user_timeline, **options).items(TWEET_COUNT)

のようにして、自分のタイムラインから自分のツイートを拾っていました。

そこで、Twitter API v2 + Tweepy を使う時の方法をTweepyのドキュメントで調べてみました。
Client — tweepy 4.10.1 documentation

 
自分のタイムラインからツイートを拾えそうなメソッドとしては

の2つがありました。

試してみたところ、

  • Client.get_home_timeline
    • 自分のタイムラインのツイートを取得
      • 自分以外のツイートも取得されてた
  • Client.get_users_tweets
    • 指定したユーザーのツイートを取得
    • 自分を指定すれば、自分のツイートを拾える

の違いがあったため、今回は Client.get_users_tweets を使うことにしました。

 

Client生成時に access_token と access_token_secret を指定する

今までは

auth = tweepy.AppAuthHandler(
    os.environ['TWITTER_CONSUMER_KEY'],
    os.environ['TWITTER_CONSUMER_SECRET'])
api = tweepy.API(auth)

のように、 consumer_keyconsumer_secret だけ指定すれば動作していました。

 
ただ、API v2では access_tokenaccess_token_secret も指定する必要があるため、追加します。
Client — tweepy 4.10.1 documentation

Client(
    consumer_key=os.environ['TWITTER_CONSUMER_KEY'],
    consumer_secret=os.environ['TWITTER_CONSUMER_SECRET'],
    access_token=os.environ['TWITTER_ACCESS_TOKEN'],
    access_token_secret=os.environ['TWITTER_ACCESS_TOKEN_SECRET']
)

 
なお、 access_tokenaccess_token_secret.env ファイルに設定し、リポジトリに追加しないようにします。

 

Client.get_me を使って、自分の user ID を取得する

Client.get_users_tweets のパラメータの説明に

Unique identifier of the Twitter account (user ID) for whom to return results. User ID can be referenced using the user/lookup endpoint. More information on Twitter IDs is here.

とあり、Client.get_users_tweets を使うには、自分の user ID が必要でした。ただ、 user ID はAPI経由で取得するしかなさそうでした。

 
そこでTweepy のメソッドを探してみたところ、 Client.get_me が使えそうでした。
https://docs.tweepy.org/en/stable/client.html#tweepy.Client.get_me

ただ、リクエストの都度 Client.get_me を呼んで user ID を取得するのはムダなため、

  • 取得した user ID をコンソールへ出力
  • 出力された内容を環境変数( .env) へ手で設定
  • API v2で user ID を使う場合は、環境変数から読み込む

とすることにしました。

そこで今回は、Djangoコマンドとして Client.get_me をラップしたものを作成しました。

import os

from django.core.management.base import BaseCommand
from tweepy import Client


class Command(BaseCommand):
    """ Twitter GET /2/users/me から自分の user_id を取得し、コンソールに表示する

        https://docs.tweepy.org/en/stable/client.html#tweepy.Client.get_me
        なお、取得した user_id は .env や環境変数に設定すること
    """
    def handle(self, *args, **options):
        client = Client(
            consumer_key=os.environ['TWITTER_CONSUMER_KEY'],
            consumer_secret=os.environ['TWITTER_CONSUMER_SECRET'],
            access_token=os.environ['TWITTER_ACCESS_TOKEN'],
            access_token_secret=os.environ['TWITTER_ACCESS_TOKEN_SECRET']
        )

        response = client.get_me()
        print(response)

 
実行すると、こんな感じでレスポンスが返ってきます。この中の User id を使えば良さそうです。

Response(data=<User id=*** name=thinkAmi username=thinkAmi>, includes={}, errors=[], meta={})

 

Client.get_users_tweets を使って自分のツイートを取得

user ID を取得できたので、次は Client.get_users_tweets を使って自分のツイートを取得します。

Client.get_users_tweets には引数があるため、今回必要そうな引数を設定します。

今回はこんな感じの実装になりました。これでTweepyにおける切り替え作業は終わりです。

self.twitter_client.get_users_tweets(
            id=os.environ['USER_ID'],
            exclude=['retweets', ],
            tweet_fields=['created_at', ],
            since_id=self.last_search.prev_since_id,
            user_auth=False,
            pagination_token=pagination_token
        )

 
なお、引数については以降で詳しく見ていきます。

 

Client.get_users_tweetsの引数について

id

Client.get_me で取得した user ID を設定します。

 

exclude

リツイートやリプライを除外するか指定できそうです。

ただ、Tweepyの説明には

When exclude=retweets is used, the maximum historical Tweets returned is still 3200. When the exclude=replies parameter is used for any value, only the most recent 800

とあり、 replies を指定すると取得できるツイート数が減ってしまいそうでした。

自分の場合はほとんどリプライを使っていないため、 retweets のみ指定することにしました。

 

tweet_fields

Tweepyの説明には

For methods that return Tweets, this fields parameter enables you to select which specific Tweet fields will deliver in each returned Tweet object. Specify the desired fields in a comma-separated list without spaces between commas and fields.

https://docs.tweepy.org/en/stable/expansions_and_fields.html#tweet-fields-parameter

とありました。

Twitterのドキュメントを見ると、デフォルトで返ってきそうなのは idtext だけのようでした。
Tweet object | Docs | Twitter Developer Platform

 
個人アプリでは created_at も利用していたため、 tweet_fields に created_at を指定することとしました。

 

since_id

API v1.1では「指定したid以降のツイートを取得する」という場合、 since_id を指定していました。
https://github.com/thinkAmi/dj_ringo_tabetter/blob/16a6663992/apps/tweets/management/commands/gather_tweets.py#L68

API v2 でも同じようなことができるかをTweepyのドキュメントで見たところ、 since_id がありました。引き続き利用すれば良さそうです。

 

user_auth

Client.get_users_tweets では、引数 user_auth のデフォルト値は False でした。

今回は OAuth 1.0a User Context to authentication を使うため、 user_auth=True にします。

False のままの場合、401エラーが返ってきます。

tweepy.errors.Unauthorized: 401 Unauthorized
Unauthorized

 

pagination_token

ツイートの取得が大量の場合、API v1.1 の時は

tweepy.Cursor(api.user_timeline, **options).items(TWEET_COUNT)

# https://github.com/thinkAmi/dj_ringo_tabetter/blob/16a6663992/apps/tweets/management/commands/gather_tweets.py#L61

のように Cursortimes を使っていました。

 
API v2の場合は next_tokenprevious_token のいずれかを指定することで、「次はここから取得する」を指定できそうです。
GET /2/users/:id/tweets | Docs | Twitter Developer Platform

今回は since_id を指定することから、 next_token がレスポンスとして返ってきます。その next_token の値を次のリクエストの pagination_token に載せれば良さそうです。

 

レスポンスと next_token について

今回 since_id を指定するため、いくつかのレスポンスパターンがありそうです。

どんな形でレスポンスされるのか見てみます。

 

since_id 以降のツイートがない場合

Tweepyの場合、 dataに None が返ってくるようです。

Response(data=None, includes={}, errors=[], meta={'result_count': 0})

 

since_id 以降のツイートはあるが、一度のリクエストですべて取得できる場合

data にツイートオブジェクトが含まれます。

一方、 meta には next_token がありません。

Response(data=[<Tweet id=*** text='***'>, ...], includes={}, errors=[], meta={'result_count': 1, 'newest_id': '***', 'oldest_id': '***'})

 

since_id 以降のツイートはあり、かつ、一度のリクエストで取得しきれない場合

dataTweetオブジェクトがあり、かつ、 metanext_token が含まれます。

Response(data=[<Tweet id=*** text='***'>, ...], includes={}, errors=[], meta={'result_count': 5, 'newest_id': '***', 'oldest_id': '***', 'next_token': '***'})

 

TweepyのTweetオブジェクトの型について

公式ドキュメントの以下に記載がありました。
Models — tweepy 4.10.1 documentation

 

動作確認

Djangoカスタムコマンド

$ python manage.py gather_tweets

を実行し、エラーとならないことを確認しました。

 

その他

Twitter API v2 へ切り替える作業はここまでで終わりです。

ただ、個人アプリではTweepyに関係ない部分にも修正を加えましたので、メモとして残しておきます。

 

Django の get_or_create や update_or_create を使うようにした

最近のDjangoでは

ができるので、実装を変更しました。

なお

Asynchronous version: aupdate_or_create()

もあるようですが、現在のDjangoアプリでは Asynchronous version を使っていないため、 aupdate_or_create などは使用していません。

 

SQLでデータベースの重複データの削除をした

改めてデータベースの中身を見たところ、いくつか重複しているツイートが存在しました。

アプリ作成当初はデータベースを適当に作っていたのが原因でしょう。。。

そこで、まずはどれだけデータが重複しているかを調べてみました。

SELECT id, tweet_id, name
FROM tweets_tweets
WHERE id NOT IN (
    SELECT tmp.id FROM (
        SELECT min(id) AS id
        FROM tweets_tweets
        GROUP BY tweet_id
    ) AS tmp
)
ORDER BY tweet_id DESC

 
データが存在することを確認できたら、一番古いid以外の重複データを削除します。

DELETE FROM tweets_tweets
WHERE id NOT IN (
    SELECT tmp.id FROM (
        SELECT min(id) AS id
        FROM tweets_tweets
        GROUP BY tweet_id
    ) AS tmp
)

 

Tweetモデルにユニーク制約を追加

運用していて tweet_id は重複することがないとわかったため、 Tweet モデルの tweet_id にユニーク制約をつけることにしました。

class Tweets(models.Model):
    """ リンゴに関係するツイートを持つModel """
    ...
    tweet_id = models.BigIntegerField('Tweet ID', unique=True)  # 追加
    ...

 
マイグレーションの作成と適用を行います。

$ python manage.py makemigrations

$ python manage.py migrate

 

LastSearchモデルに updated_at を追加

機能としては不要なのですが、運用する中で「LastSearchの更新がきちんと行えているかを知るために、いつLastSearchモデルを更新したか」を把握したくなりました。

そこで、LastSearchモデルに udpated_at を追加しました。

なお、 auto_now=True を付与し、データの更新をするたびに updated_at も更新されるようにします。

class LastSearch(models.Model):
    """ 前回検索時の情報を持たせておくModel """
    prev_since_id = models.BigIntegerField('前回検索時のsince_id')
    updated_at = models.DateTimeField('更新日時', auto_now=True)  # 追加

 
こちらもマイグレーションの作成と適用を行います。

 

テストコードを修正

Twitter API v2 対応に伴い、テストコードを修正しました。

また、実装ロジックも修正したため、不要となったテストコードは削除しました。

 

ソースコード

Githubに上げました。
https://github.com/thinkAmi/dj_ringo_tabetter

今回のプルリクはこちらです。
https://github.com/thinkAmi/dj_ringo_tabetter/pull/18