TwitterAPI  devise連携/グラフ可視化/データの効率的格納/API高速化

**追記：[この技術を使って、コミュニティ解析をするサービスをリリースしました。](http://qiita.com/items/abb6b7969247cf747b29)**

[Qiita「Twitter API (Ruby)」](http://qiita.com/items/8ab1093417d5976237fc)の続きです。

年末から僕は、TwitterAPIでユーザー情報とフォロー関係を取得し、友人を自動でコミュニティ分類するサービスを作っています。その途中で得られた知見をまとめます。

##目次

1. Devise連携	：ログインユーザーがTwitterAPIを使えるように。
2. データ取得	：TwitterAPIから大量のデータを取るサンプル。重いので非同期処理。
3. Visualize	：得たソーシャルグラフを可視化してみる(綺麗)
4. 高速化(DB)　：redis、それは危険なほどのスピード
5. 高速化(並列処理)　：並列処理でAPIアクセスがこんなに速く！


#1.Devise連携
認証用Gem, Deviseとの連携です。[Qiita「deviseでfacebook,twitter認証」](http://qiita.com/items/9f1faa509f4d3653a1b2)の続き。
##1-1.tokenを取得
下記のようにコールバックからtokenを取得すれば、そのままTwitterAPIに利用することができます。今回はsessionに保存します。

OAuth認証の説明は省略しますが、これによりログインユーザーがTwitterAPIを使えるようになり、フォローしている鍵アカウントの情報なども取れるようになります。(token認証なしではAPI利用が制限されます)

```controllers/users/omniauth_callbacks_controller.rb 
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を作成するメソッドを定義すると便利です。

```models/client.rb
class Client
  def self.make(token, secret)
    Twitter::Client.new(
      :oauth_token => token,
      :oauth_token_secret => secret
    )
  end
end
```
[Qiita「Twitter API (Ruby)」](http://qiita.com/items/8ab1093417d5976237fc)の最後にて、```config/initializer/twitter.rb```に自分のアカウントでログインするためのtoken,token_secretも記述していましたが、今回はユーザーにログインさせるので記述は不要です。

```config/initializer/twitter.rb
Twitter.configure do |config|
  #この2つのみで良い
  config.consumer_key = ""
  config.consumer_secret = ""
end
```

##1-3.データ取得例
実際にcontrollerからclientを作成します。データを取るところまでの簡単な例を書きますが、**APIエラーを見越して例外取得もしています。** （重要！）

```controllers/hoge_controller.rb
  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ユーザーの情報はログインユーザーで情報を取得すると効率が良いです。


```mode/twitter_get_data.rb
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を立ち上げる必要があります。詳細は公式サイトで。

* [Resque(Gem)](https://github.com/defunkt/resque)

```Gemfile
gem 'resque', require: 'resque/server'
gem 'redis'
```

```lib/tasks/resque.rake
require 'resque/tasks'

task "resque:setup" => :environment
```
```config/initializer/resque.rb
Resque.redis = 'localhost:6379'
```

で設定し、

```app/workers/run_processor.rb
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
```


```hoge_contoroller.rb
def get_data
.
  Resque.enqueue(RunProcessor, session)
.
end
```
でキューにジョブを投げます。

#3.Visualize
取ってきた枝情報を以下の形式で整形すると、フリーソフト[gephi](http://oss.infoscience.co.jp/gephi/gephi.org/index.html)にて可視化することができます。また、クラスター係数など、複雑ネットワークにおける様々な情報を得ることができます。

```edges.txt
Tom Jerry
Chage Asuka
.
.

```

詳細は省きますが、以下のように可視化できます。

![unko](http://www7411uf.sakura.ne.jp:8080/socialgraph.png)
* [Gephiクイックスタート](http://oss.infoscience.co.jp/gephi/gephi.org/users/quick-start.1.html)


#4.高速化(DB)

はじめはRDBMSにデータを格納していたのですが、800人フォローしている僕のアカウントを起点にとるだけで50万の枝情報となり、DBへの保存がネックになりました。
Resqueで導入したredisを使えばデータ構造を保存できるため、APIで取ってきた隣接リストやハッシュがそのまま入り、高速化が図れます。

```config/initializer/redis.rb
$redis = Redis.new()
$redis.ping()
```

以下追記。

```mode/twitter_get_data.rb
  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の利用も検討の余地があります。

* [「GraphDB徹底入門」〜構造や仕組み理解から使いどころ・種々のGraphDBの比較まで幅広く〜](http://www.slideshare.net/doryokujin/graphdbgraphdb)


#5.高速化(並列処理)
パフォーマンスを計測してみたところ、**TwitterAPIに17分**、**redis格納に3.7秒**という結果になりました。APIの待ち時間が長そうなので、並列処理を試してみます。

* [Parallel(Gem)](https://github.com/grosser/parallel)


```Gemfile
gem 'parallel'
```

```mode/twitter_get_data.rb
  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制限など中々手のかかる子ですが、その分かわいく思えてきます。あなたもお試しあれ。