追記:この技術を使って、コミュニティ解析をするサービスをリリースしました。
Qiita「Twitter API (Ruby)」の続きです。
年末から僕は、TwitterAPIでユーザー情報とフォロー関係を取得し、友人を自動でコミュニティ分類するサービスを作っています。その途中で得られた知見をまとめます。
##目次
- Devise連携 :ログインユーザーがTwitterAPIを使えるように。
- データ取得 :TwitterAPIから大量のデータを取るサンプル。重いので非同期処理。
- Visualize :得たソーシャルグラフを可視化してみる(綺麗)
- 高速化(DB) :redis、それは危険なほどのスピード
- 高速化(並列処理) :並列処理でAPIアクセスがこんなに速く!
#1.Devise連携
認証用Gem, Deviseとの連携です。Qiita「deviseでfacebook,twitter認証」の続き。
##1-1.tokenを取得
下記のようにコールバックからtokenを取得すれば、そのままTwitterAPIに利用することができます。今回はsessionに保存します。
OAuth認証の説明は省略しますが、これによりログインユーザーがTwitterAPIを使えるようになり、フォローしている鍵アカウントの情報なども取れるようになります。(token認証なしではAPI利用が制限されます)
def twitter
@user = User.find_for_twitter_oauth(request.env["omniauth.auth"], current_user)
session[:token] = request.env["omniauth.auth"]["credentials"]["token"]
session[:secret] = request.env["omniauth.auth"]["credentials"]["secret"]
.
.
.
##1-2.clientを作成
この際、TwitterAPIを叩くためのclientを作成するメソッドを定義すると便利です。
class Client
def self.make(token, secret)
Twitter::Client.new(
:oauth_token => token,
:oauth_token_secret => secret
)
end
end
Qiita「Twitter API (Ruby)」の最後にて、config/initializer/twitter.rb
に自分のアカウントでログインするためのtoken,token_secretも記述していましたが、今回はユーザーにログインさせるので記述は不要です。
Twitter.configure do |config|
#この2つのみで良い
config.consumer_key = ""
config.consumer_secret = ""
end
##1-3.データ取得例
実際にcontrollerからclientを作成します。データを取るところまでの簡単な例を書きますが、APIエラーを見越して例外取得もしています。 (重要!)
def get_data
return unless user_signed_in?
begin
@client = Client.make(session[:token], session[:secret])
data = @client.current_user #なんか取ってきます
render :json => data
rescue => exc
render :json => exc
end
end
#2.データ取得
例として、ログインユーザーから1歩分のユーザー情報と、2歩分までのフォロー情報(友人の友人関係)を取ることを考えます。
##2-1.コード
- データベースに格納する部分のコードは、「4.高速化」にて記述します
- TwitterAPI ver1.0では350requests/hour しかできないので注意する必要があります。
- 他にもAPIは様々なエラーを吐くので、その都度対処する必要があります。
- コードは略しますが、もしwhitelistアカウント(API制限20000回)を持っているなら、基本はwhitelistユーザーで情報を取得し、エラーが吐かれたprotectedユーザーの情報はログインユーザーで情報を取得すると効率が良いです。
class TwitterGetData
def initialize client
@client = client
@id = client.current_user.id
end
def run #1歩分のユーザー情報と、2歩分のフォロー情報をDBに格納するメソッド
fetchMyData
fetchFriendsData
end
def fetchMyData
fetchAndSaveInfo @id
fetchAndSaveEdges @id
end
def fetchFriendsData
friends = getEdgesFromDB @id
friends.each do |id|
fetchAndSaveInfo id
fetchAndSaveEdges id
end
end
def fetchAndSaveInfo id
info = fetchInfo(id)
saveInfo(id, info) if info
true
end
def fetchAndSaveEdges id
edges = fetchEdges(id)
saveEdges(id, edges) if edges and edges.size > 0
true
end
def fetchInfo id
id = id.to_i
begin
info = @client.user(id)
rescue => exc
case exc.class.to_s
when Twitter::Error::NotFound.to_s #そのユーザーが居ない場合
delNode id
return false
else
raise exc.to_s + id.to_s
end
end
info
end
def fetchEdges id
id = id.to_i
begin
my_follows = @client.friend_ids(id).ids #5000人まで。それ以上取りたい時は:next_cursorを引数にとる必要あり。
rescue => exc
case exc.class.to_s
when Twitter::Error::NotFound.to_s #そのユーザーが居ない場合
delNode id
return false
else
raise exc.to_s + id.to_s
end
end
my_follows
end
def delNode id #後述、DBからデータを消去
end
def saveInfo id, info #後述、ユーザー情報を格納
end
def saveEdges id, edges #後述、ユーザーのフォロー情報を格納
end
def getEdgesFromDB id #後述、DBからフォロー情報を取得
end
end
##2-2.実行
@client = Client.make(session[:token], session[:secret])
TwitterGetData.new(@client).run
でデータ収集を開始します。(めちゃくちゃ時間がかかります)
##2-3.非同期処理
データ収集中は30分サーバーが応答しないなどの状態に陥るので、delayed_jobや、resqueなどの非同期処理ができるgemを使うと良いでしょう。
今回はresqueを使いました。redis(key-valueなNoSQL)をインストールして起動し、resque workerを立ち上げる必要があります。詳細は公式サイトで。
gem 'resque', require: 'resque/server'
gem 'redis'
require 'resque/tasks'
task "resque:setup" => :environment
Resque.redis = 'localhost:6379'
で設定し、
class RunProcessor
@queue = :fetch_twitter
def self.perform(session)
begin
p "background running"
client = Client.make(session["token"], session["secret"])
t = TwitterGetData.new client
t.run()
rescue => exc
p exc
end
end
end
def get_data
.
Resque.enqueue(RunProcessor, session)
.
end
でキューにジョブを投げます。
#3.Visualize
取ってきた枝情報を以下の形式で整形すると、フリーソフトgephiにて可視化することができます。また、クラスター係数など、複雑ネットワークにおける様々な情報を得ることができます。
Tom Jerry
Chage Asuka
.
.
詳細は省きますが、以下のように可視化できます。
#4.高速化(DB)
はじめはRDBMSにデータを格納していたのですが、800人フォローしている僕のアカウントを起点にとるだけで50万の枝情報となり、DBへの保存がネックになりました。
Resqueで導入したredisを使えばデータ構造を保存できるため、APIで取ってきた隣接リストやハッシュがそのまま入り、高速化が図れます。
$redis = Redis.new()
$redis.ping()
以下追記。
def info_key str
str.to_s + "_info"
end
def edges_key str
str.to_s + "_edges"
end
def delNode id
$redis.del info_key(id)
$redis.del edges_key(id)
end
def saveInfo id, info
key = info_key(id)
info = info.to_hash
$redis.del key
$redis.mapped_hmset key, info
end
def saveEdges id, edges
key = edges_key(id)
edges = edges.to_a
$redis.del key
$redis.sadd key, edges
end
def getInfoFromDB id
$redis.hgetall info_key(id)
end
def getEdgesFromDB id
$redis.smembers edges_key(id)
end
用途によってはNeo4jやFlockDBなど、GraphDBの利用も検討の余地があります。
#5.高速化(並列処理)
パフォーマンスを計測してみたところ、TwitterAPIに17分、redis格納に3.7秒という結果になりました。APIの待ち時間が長そうなので、並列処理を試してみます。
gem 'parallel'
def fetchFriendsData
friends = getEdgesFromDB @id
#friends.each do |id|
Parallel.each(friends, :in_threads=> friends.size * 2) do |id|
fetchAndSaveInfo id
fetchAndSaveEdges id
end
end
**17分の処理が33秒になりました。**流石にびっくりです!!!
#終わりに
bot作成にクライアントやサービス作成、色々と遊べるTwitterAPI。
アクセスの遅さやAPI制限など中々手のかかる子ですが、その分かわいく思えてきます。あなたもお試しあれ。