さんだすメモ

さメモ

技術ブログでは、ない・・・

Twitterを使い始めた[Ruby]

はじめに

追記: 今は使えない方法です。

Rubyを使って検索しました。明らかに最善じゃないところがありますが、メモってことでお願いします。
扱いやすさのためClass Tsitterを作ります。名前は適当です。

  • initialize(username, password)
  • get_rate_limit_status(resource_list)
  • search_tweet(search_word, count:40)

initialize(username, password)について

インスタンス生成の際にログインしてapiを使える状態にします。

インスタンス変数を初期化

@hash_cookie = {'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0'}
@hash_header = {}

Cookie記録用とリクエストヘッダに使うためのハッシュです。

/loginを見てログインフォームを確認する

# host = 'twitter.com', port = 443
https_twitter = Net::HTTP.new('twitter.com', 443)
https_twitter.use_ssl = true
https_twitter.verify_mode = OpenSSL::SSL::VERIFY_PEER

# リダイレクト先を設定してloginにアクセス
path = '/login'
query = 'hide_message=true&redirect_after_login=https%3A%2F%2Ftweetdeck.twitter.com%2F%3Fvia_twitter_login%3Dtrue'
response_login = https_twitter.start { |https|
  https.get("#{path}?#{query}")
}

# cookieを登録
self.register_cookie(response_login)

# form欄にあるauthenticity_tokenを取得
response_login.body.force_encoding('utf-8').match(/<input type="hidden" value="([^"]*)" name="authenticity_token">/)
authenticity_token = $1

フォームは次のような形になっていて、この中にpostすべき情報が書かれています。

<form action="https://twitter.com/sessions" class="LoginForm js-front-signin" method="post"
  data-component="dialog"
  data-element="login"
>
...
</form>

/sessionsにpost

# formに入力してpost
path = '/sessions'
response_sessions = https_twitter.start { |https|
  request = Net::HTTP::Post.new(path, @hash_header)
  request.set_form_data(
    {
      'session[username_or_email]' => username,
      'session[password]' => password,
      'remember_me' => '0',
      'return_to_ssl' => 'true',
      'redirect_after_login' => 'https://tweetdeck.twitter.com/?via_twitter_login=true',
      'authenticity_token' => authenticity_token
    }
  )
  https.request(request)
}

# cookieを登録
self.register_cookie(response_sessions)

tweetdeckに戻る

# host = 'tweetdeck.twitter.com', port = 443
https_tweetdeck = Net::HTTP.new('tweetdeck.twitter.com', 443)
https_tweetdeck.use_ssl = true
https_tweetdeck.verify_mode = OpenSSL::SSL::VERIFY_PEER

path = '/'
query = 'via_twitter_login=true'
response_tweetdeck = https_tweetdeck.start { |https|
  request = Net::HTTP::Get.new("#{path}?#{query}", @hash_header)
  https.request(request)
}

bundle...jsからbearer_tokenを取得

# host = 'ton.twimg.com', port = 443
https_ton = Net::HTTP.new('ton.twimg.com', 443)
https_ton.use_ssl = true
https_ton.verify_mode = OpenSSL::SSL::VERIFY_PEER

# urlを取得
url_bundle = response_tweetdeck.body.force_encoding('utf-8').match(/<script src=([^<>]*)><\/script>\z/)[1]

uri_bundle = URI.split(url_bundle)
path = uri_bundle[5]
response_bundle = https_ton.start { |https|
  https.get(path)
}

# bearer_tokenを取得
bearer_token = response_bundle.body.force_encoding('utf-8').match(/bearer_token:"([^"]*)"/)[1]

どうも固定らしいのでいちいち調べる必要はないですね。

ヘッダーを編集する

@hash_header['Authorization'] = "Bearer #{bearer_token}"
@hash_header['X-Csrf-Token'] = @hash_cookie['ct0'].split('=')[1]
@hash_header['X-Twitter-Auth-Type'] = 'OAuth2Session'
@hash_header['X-Twitter-Client-Version'] = 'Twitter-TweetDeck-blackbird-chrome/4.0.190220122730 web/'

X-Csrf-TokenCookiect0と同じ値である必要があります。
以上でapiを利用できるようになります。

接続する

# host = 'api.twitter.com', port = 443
@https_api = Net::HTTP.new('api.twitter.com', 443)
@https_api.use_ssl = true
@https_api.verify_mode = OpenSSL::SSL::VERIFY_PEER

get_rate_limit_status(resource_list)について

apiの制限までの残り回数やリセットされる時刻などを得られます。

def get_rate_limit_status(resource_list)
  path = "/1.1/application/rate_limit_status.json"
  query = "resources=#{resource_list.join(',')}"

  response_rate_limit_status = @@https_api.start { |https|
    request = Net::HTTP::Get.new("#{path}?#{query}", @hash_header)
    https.request(request)
  }

  return response_rate_limit_status
end

rate_limit_statusを取得するのにもapi制限があるので使いすぎないよう注意します。'rate_limit_context' => 'resources' => 'search' => '/search/universal'といった具合に辿り、'remaining''reset'などで欲しいパラメータを得られます。

search_tweet(search_word, count:40)について

path = '/1.1/search/universal.json'
query = URI.escape("q=#{search_word}&count=#{count}&modules=status&result_type=recent&pc=false&ui_lang=ja&cards_platform=Web-13&include_entities=1&include_user_entities=1&include_cards=1&send_error_codes=1&tweet_mode=extended&include_ext_alt_text=true&include_reply_count=true")

response_search = @https_api.start { |https|
  request = Net::HTTP::Get.new("#{path}?#{query}", @hash_header)
  https.request(request)
}

return response_search

検索クエリにsince_idmax_idを使うことで検索範囲をツイート単位で指定できます。

応答

JSON形式でハッシュが返ってきます。内容は大体次のような感じで取り出せます。

hash_search_result = JSON.parse(response_search.body.force_encoding('utf-8'))

# modulesに各ツイートの内容が配列で格納されている
modules = hash_search_result['modules'].each { |hash_module|

  full_text = hash_module['status']['data']['full_text']
  screen_name = hash_module['status']['data']['user']['screen_name']
  id_str = hash_module['status']['data']['id_str']
  
  url = "https://twitter.com/#{screen_name}/status/#{id_str}"
  
  # Wed Feb 20 04:09:32 +0000 2019
  # 曜日 月 日 時:分:秒 +0000 年
  hash_module['status']['data']['created_at'].match(/^(...) (...) (..) (..):(..):(..) (\+....) (....)$/)
  time = Time.gm($8, $2, $3, $4, $5, $6, 0).localtime('+09:00')

  # ######### #
  # 適当な処理 #
  # ######### #
}

api制限

レスポンスヘッダにapi制限についての情報も付いてきます。

# 残り回数
response_search_result.get_fields('x-rate-limit-remaining')

# 残り回数がリセットされる時刻
response_search_result.get_fields('x-rate-limit-reset')

エラーなど

2種類ほどエラーが出ました。独立して起こっていた気がします。

  • response_search_resultの内容(つまりhash_search_result)が{ "errors" => ...}のような感じでキーにmodulesがない
  • レスポンスヘッダに'x-rate-limit-remaining''x-rate-limit-reset'がない

最後に

search_tweetは毎回セッションを閉じる形になるので、keep-aliveな通信ではないみたいですね。今回は時間がなかったのでsearch_tweetをたくさん回してしまいました。また機会があれば、負荷をかけない形でやりたいです。