機略戦記

Maneuver warfare

Twitter APIを制限目一杯まで実行したい

結論

  • あるAPIを残り何回叩けるか、いつ制限がリセットされるかなどの情報を取得できるTwitter APIがあるので、そいつから取得した情報を元に制限一杯まで実行すれば良い。

このAPI:

dev.twitter.com

背景

やりたい事

  • あるTwitterアカウントをフォローしているユーザーその他にどんなアカウントをフォローしているのかを調べたい。

最初に試した方法

  • あるTwitterアカウントをフォローしているユーザーを取得したのち、それぞれのユーザーに対してフォローしているアカウントの一覧を取ろうとした。
  • ruby gemのにTwitter APIをいい感じにラップしてくれるやつがあったので、それを使った。

github.com

最初に試した方法の課題点

  • あるTwitterカウントのフォロワー数分APIを叩く事になる。
  • twitter APIには非常に厳しい実行回数制限がある。
  • 実行回数の制限に引っかかると、例外を吐いてプログラムが終了するので全然はかどらない。

参考:

Rate Limits: Chart | Twitter Developers

対策

  • あるAPIを残り何回叩けるか、いつ制限がリセットされるかなどの情報を取得できるTwitter APIがあるので、そいつから取得した情報を元に制限一杯まで実行する。

実装

require 'twitter'

# API制限を表現するクラス
class APILimitStatus
  def initialize(client, target_end_point, target_api)
    @client = client
    @target_end_point = target_end_point
    @target_api = target_api

    setup_or_update_limit_status!
    check_arguments!
  end

  # もし制限に達していたら制限がリセットされるまでスリープする。
  def sleep_to_reset_if_limit!
    setup_or_update_limit_status!

    if remaining <= 0
      sleep(need_seconds_for_reset)
    end
  end

  private

  # 次回制限リセットまでの残り秒数
  def need_seconds_for_reset
    now = Time.now.to_i
    (reset_at - now) > 0 ? (reset_at - now) : 0
  end

  # 残り実行回数リセット時刻
  def reset_at
    @limit_status[@target_end_point][@target_api][:reset]
  end

  # 残り実行可能回数
  def remaining
    @limit_status[@target_end_point][@target_api][:remaining]
  end

  # limit_statusをupdateする。なければsetupする。
  def setup_or_update_limit_status!
    limit_status = @client.__send__(:perform_get, '/1.1/application/rate_limit_status.json')
    @limit_status = limit_status[:resources]
  end

  # 制限を調べたいAPIの指定が間違っていたら雑な例外を出す。
  def check_arguments!
    raise if @limit_status[@target_end_point].nil?
    raise if @limit_status[@target_end_point][@target_api].nil?
  end
end

# 設定
client = Twitter::REST::Client.new do |config|
  config.consumer_key        = ""
  config.consumer_secret     = ""
  config.access_token        = ""
  config.access_token_secret = ""
end

SURVEY_USER_ID = "hoge"

# あるidをフォローしているユーザーの一覧
follower_api_limit = APILimitStatus.new(client, :followers, :"/followers/ids")
follower_api_limit.sleep_to_reset_if_limit!

follower_ids = client.follower_ids(user_id: SURVEY_USER_ID).attrs[:ids]

# ランダムな順番に並び替える。
# これなら一定数のfollower分のfriendが取得できた時点でサンプリングされたデータとして扱える
follower_ids.shuffle!

# あるidをフォローしているユーザーがフォローしているユーザーの取得
friend_api_limit = APILimitStatus.new(client, :friends, :"/friends/ids")

follower_ids.each do |follower_id|
  friend_api_limit.sleep_to_reset_if_limit!

  begin
    friend_ids = client.friend_ids(user_id: follower_id).attrs[:ids]
    puts "#{follower_id},#{friend_ids.to_s}"

  rescue Twitter::Error => e # 鍵アカとかも例外になるので。
    puts "#{follower_id},#{e.to_s}"
  end
end