【Node.js】TwitterAPIのRateLimitとcursorに気をつけろ


Node.jsアドベントカレンダー18日目の記事です。

APIの勉強で利用が楽、且つわかりやすいものが作れるのはTwitterか小説家になろうだと思っています。
参考:[JavaScript]なろうのランキングをAPIで一括取得する

現在参画中のプロジェクトではAPIもフロントも両方開発していますが、サービスが本格運用前なことに加えてある程度仕様がゆるい状態での開発のため、APIリミットがキツいイメージがあったTwitterを触ってみることにしました。

Node.jsのTwitterAPIを使ってみよう系エントリーについてはAPI利用申請の部分に偏った物が多かった事と、
特徴となるcursorに触れている物が少なかったこともありコード側に寄せて実装時気になった箇所を纏めました。

尚、Twitter API利用開始についてはTwitter APIを使ってみるを参考にしました。

3行で

・非同期処理時の残cursorの扱い(結果参照しPromiseを生成する)
・取得対象に応じて設定値が著しく異なるRateLimitに注意する
・entitiyの罠(screen_nameとUID)

実装

app.js

  /* express関連の処理省略 API内部処理のみ記述 */

  // 検索対象のIDを指定(実際はreqにて取得)
  const userId = "任意のID"

  // 初回カーソルの指定
  let now_cursor = -1

  // API取得情報の保持
  const user = new Array()

  const getList = (nc) => {
    // 引数を受けてPromiseを返すfunctionを生成して返す
    return new Promise(() => {
      client.get("followers/list", { screen_name: userId, cursor: nc })
        .then((res) => {
          user.push(res.users)
          now_cursor = res.next_cursor

          // cursorが0以外であれば次ページを取得、0ならファイル出力
          if (now_cursor > 0) getList(now_cursor)
          else fileOutput(user, 'follow')
        })
        .catch((e) => {
          // レートリミットの場合、15分経過したらリトライする
          if(e.code === 88) setTimeout(getList(now_cursor), 15 * 60* 1000)
        })
    })
  }

  // APIによるフォローリスト取得処理の実行
  getList(now_cursor)

今回はexpressのサーバーをFirebaseにてホスティングしてクライアントサイドから叩く想定のコードのうち、
取得処理のコアになる部分を抜粋しています。requestにてtwitterIdを受け取ったのち、そのidユーザーの
フォローしている人を全て取得する処理となります。

非同期処理時の残cursorの扱い(結果参照しPromiseを生成する)

DM欄・フォロー欄・フォロワー欄・リプライ欄等の頻繁に更新が発生しないが一覧で表示したい項目については、新規ツイート取得と異なり全量取得しようとするとAPIによる取得が複数回発生する可能性があります。TwitterAPIのentityの使用上、連続するデータを取得するためにcursorを用いてページ遷移の如く次ページのデータへ移動し、データを取得していく必要があります。

リセットタイムとRateLimitを考慮してある程度先読みさせておくとAPIサーバーへの頻繁なアクセスを減らす事ができ、負荷分散ができることに加えてRateLimitを無駄にしないメリットがあります。
(5の倍数で処理を実行しデータをストック、cursorを保持して待機させ、細かいAPIアクセスを減らすなど)

上記コードを伝えたい箇所のみを使用して簡略すると以下の内容になります。

  const getList = (nc) => {
    return new Promise(() => { client.get()

        /* client.get()処理が終了するまで待機、成功でthen、失敗でcatchに入る*/

        // 受け取った結果を使用して次のPromiseを生成する ※疑似ループ処理
        .then((res) => if(res.nextcursor !== 0) getList(res.next_cursor) }) 

        // 何かしらのエラーが発生したらエラー出力して処理を終了
        .catch((e) => { console.log(e) return }) // RateLimit:コード88 の可能性が一番高くなる

返却される結果の1要素を用いて次の処理を生成する必要があるため、引数を投げられるPromiseを生成する関数の内部で再帰的に処理させる事によりgetList()を実行するだけでループの上全量取得を行います。(RateLimitは除く)

※なにかしら制御しないとアクセスしまくってすごいことになるので、今回のようなエラーコードが
 返却されるものでなく自作APIを叩く場合は必ず制御を入れるようにしましょう。(エラー返すとか、フラグ制御とか)

少し本論とズレましたが、TwitterAPIのentityでcursorを使って次結果に移動するため保持する必要があることと、APIの制限に柔軟に対応できるよう一定回数の処理を纏めて実行できる処理を作りRateLimitを無駄にしないことを頭に入れておけばTwitterAPIによる開発がしやすくなると思います。

取得対象に応じて設定値が著しく異なるRateLimitに注意する

TwitterAPIのRate Limitの欄を見ると新ツイートの取得等と異なり、比較的短時間での変動が少ないList・follower等の取得については15分に15回とかなり強めな値に設定されています。List等であれば1回のリクエストですべて取りきれますが、特にフォロワー取得については今回のコードでフォロワー(1-200程度)を検証中に数回取得した所、リクエスト回数全てを消費してしまいました。

Twitter公式のフォロワーページでスクロールを行うと、一定距離スクロールをした所で追加取得の実行が確認できます。自己紹介文などbioの長さによりますが4-50userで1リクエスト=cursor1回分みたいな扱いになっている印象を受けました。15count*約50user≒750人以上取得する場合、1回(15分)のRateLimit内では取り切れない可能性があります。

とはいえ更新が少ない箇所のため見せ方とか作り次第でどうにかできるものではありますが、RateLimitが極端に少ない箇所もあるよという所は開発する機能によってはクリティカルな部分となるため認識しておく必要があると思います。

entitiyの罠(screen_nameとUID)

ここに関してはentityの構造をちゃんと見ろという話ではありますがUserIdは各ユーザーに振られた固定の数値によるIDでありTwitterIdとして認識しているものはentityではscreen_nameである、だとか内部名称が自分が認識しているものと異なるので、先にある程度Twitter Developer Documentationのentityを見ておくとラグがなく作業ができるかなと思いました。(他の記事だとあまり触れられていなかったので念の為)

entity構造については参考サイトが日本語かつ公式より見やすかったため、必要そうな箇所を一通り読んでおけば比較的スムーズに実装できるかと思います。Twitter DeveloperのRate Limitと合わせて確認することをお勧めします。

参考:Twitter REST APIの使い方

補足

npmのTwitterモジュールは4年前から更新ストップしているため、Twitterの仕様変更によってはモジュールを使用したリクエストができなくなる可能性があります。(2020年12月現在で使えており生存ログの意味でもこの記事を書いています)
といってもWeeklyDL数は圧倒的だったりサクッと使いやすいため、使えなくなるタイミングについては注視しておく必要がありそうです。

まとめ

・TwitterAPIはcursorのクセを理解して使う
・公式ドキュメント(特にentityとRateLimit)はよく読もう
・はじめからRateLimitを無駄にしない作りを意識しておくと後で困らないかも