Twitter」タグアーカイブ

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

前提

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

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

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

概要

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

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

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

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

今回は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

[Python3] GithubAPI/TwitterAPIを用いて、最新のコミットログをツイートする

前提

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

要素 バージョン
debian 8.6
python 3.6.2

概要

GithubAPIを用いて、自身がGithub上のリモートリポジトリに対してpushした内容からコミットログを取得し、それに関する情報をTwitterAPIを用いてツイートしたお話。

本記事ではTwitterAPIを利用するためのコンシューマキー、アクセストークンは取得済みであることを前提としている。

同じことはGithubのWebhookを使ってできるしむしろ自然だが、その場合サーバーを用意する必要があるので、今回はあえてpull型で非効率なやり方を採用

動作イメージ

リモートリポジトリがGithub上にあるリポジトリに対して適当にコミット/プッシュ

$ git commit -m "コミットテスト"
[master e6bb9e4] コミットテスト
 1 file changed, 1 insertion(+)
$ git push origin master
Counting objects: 3, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (3/3), done.

今回実装したコマンドを実行する

$ python main.py

以下のように、リポジトリ名、コミットメッセージ、diffへのリンクがツイートされる

GithubAPIについて

GithubAPIは、名前の通りGithubの各種読み書きを行うためのAPI。
公開されているデータの取得に関しては認証無しで手軽に利用できる。

例えば、curlコマンドを用いて以下のURLに対してGETリクエストを送信すると

curl -i https://api.github.com/users/sa2knight/events

以下のように、該当ユーザのGithubでのイベントログを取得することができる

[
  {
    "id": "6516862394",
    "type": "PushEvent",
    "actor": {
      "id": 16274215,
      "login": "Sa2Knight",
      "display_login": "Sa2Knight",
      "gravatar_id": "",
      "url": "https://api.github.com/users/Sa2Knight",
      "avatar_url": "https://avatars.githubusercontent.com/u/16274215?"
    },
    "repo": {
      "id": 96615971,
      "name": "Sa2Knight/degulog2",
      "url": "https://api.github.com/repos/Sa2Knight/degulog2"
    },
    "payload": {
      "push_id": 1946817475,
      "size": 1,
      "distinct_size": 1,
      "ref": "refs/heads/master",
      "head": "4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29",
      "before": "afd8b8647c2b810ceb731a74bad20c7388c19974",
      "commits": [
        {
          "sha": "4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29",
          "author": {
            "email": "shingo.sasaki.0529@gmail.com",
            "name": "shingo sasaki"
          },
          "message": "バックアップ追加",
          "distinct": true,
          "url": "https://api.github.com/repos/Sa2Knight/degulog2/commits/4208370eb0cda0a1bab02fd3fbe5bf7e3e6f5f29"
        }
      ]
    },
    "public": true,
    "created_at": "2017-08-27T15:22:08Z"
  },
  {
    "id": "6516846556",
    "type": "PushEvent",
(以下省略)

ここで取得できるイベントは、Webで言う以下のような、そのユーザのリポジトリに対する操作全般の情報なので、今回はこのAPIを用いることにする

ライブラリの導入

今回はPython3を用いて実装するので、Pythonのパッケージ管理ツールであるpipを用いて、以下の二種類のライブラリを導入する。

  • requests
    — HTTPライブラリ
  • twitter
    — TwitterAPI用

TwitterAPIの利用には認証も絡んでくるので、専用のライブラリを利用する。GithubAPIは特定URLにGETするだけなのでHTTPライブラリで直接実行することに。

$ pip install requests
$ pip install twitter

ソースコード

最新版はこちら

import twitter
import requests
import json
import os

FILE_NAME      = "last_id"
EVENTS_URL     = "https://api.github.com/users/sa2knight/events"
REPOSITORY_URL = "https://api.github.com/repos"

def tweet(text):
  auth = twitter.OAuth(consumer_key=os.environ['TWITTER_CONSUMER_KEY'],
                       consumer_secret=os.environ['TWITTER_CONSUMER_SECRET'],
                       token=os.environ['TWITTER_ACCESS_TOKEN'],
                       token_secret=os.environ['TWITTER_ACCESS_SECRET'])
  t = twitter.Twitter(auth=auth)
  t.statuses.update(status=text)

def tweet_event(event):
  tweet_text = f"""
  @null
  Githubにコミットをプッシュしました。
  [{event['repository']}]
  「{event['commits'][0]['message']}」
  """.strip()
  if 1 < len(event['commits']):
    tweet_text += f"ほか{len(event['commits']) - 1}件"
  tweet_text += f"\n\n{event['commits'][0]['url']}"
  tweet(tweet_text)

def save_id(id):
  with open(FILE_NAME, 'w') as f:
    f.write(id)

def load_id():
  if os.path.exists(FILE_NAME):
    with open(FILE_NAME, 'r') as f:
      return f.readline()
  else:
    return ''

def is_new_id(id):
  return id != load_id()

def parse_commit_log(repo_name, commit):
  return {
    'url': f"https://github.com/{repo_name}/commit/{commit['sha']}",
    'message': commit['message']
  }

def get_repository_description(repo_name):
  url = f"{REPOSITORY_URL}/{repo_name}"
  response = requests.get(url)
  repository = json.loads(response.text)
  return repository['description']

def get_recent_push_event():
  response = requests.get(EVENTS_URL)
  events   = json.loads(response.text)
  recent_event = list(filter(lambda e: e['type'] == 'PushEvent', events))[0]
  repo_name    = recent_event['repo']['name']
  commits      = list(map(lambda c: parse_commit_log(repo_name, c), recent_event['payload']['commits']))
  return {
    'id':         recent_event['id'],
    'repository': get_repository_description(recent_event['repo']['name']),
    'commits':    commits,
  }

event = get_recent_push_event()
if is_new_id(event['id']):
  tweet_event(event)
  save_id(event['id'])
  print('ツイートを投稿しました')
else:
  print('ツイートは不要です')

スクリプトを実行すると概ね以下の手順でツイートされる
1. GithubAPIを叩いて、自身の最新のコミットを含んだイベントログを取得
2. 直近で取得したイベントIDをファイルに保存しておき、そのIDと比較、一致していた場合変更なしとして終了
3. イベントログを元にツイートするテキストを生成
4. TwitterAPIを用いてツイート

所感

  • 本スクリプトを15分おきぐらいにcronで実行すれば、概ねコミット内容がツイートされる
  • Webhookと違ってpull型になるので、リアルタイムにはできない
  • 単にGithubAPIが使いたくて遊んだだけで実用性はあんまり無い