[Ruby] Twitterから画像を自動収集する

概要

Twitter上に特定ワードと共に添付された画像ファイルを、任意の枚数自動でダウンロードをするCUIツールをRubyで実装したお話。
画像収集方法として他に以下を検討したが、それぞれ以下の理由で見送った。

  • Bing Image Search API
    — これまで画像収集でお世話になっていたAPIだが、Azureに統合(?)され、実質有料化したので見送り
  • Instagram API
    — イケると思って調べたが、どうやら他のユーザの投稿写真を取得するためには承認を得る必要がある模様
  • Tumblr API
    — Tumblrがよくわからなかったため
  • Google画像検索などのクローリング
    — Pythonでクローリングしようとも検討したが、あまり美しく無いと思ったので見送り

他にもいくつか調査したが、最終的に普段から利用しているTwitterAPIを使うことにした。TwitterのSearchAPIを用いると、ツイートに添付されている画像に関してもまとめて取得できるようになっているので、比較的簡単にできると踏んだため。

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
ruby 2.4.1
gem 2.6.11
twitter_oauth 0.4.94

また、記事内で必要となるTwitterAPI用のトークンは既に取得済みであることを前提とする

ライブラリのインストール

今回はRubyによるCUIツールの形式で実装する。RubyからTwitterAPIを叩くために、普段から利用しているGemライブラリであるtwitter_oauthを今回も使用する。

以下のコマンドを叩くか、あるいは同様のGemfileを作成する。

$ gem install twitter_oauth

TwitterAPI認証

Twitterクラスを実装し、TwitterAPI認証を行うauthメソッドを実装する。API認証をコンストラクタにしなかったのは、インスタンス生成のタイミングで認証が行われるのが気持ち悪いと個人的に感じたため。

authメソッドでは、前項でインストールしたtwitter_oauthモジュールに含まれるClientクラスのインスタンスを生成する。その際にトークンが必要になるので、今回は環境変数から読み込むようにした。

require 'twitter_oauth'
require 'net/http'
require 'uri'
class Twitter
  #
  # TwitterAPIの認証を行う
  #
  def auth
    @twitter = TwitterOAuth::Client.new(
      :consumer_key    => ENV['TWITTER_API_KEY'],
      :consumer_secret => ENV['TWITTER_API_SECRET'],
    )
    puts "Twitter APIの認証完了"
  end
end

検索ワードに合致する写真一覧を取得

特定ワードでツイートを検索し、その中に画像ファイルが添付されている場合にはURLを配列に追加。画像ファイルの総数が指定した枚数に達するまで、再帰的にツイートの検索を繰り返す。という一連の処理を行うsearch_picturesメソッドを実装する。

また、以下ページを参考に、TwitterAPIのレスポンスから添付画像のURLを取得するextract_pictures_from_tweetsメソッドを実装し、ツイート取得ごとに実行する。
GET search/tweets – ツイートを検索する

@@SLEEP_TIMEはAPI呼び出し毎に挟む待機時間。APIは時間あたりの利用回数が決まっているので、それに引っかからないように調整する。

#
# 検索ワードに合致する写真一覧を取得
#
def search_pictures(word, num = 10, opt = {})
  @twitter or self.auth
  params = {
    lang:        'ja',
    locale:      'ja',
    result_type: 'mixed',
    count:       200,
  }.merge(opt)
  puts "画像検索中(残り#{num}枚)"

  tweets = @twitter.search(word, params)['statuses']
  max_id = tweets[-1]['id']
  pictures = extract_pictures_from_tweets(tweets)

  if num <= pictures.count
    return pictures.take(num)
  else
    sleep @@SLEEP_TIME
    return pictures.concat self.search_pictures(word, num - pictures.count, max_id: max_id)
  end
end
#
# TwitterAPIで取得したツイート一覧からmedia情報を抜き取る
#
def extract_pictures_from_tweets(tweets)
  pictures = tweets.map do |t|
    if media = t['entities']['media']
      media.map {|m| m['media_url']}
    else
      []
    end
  end
  pictures.flatten.uniq
end

画像のダウンロード

前項のsearch_picturesメソッドを用いて取得した画像URL一覧を元に、指定したディレクトリにダウンロードするためのdownload_picturesメソッドを以下のように実装する。

こちらは単純に、個々の画像URLに対してNet::HTTPを用いてダウンロードしている。こちらもマナーとして、ダウンロード1件毎に@@SLEEP_TIME秒待機する。


#
# ツイッター上の画像をまとめてダウンロードする
#
def download_pictures(word, download_dir, num = 10)
  pictures = self.search_pictures(word, num)
  pictures.each_with_index do |picture, idx|
    filename = File.basename(picture)
    filepath = "#{download_dir}/#{filename}"
    open(filepath, 'wb') do |file|
      puts "downloading(#{idx + 1}/#{pictures.count}): #{picture}"
      file.puts(Net::HTTP.get_response(URI.parse(picture)).body)
    end
    sleep @@SLEEP_TIME
  end
end

動作確認

実行方法

irbなんかを起動して、

Twitter.new.download_pictures('#デグー', '/share/images', 20)

と実行することで、”#デグー”が含まれたツイートに含まれている画像20件をダウンロードする。もちろん’#デグー’が含まれたツイートに画像が添付されてない場合もあるので、写真数が20に達するまで再帰的にツイートを検索し続ける。

デモ

ソースコード

今回は諸事情でGithubにリポジトリを上げていないので、以下に全ソースコードを掲載する

require 'twitter_oauth'
require 'net/http'
require 'uri'

class Twitter2

  @@SLEEP_TIME = 1

  #
  # TwitterAPIの認証を行う
  #
  def auth
    @twitter = TwitterOAuth::Client.new(
      :consumer_key    => ENV['TWITTER_API_KEY'],
      :consumer_secret => ENV['TWITTER_API_SECRET'],
    )
    puts "Twitter APIの認証完了"
  end

  #
  # 検索ワードに合致するツイート一覧を取得
  #
  def search_pictures(word, num = 10, opt = {})
    @twitter or self.auth
    params = {
      lang:        'ja',
      locale:      'ja',
      result_type: 'mixed',
      count:       200,
    }.merge(opt)
    puts "画像検索中(残り#{num}枚)"

    tweets = @twitter.search(word, params)['statuses']
    max_id = tweets[-1]['id']
    pictures = extract_pictures_from_tweets(tweets)

    if num <= pictures.count
      return pictures.take(num)
    else
      sleep @@SLEEP_TIME
      return pictures.concat self.search_pictures(word, num - pictures.count, max_id: max_id)
    end
  end

  #
  # ツイッター上の画像をまとめてダウンロードする
  #
  def download_pictures(word, download_dir, num = 10)
    pictures = self.search_pictures(word, num)
    pictures.each_with_index do |picture, idx|
      filename = File.basename(picture)
      filepath = "#{download_dir}/#{filename}"
      open(filepath, 'wb') do |file|
        puts "downloading(#{idx + 1}/#{pictures.count}): #{picture}"
        file.puts(Net::HTTP.get_response(URI.parse(picture)).body)
      end
      sleep @@SLEEP_TIME
    end
  end

  private

    #
    # TwitterAPIで取得したツイート一覧からmedia情報を抜き取る
    #
    def extract_pictures_from_tweets(tweets)
      pictures = tweets.map do |t|
        if media = t['entities']['media']
          media.map {|m| m['media_url']}
        else
          []
        end
      end
      pictures.flatten.uniq
    end

end

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です