RTしたイラストを自動で保存したい

こんにちは、CCSの幽霊こと、ねるです。
ノリでブログ開設してみましたが、タイトルで詰みました。誰かいいタイトルください。

この記事は「CCS †裏† Advent Calendar 2017」15日目の記事になります。
(裏らしさはほぼ)ないです。

前日の記事は princecoffee.hatenablog.com

はじめに

タイトル通りこの記事では 僕の本業であるTwitterで流れてきたイラストをひたすらRTして保存する作業を自動化して晴れてニートになろうという話をします。
これでえっちな絵の収集が捗るね(申し訳程度のR18要素)
ぶっちゃけIFTTT使えばよくね?って話なんですが、まあ…

また、当初の予定ではクローリングとかスクレイピングのネタとして書くはずだったんですが、Twitterの規約でクローリングが禁止されているようなので大人しくAPIを利用することにしました。その結果内容が非常に薄くなりましたが、他に書くネタもないので許してください。

【重要】超初心者による記事ですので正確さに欠けると思います。問題、誤り等ありましたら指摘をよろしくお願いします。

目次

環境

標準ライブラリ以外にRequestsTweepyのインストールが必要
tweepy がメンテナンスされなくなった - Qiitaそうなのでリンク先で紹介されている代替品を使うのが良さそうです。ただし、この記事ではTweepyを使っているので注意!
また、Requestsは標準のurllib.requestでも代用可能です。

準備

事前準備としてTwitterにアプリケーションを登録し、APIを利用するための各種Keyを取得する必要があります。
登録はTwitter Application Managementで行います。登録すると以下のKeyが手に入るので覚えておいてください。

API(ここでは特にWeb API)ってなに?って方はこの記事を読むとなんとなく理解できると思います。僕も厳密にはわかってないので丸投げです。

保存までの流れ

  1. TweepyでTwitterAPIの取得
  2. アカウントのツイート数を取得
  3. 前回取得したツイート数と比較しツイートが増えていたら、増えた数分のツイートを取得(初回は最新の1ツイートを取得)
  4. 取得したツイートがRTかどうかを判定
  5. RTであれば添付画像のURLを取得
  6. 画像をダウンロードして設定したフォルダに保存(+ログ出力)

ざっとこんな流れです。
もう少し詳しく説明したいところですが、説明するのも難しい(めんどい)のでとりあえずスクリプトを見てみましょう。

スクリプト

get_rt_image.py

# -*- coding: utf-8 -*-

import datetime
import sys
import json
import os
from time import sleep
import requests
import tweepy_api

class GetRtImage:
    def __init__(self, data_path, key_path):
        self.data  = self.get_data(data_path)
        self.api = self.get_api(key_path)

    # data.jsonの読み込み
    def get_data(self, data_path):
        with open(data_path, mode="r") as fp:
            data = json.load(fp)
        return data

    # APIの取得
    def get_api(self, key_path):
        with open(key_path, mode="r") as fp:
            key = json.load(fp)
        api = tweepy_api.TweepyApi(key).get_api(key_path)
        return api

    # ツイート数の取得
    def get_tweets(self):
        user = self.api.get_user(id=self.data["id"])
        tweets = user.statuses_count
        return tweets

    # TL取得とRTの判別
    def detect_rt(self, num):
        # 引数で与えられた数分のツイートを取得(古い方から取得するためreversed)
        timeline = [self.api.user_timeline(id=self.data["id"], count=num)[i] for i in reversed(range(num))]

        # RTのみのリストを生成
        tl_rt = [tl for tl in timeline if tl.retweeted]
        return tl_rt

    # 画像URLの取得と保存
    def download_image(self, tl_rt):
        for rt in tl_rt:
            # 投稿者名
            screen_name = rt.retweeted_status.author.screen_name
            # 投稿日時(JSTに補正)
            jst = rt.retweeted_status.created_at + datetime.timedelta(hours=9)
            time_str = jst.strftime("%Y-%m-%d-%H%M%S")

            # 画像付きか判定しURLを取得
            if "media" in rt.retweeted_status.entities:
                image_url = [media["media_url_https"] for media in rt.retweeted_status.extended_entities["media"]]

                # 画像の保存
                for index, img_url in enumerate(image_url):
                    # ユーザーごとにディレクトリを作成
                    if not os.path.exists(self.data["path"] + screen_name):
                        os.mkdir(self.data["path"] + screen_name)

                    # ファイル名の設定
                    filename = screen_name + "_" + time_str + "_" + str(index+1) + ".jpg"
                    fullpath = self.data["path"] + screen_name + "/" + filename

                    # 画像のダウンロード
                    res = requests.get(img_url)
                    with open(fullpath, "wb") as fp:
                        fp.write(res.content)
                    sleep(3) # リクエストの間隔をあける

                    # ログ書き込み
                    log_path = os.path.dirname(os.path.abspath(sys.argv[0])) + "/log.txt"
                    with open(log_path, mode="a") as fp:
                        now = datetime.datetime.now()
                        now_str = now.strftime("%Y-%m-%d-%H:%M:%S")
                        text = "Saved " + filename + ". (at " + now_str + ")\n"
                        fp.write(text)


