以前、個人アプリを 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 APIも v1.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
自分のタイムラインからツイートを拾えそうなメソッドとしては
- Client.get_home_timeline
- Client.get_users_tweets
の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_key
と consumer_secret
だけ指定すれば動作していました。
ただ、API v2では access_token
と access_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_token
と access_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 を取得するのはムダなため、
とすることにしました。
そこで今回は、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のドキュメントを見ると、デフォルトで返ってきそうなのは id
と text
だけのようでした。
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
のように Cursor
と times
を使っていました。
API v2の場合は next_token
か previous_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 以降のツイートはあり、かつ、一度のリクエストで取得しきれない場合
data
にTweetオブジェクトがあり、かつ、 meta
に next_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では
- データがなければ create 、データがあれば get
- データがなければ create 、データがあれば update
ができるので、実装を変更しました。
なお
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