susunshunのお粗末な記録

お粗末に丁寧に生きる

twitterでtodo管理アプリを作る その2 ー twitter gem / ruby

前回記事でアプリの構想について書きました。susunshun.hatenablog.com

今回作るもの

  • twitter api を使って特定のハッシュタグが付いてるツイートをタイムラインから随時取得するバッチ
  • 取得したツイートはDBに格納

REST apiとstreaming api

rubytwitterのタイムラインを抽出するアプリを作る上で、REST api(twitter gem)とstreaming api(tweetstream gem)、二つの選択肢がありましたが今回はREST apiを選びました。

REST api

能動的にタイムラインからツイートを取得する必要がある

”必要がある”というよりはこの形式がAPIでは一般的ですが、

  1. Twiterにhttp接続
  2. クライアントからリクエストを送信
  3. 送信されてきたリクエストに対してTwitterからレスポンスを返却(同じリクエストを送れば同じレスポンスが帰ってくる)
  4. 接続クローズ

という順でツイートを取得します。
なので継続的にツイートを取得した場合は繰り返し上記処理を実行しなければいけないわけです。

APIの利用制限は180回/15分(平均:5秒に1回まで)

接続方式や取得する情報の種類にもよりますが、APIの利用には上限があります。
とはいえ通常の使用ならば全然問題ないくらいの制限です。

streaming api

タイムラインをプッシュで受け取ることができる

どういうことかと言うと、

  1. Twiterにhttp接続
  2. クライアントからリクエストを送信
  3. Twitterからレスポンスを返却
  4. Twitterからレスポンスを返却
  5. Twitterからレスポンスを返却
  6. Twitterからレスポンスを返却
  7. Twitterからレスp (ry

つまり接続を切るまでは延々とレスポンスが送られ続けます。
具体的にはとあるキーワードでタイムラインを検索すると、そのキーワードを含むツイートが行われるたびにレスポンスでそのツイートの内容が送信されてきます。

制限事項

streaming apiはRESTfulではないので”180回/15分”のような制限はありませんが、

  • 1つのアカウントからの同時接続数は1つ
  • 接続が切断されることもある(なので再接続する仕組みを持つことが前提となります)
  • パブリックなタイムラインから取得できるツイートは全ツイートの1%(ユーザー指定の場合はちゃんと全ツイート取得できます)

等々、万能ではありません。

今回はパブリックなタイムラインから特定ハッシュタグが付いているツイートを漏れなく抽出したいので消去方でstreaming apiは無しです。REST apiを繰り返し実行して漏れなくツイートを取得する手法をとります。

twitter gem

rubyではREST apiを利用するためのgem、twitter gem がありますのでそれを導入します。

実はこの時点でほとんどrubyを書いたことがない。gemもよくわかってないw

今回インストールしたのは最新バージョン(5.14.0)。

$ gem list twitter

*** LOCAL GEMS ***

twitter (5.14.0)

いざ、実装

実装にあたり、この記事が大変参考になりました。qiita.com

ただこの記事は使用しているtwitter gemのバージョンが4系なので5系では動きません。
そこは最新のリファレンスを参照して手直し。
File: README — Documentation for twitter (5.14.0)

結果、以下の様になりました。

#encoding:UTF-8
require "twitter"
require 'active_record'
require 'time'

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

ActiveRecord::Base.establish_connection(
  "adapter" => "sqlite3",
  "database" => "./tweet.db"
)

# ActiveRecordのログを標準出力
# ActiveRecord::Base.logger =Logger.new(STDOUT)

class Tweet < ActiveRecord::Base
end

since_id = 0

# 検索キーワード(static)
KEYWORD = "艦これ"

# 無限ループ
loop do
    # 60秒待機
    sleep(5)
    begin
      # KEYWORDをハッシュタグに含むツイートを取得
      # "-rt"で引用リツイートは除外
      # 前回取得した最後のツイートのid以降のツイートから取得
      client.search("##{KEYWORD} -rt",:count =>2, :result_type => "recent", :since_id => since_id).attrs[:statuses].reverse.each do |tweet|
        
        # ユーザ名、Tweet本文、投稿日を1件づつ表示
        p tweet[:user][:name]
        p tweet[:text]
        p Time.parse(tweet[:created_at]).getlocal # 取得するツイートは時差があるので日本の標準時刻に修正
        
        # DBへTweetを登録
        Tweet.create(:user => tweet[:user][:name] , :body => tweet[:text])

        p "===================="

        # 取得した最後のTweet idを保存
        since_id=tweet[:id]
      end

    # エラーが発生した場合は5秒待ってからリトライ
    rescue Twitter::Error::ClientError
      sleep(5)
      retry
  end
end

参考までにテーブルスキーマ

sqlite> .schema tweets
CREATE TABLE tweets(
 id integer primary key,
 user text,
 body text,
 created_at,
 updated_at
);

実行結果

$ ruby getTweetnew.rb
"モノ★モノ.jp 相互フォロー100%"
"#艦隊これくしょん #艦これ 島風\n#コスプレ 6点セット \n[楽天] http://t.co/qkHULFscKu\n\n #RakutenIchiba http://t.co/iq0P5fuZjX"
2015-05-14 01:14:53 +0900
"===================="
"Kazuhiro Wakatsuki"
"ローマを求めてE-6甲に出撃すること2回。どちらもボス前で撤退。攻略中に使わなかった艦娘を多数投入したのが原因と思われる。一度攻略時の編成に戻して、少しずつ入れ替えするしかなかろう。 #艦これ"
2015-05-14 01:15:03 +0900
"===================="
(以下省略)

DBにも無事格納されています。

sqlite> select * from tweets;
1|モノ★モノ.jp 相互フォロー100%|#艦隊これくしょん #艦これ 島風
#コスプレ 6点セット 
[楽天] http://t.co/qkHULFscKu

 #RakutenIchiba http://t.co/iq0P5fuZjX|2015-05-13 16:15:25.727867|2015-05-13 16:15:25.727867
2|Kazuhiro Wakatsuki|ローマを求めてE-6甲に出撃すること2回。どちらもボス前で撤退。攻略中に使わなかった艦娘を多数投入したのが原因と思われる。一度攻略時の編成に戻して、少しずつ入れ替えするしかなかろう。 #艦これ|2015-05-13 16:15:25.734683|2015-05-13 16:15:25.734683

twitter gemは以外とリファレンスが少なく結構手こずりましたが、雛形はこんな感じで!

5系は特に情報が確立していないですし、since_id周りで不具合があったので、twitter gemではなくOAuth認証を使うという選択肢もあるかもしれませんね。(もちろん使いこなせればtwitter gemの方が便利ですが!)

qiita.com