Twitter gemでツイート検索する場合の要点、及び:since_id指定を有効にするモンキーパッチ

Twitter gem を使って検索する場合には注意しないと API 規制を受けてしまうことがあります。この記事では検索する場合の tips 、そして `:since_id` 指定していても API 規制を受けてしまうことを解決するモンキーパッチを公開します。

#API上限
通常の認証：180回/15分（平均：5秒に1回まで）
Application-only auth：450回/15分（平均：2秒に1回まで）

#アクセスクライアントインスタンスの作り方
##通常の認証
```rb
require 'twitter'

client = Twitter::REST::Client.new(
  consumer_key:        'YOUR_CONSUMER_KEY',
  consumer_secret:     'YOUR_CONSUMER_SECRET',
  access_token:        'YOUR_ACCESS_TOKEN',
  access_token_secret: 'YOUR_ACCESS_SECRET',
)
```
##Application-only auth
```rb
require 'twitter'

client = Twitter::REST::Client.new(
  consumer_key:        'YOUR_CONSUMER_KEY',
  consumer_secret:     'YOUR_CONSUMER_SECRET',
)
```
#検索：`Twitter::REST::Search#search(query, options = {})`
##`options`パラメータ：
###標準
- `:geocode`: 地点。`"緯度,経度,距離"`のフォーマットで指定。距離は mi または km 。<br>例：`"35.681382,139.766084,20km"`（東京駅を中心とした半径20km以内。スペースを空けるとダメ）
- `:lang`: 言語。 [ISO 639-1コード](http://ja.wikipedia.org/wiki/ISO_639-1%E3%82%B3%E3%83%BC%E3%83%89%E4%B8%80%E8%A6%A7) で指定。
- `:locale`: 地域。有効なのは `"ja"` のみ。
- `:result_type`: 内容指定。デフォルトは `"mixed"` 。
    - `"recent"`: 最近のツイート。 
    - `"popular"`: ポピュラーなツイート。 
    - `"mixed"`: popular と recent を混ぜたツイート。
- `:count`: 取得数。デフォルトは15。最大は100。
- `:until`: 指定日まで検索。`"YYYY-MM-DD"` のフォーマットで指定。ただし API では1週間までしか取得できず、 **`:until` に1週間以前を指定してもやはり取得出来ない**。
- `:since_id`: 指定 ID 以降から取得（最も古いツイート ID）。`0`または `nil` を指定すれば指定してない状況と同じ。 **残念なことに、検索結果が100件以上ある場合には無効**（検索結果が100件以上でも有効にするモンキーパッチを後記します）。
- `:max_id`: 指定 ID 以前を取得（最も新しいツイート ID）。

###`query`内での設定だがパラメータ指定も可能なもの
- `:from`: 指定アカウントからのツイートを検索。アカウント名（screen_name）を指定する。
- `:to`: 指定アカウントへのツイート（ただしツイート冒頭にある場合のみ）を検索。アカウント名（screen_name）を指定する。
- `:exclude`: `"retweets"` を指定すると、検索結果から公式 RT を排除する。
- `:filter`: `"links"` を指定すると、リンクを含むツイートのみを検索する。

参考：

- [Twitter 検索API メモ - 超自己満足プログラミング](http://d.hatena.ne.jp/favril/20090327/1238138002)
- [Method: Twitter::REST::Search#search — Documentation for twitter (5.14.0)](http://www.rubydoc.info/gems/twitter/Twitter/REST/Search:search)

#検索例
```
query = "#ブラバンツなんたら" # 4/15の夜に *私だけ* が投稿したハッシュタグ。121ツイートあります。
since_id = nil
result_tweets = client.search(query, count: 100, result_type: "recent",  exclude: "retweets", since_id: since_id)
#=> #<Twitter::SearchResults:0x007ffb10d17cd0
     @attrs=
      {:statuses=>
        [{:metadata=>{:iso_language_code=>"ja", :result_type=>"recent"},
          :created_at=>"Wed Apr 15 15:39:04 +0000 2015",
          :id=>588365933179117568,
          :id_str=>"588365933179117568",
          :text=>"今気付いた、3位に汁だったんだw #ブラバンツなんたら",
    （略）
```
#検索結果インスタンス `result_tweets` の取り扱い
##検索結果が少ない（（一週間以内の）検索可能範囲全体で100件未満の）場合（1回で全ての結果が取得できる場合）
`result_tweets` そのまま、もしくは `result_tweets.to_a` を対象にしてイテレータを使う。
**検索結果が100件以上ある場合にこの方法を使うと API 規制を受ける場合があります**。（詳細は後記）。
##検索結果の全てが1回で取得出来ていない場合（検索可能範囲全体で検索結果が100件以上）
`result_tweets.take(100)` などで取得数を指定してイテレータを使う。`take(1000)` などと `:count` 指定数以上の数を指定した場合にも、指定数量を上限としたツイート数を取得してくれる。 `take` メソッドの代わりに、引数付き `first` メソッドでも OK 。

##検索結果が多い場合に `result_tweets.to_a` を使うと
取得出来る（1週間以内のツイート） **全て** の検索結果を取得しようとします。そのため、**検索結果が非常に多い**（`:count` を100に設定していた場合、通常で最大18000件以上、Application-only authで最大45000件以上）**場合には、 API を使い切るまでアクセスし、`RateLimited` エラーで終了してしまいます**。しかも規制が掛からないようにと考えて `:since_id` を指定していても、**その `:since_id` が検索2回目以降に含まれる範囲にある場合には `:since_id` 指定は有効になりません**（要注意ポイント）。 ~~これは Twitter gem のバグだと思います。~~
対策としては

- `take` メソッドを使って取得数を決めてしまう
- **後記するモンキーパッチ** を当てて `:since_id` 指定を確実に有効にする

といった方法を用いましょう。

##検索結果の各ツイートインスタンス `Twitter::Tweet` のメソッド例
各ツイートインスタンスを `tw` とします。

- ID ： `tw.id`
- ツイート本文： `tw.full_text`
- アカウント名： `tw.user.name`
- スクリーンネーム： `tw.user.screen_name`
- ツイート数　： `tw.user.statuses_count`
- フォロー数　： `tw.user.friends_count`
- フォロワー数： `tw.user.followers_count`
- リツイート数： `tw.retweet_count`
- お気に入り数： `tw.favorite_count`
- 添付画像URL： `tw.media.first.expanded_url.to_s`
- リンク先URL： `tw.urls.map { |t| t.expanded_url.to_s }`<br>（リンク先を複数有する場合もあるので）

###`take`メソッドで取得数を指定してからイテレータを使う例
```
result_tweets.take(100).each_with_index do |tw, i| 
  puts "#{i}: @#{tw.user.screen_name}: #{tw.full_text}" 
end
#=> 0: @riocampos: 今気付いた、3位に汁だったんだw #ブラバンツなんたら
    1: @riocampos: パテルスキーもトップ10に入ったのか #ブラバンツなんたら
     ：
    98: @riocampos: ルームポットのホーニヒ #ブラバンツなんたら
    99: @riocampos: あたっこ。ルームポット？ #ブラバンツなんたら
```
100件取得されました。

###取得数を指定せず検索結果インスタンスに対して直接イテレータを使う例
```
result_tweets.each_with_index do |tw, i| 
  puts "#{i}: @#{tw.user.screen_name}: #{tw.full_text}" 
end
#=> 0: @riocampos: 今気付いた、3位に汁だったんだw #ブラバンツなんたら
    1: @riocampos: パテルスキーもトップ10に入ったのか #ブラバンツなんたら
     ：
    119: @riocampos: sporzaで #ブラバンツなんたら きた\o/
    120: @riocampos: #ブラバンツなんたら かなぁ
```
全件121件取得されました。

#`:since_id`指定を有効にするモンキーパッチ
```rb
module Twitter
  class SearchResults
    def next_page
      return nil unless next_page?
      hash = query_string_to_hash(@attrs[:search_metadata][:next_results])
      since_id = @attrs[:search_metadata][:since_id]
      hash[:since_id] = since_id unless since_id.zero?
      hash
    end
  end
end
```
このモンキーパッチを使うと、 API へのアクセスが2回以上にわたる場合にも `:since_id` が有効になります（ https://github.com/sferik/twitter/pull/682 でプルリクエスト提出済み）。

#参考
本件に関してより詳しい内容はこちらに書いています。要点だけを Qiita に引用しました。

- [Twitter gemを使って検索するときのtips - 別館 子子子子子子（ねこのここねこ）](http://d.hatena.ne.jp/riocampos+tech/20150421/twitter_gem_search_tips)