def main():
    data_path = os.path.dirname(os.path.abspath(sys.argv[0])) + "/data.json"
    key_path = os.path.dirname(os.path.abspath(sys.argv[0])) + "/key.json"

    rt_image = GetRtImage(data_path, key_path)
    tweets = rt_image.get_tweets() # 現在のツイート数

    # 初めて取得する際は最新のツイートだけチェック(以降は差分)
    if rt_image.data["tweets"] == 0:
        rt_image.download_image(rt_image.detect_rt(1))
    elif tweets > rt_image.data["tweets"]:
        rt_image.download_image(rt_image.detect_rt(tweets - rt_image.data["tweets"]))

    # ツイート数を記録
    rt_image.data["tweets"] = tweets
    with open(data_path, mode="w") as fp:
        json.dump(rt_image.data, fp)


if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        # エラーログ書き込み
        error_path = os.path.dirname(os.path.abspath(sys.argv[0])) + "/error.txt"
        with open(error_path, mode="a") as fp:
            now = datetime.datetime.now()
            now_str = now.strftime(". (at %Y-%m-%d-%H:%M:%S)\n")
            fp.write("Error: " + str(e) + now_str)

tweepy_api.py

import tweepy
import json
import os
import webbrowser

class TweepyApi:
    def __init__(self, key):
        self.key = key
        self.CONSUMER_KEY = self.key["CONSUMER_KEY"]
        self.CONSUMER_SECRET = self.key["CONSUMER_SECRET"]

        self.code_input = False
        if self.key["ACCESS_TOKEN"] == "" or self.key["ACCESS_SECRET"] == "":
            self.code_input = True
        if not self.code_input:
            self.ACCESS_TOKEN = key["ACCESS_TOKEN"]
            self.ACCESS_SECRET = key["ACCESS_SECRET"]

    def get_api(self, key_path):
        auth = tweepy.OAuthHandler(self.CONSUMER_KEY, self.CONSUMER_SECRET)

        # 認証コードの入力を受け付ける
        if self.code_input:
            redirect_url = auth.get_authorization_url()
            webbrowser.open(redirect_url) # 既定ブラウザで認証ページを開く
            verifier = input('Type the verification code: ').strip()
            auth.get_access_token(verifier)
            self.ACCESS_TOKEN = auth.access_token
            self.ACCESS_SECRET = auth.access_token_secret
            self.key["ACCESS_TOKEN"] = self.ACCESS_TOKEN
            self.key["ACCESS_SECRET"] = self.ACCESS_SECRET

            # キーを保存して次回からは読み込んで使う
            with open(key_path, mode="w") as fp:
                json.dump(self.key, fp)

        auth.set_access_token(self.ACCESS_TOKEN, self.ACCESS_SECRET)
        api = tweepy.API(auth)

        return api

if __name__ == '__main__':
    key_path = os.path.dirname(os.path.abspath(sys.argv[0])) + "/key.json"
    with open(key_path, mode="r") as fp:
        key = json.load(fp)
    api = TweepyApi(key).get_api(key_path)
    api.update_status("tweepyでtweetテスト")
    print('ツイート成功')

data.json

{"id": "ここにTwitterアカウントのID", "tweets": 0, "path": "ここに画像を保存したいパス"}

key.json

{"CONSUMER_KEY": "取得した", "CONSUMER_SECRET": "API キーを", "ACCESS_TOKEN": "それぞれ", "ACCESS_SECRET": "ここに入力"}

ACCESS_TOKENとACCESS_SECRETは空にしておくと初回実行時に取得できます。
アプリケーション登録時に使用したアカウント以外のRT画像を保存する際などには空にしておきます。

以上のファイルを同じディレクトリに配置します。

試してみる

適当に画像付きツイートをRTして実行してみます。
f:id:nel52:20171215004524p:plain f:id:nel52:20171215004503p:plain

このように保存した画像のファイル名と保存日時がlog.txtに出力されました。

f:id:nel52:20171215004713p:plain f:id:nel52:20171215004724p:plain f:id:nel52:20171215004732p:plain こんな感じにダウンロードした画像は(data.jsonで指定した)フォルダ内に投稿者のユーザー名ごとに保存されました。
ユーザー名と画像は一応モザイクかけてます。

cronで自動実行

収集するスクリプトを書いたはいいものの手動で実行していてはあまり意味がないですね。そこで、コマンドを指定したスケジュールで自動的に実行してくれるcronの設定を行います。

1. 実行するスケジュールを設定する

以下のコマンドでスケジュールを記述するエディタを起動する。
$ crontab -e

2. スケジュールの記述方法

スケジュールは
分 時 日 月 曜日 実行したいコマンド
のようにして指定します。
ここで注意したいのが、コマンドを絶対パスで指定することです。
ちなみに、コマンドのパスは
$ which コマンドの名前
で確認できます。

スケジュールの記述例
  • 毎時15分
    15 * * * * /省略/python3 /省略/get_rt_image.py

  • 毎日12時30分
    30 12 * * * /省略/python3 /省略/get_rt_image.py

  • 毎日12時・19時・20時・21時・23時の52分
    52 12,19-21,23 * * * /省略/python3 /省略/get_rt_image.py

他にも色々な指定方法があるのでこちらを参考にするといいと思います。

3.スケジュールの確認

現在設定されているスケジュールは
$ crontab -l
コマンドで確認することができます。

これで自動でRT画像を収集できますね。
ただし、僕は現状普通のPCで動かしているので、スケジュールした時刻にPCを起動していないといけませんが...

欲しい機能と問題点

  • ふぁぼにも対応(ふぁぼした順に並んでなかったのでめんどくさそう)
  • 動画付きツイートをRTした場合、その動画のサムネを保存してしまう(まあ仕様ということで)
  • ツイ消しでツイート数を減らされると画像を取得できない。ツイートIDをチェックする方式に変えれば良さそう(?)
  • 雑なエラー処理(Tweepyでの1回のツイート取得上限が20らしいので、それぞれの実行の間にツイートが20を越えると多分エラー etc.)
  • 一部画像URLがあるはずの場所に格納されていないツイートがあり、保存できない場合があるのですが、今のところ対策がわかりません…そのうち修正します(多分)

終わりに

この他にも「特定のアカウントのアイコンの変更を通知するbot(こんな感じの)」や「指定したハッシュタグのついた画像を保存orRT」みたいに色々とbotが作れて楽しそうです。
あと、PixivPyを使えばpixivでも同じようなことが出来ます。

また、同じような手法でイラスト以外にも様々な情報を様々なサイトから自動で収集することができます。 APIが提供されていない場合でもHTMLから特定の情報を抜き出すスクレイピングという技術を使うと出来ます。 ただ、これについては冒頭でも話したように規約や法律で禁止されている場合があるので、やってみる際は注意するようにしてください。無闇矢鱈に情報を大量収集するのは倫理的問題もありそうですし…

というわけで以上になります。最後までお付き合い頂きありがとうございました。

明日の記事の担当は翡翠さん(@utanknight)です。(音声作品のオタクとしては非常に楽しみ。)

参考ページ

API Keyの取得について

Tweepyを使って、PythonでTwitterのAPIを超簡単に操作する - StatsBeginner: 初学者の統計学習ノート

Twitter REST APIの使い方

Tweepyについて

Tweepy Documentation — tweepy 3.5.0 documentation

Pythonメモ: Tweepyのややこしいレスポンスデータの読み方 〜Twitter API活用の最初の難関〜 - StatsBeginner: 初学者の統計学習ノート

requestsについて

Requests: 人間のためのHTTP — requests-docs-ja 1.0.4 documentation

Requests の使い方 (Python Library) - Qiita

cronについて

cronの設定方法 - Qiita

Macでcronを使う時の注意点 - Qiita

その他

一番分かりやすい OAuth の説明 - Qiita