22
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PythonでTwitterの画像生成&自動返信botを作る

Posted at

#はじめに
前回定期ツィートbotを自作してみて楽しかったので、今度は自動画像生成と自動返信の機能のあるbotを作成してみました。

今回作成したbotの仕様は以下の通りです。

  • BotはRaspberry Pi上で動作する。
  • 特定の定期ツィートに対し、時間内にフォロワーから特定のワードを含む返信があった場合に、自動で返信を返す。
  • 自動返信は各フォロワーにつき1日1回限りで、フォロワー以外には反応しない。
  • 自動返信するツィートには、返信をくれたフォロワーの名前を記入した画像を添付する。
  • 添付する画像は複数の画像素材をランダムに組み合わせて自動生成される。

#環境

  • Raspberry Pi 3 Model B+
  • Python: 3.7.4

使用ライブラリ:

  • Twython
  • NumPy
  • Pillow

ライブラリはそれぞれ事前にインストールしておく必要があります。

Twython:

pip3 install twython

NumPyはapt-getとpipのどちらからでもインストールできますが、apt-getで入れるのがおすすめらしいです。

sudo apt-get install python-numpy

Raspberry Piで使用する場合、Pillowをインストールする前にFreeTypeライブラリをインストールしないとImageFontモジュールが使用できません。FreeTypeのインストールのやり方はhttps://noknow.info/it/os/install_freetype_from_source?lang=ja を参照してください。FreeTypeインストール後、pipでインストールします。

pip3 install Pillow

※実の所、FreeTypeのインストールが必要十分条件なのか把握できてません。
私の場合、プログラム実行時にThe _imagingft C module is not installedのエラーコードが出た後に、様々なライブラリのインストールを試し、最後に上記FreeTypeのインストールを行った後にターミナルを再起動することでImageFontモジュールを動作させることができるようになりました。
もし、FreeTypeをインストールしてもThe _imagingft C module is not installedになる場合は、

pip3 uninstall Pillow
sudo apt-get install libfreetype6
sudo apt-get install libfreetype6-dev
sudo apt-get install libjpeg-dev
pip --no-cache-dir install -I pillow

と実行し、ターミナルを再起動してみてください。

####前提条件
既にTwitter APIに登録してAPIキーを取得しているものとします。

#自動返信botの作成
##画像素材の準備
まず絵を描きます。
イラスト作成_スクリーンショット.jpeg

手間をかけられない場合はフリー素材等を利用しても大丈夫です。

背景画像以外は透過PNGで作成します。

画像ファイルはたくさん用意した方が生成画像のパターンが多くなって楽しくなります。
今回は背景3種類、キャラ6種類、吹き出し1種類の計10枚を用意しました。これらのファイルを画像素材と名付けたディレクトリに入れておきます。
画像素材一覧.png

##Twitter APIキーファイルの作成
auth.pyというファイルを作成して4つのキーを記述します。
(''には自分のアカウントで取得したAPIキーを入れてください。)

auth.py
consumer_key = ''
consumer_secret = ''
access_token = ''
access_token_secret = ''

##フォントファイルの用意
画像に文字列を記載するためにフォントファイルが1つ以上必要になります。自分の好みのフォントを用意してください。
今回は以下の6種類のフリーフォントをダウンロードして使用しました。

はなぞめフォント:https://www.asterism-m.com/font/hanazomefont/
アプリ明朝:http://flopdesign.com/blog/font/5852/
やさしさゴシック手書き:http://fontna.com/freefont/?p=40
あずきフォント:http://azukifont.com/font/azuki.html
うつくし明朝体:https://www.flopdesign.com/freefont/utsukushi-mincho-font.html
えり字:http://v7.mine.nu/pysco/gallery/font/06.html

フォントファイルを"Fonts"と名付けたディレクトリにまとめて入れておきます。

##定期ツィートするプログラムの作成
返信をもらう元となる定期ツィートを行うプログラムを作成します。
以下にコードを示します。

aisatsu.py
import sys
from logging import getLogger, StreamHandler, FileHandler, DEBUG, Formatter

from twython import Twython, TwythonError
from auth import (
    consumer_key,
    consumer_secret,
    access_token,
    access_token_secret
)

twitter = Twython(
    consumer_key,
    consumer_secret,
    access_token,
    access_token_secret
)

logfilename = "./aisatsu-log.txt"
savefilename = "aisatsu-id.txt" #ツィートID保存用ファイル
replylog = "reply-list.txt"     #返信リスト

logger = getLogger(__name__)
handler1 = StreamHandler()
handler1.setFormatter(Formatter("%(asctime)s %(levelname)8s %(message)s"))
handler2 = FileHandler(logfilename)
handler2.setLevel(DEBUG)
handler2.setFormatter(Formatter("%(asctime)s %(levelname)8s %(message)s"))
logger.setLevel(DEBUG)
logger.addHandler(handler1)
logger.addHandler(handler2)
logger.propagate = False


def main():
    text = "おはようございます。\n自動返信bot正式稼働しました。\n\nこのツィートに朝の挨拶を返信していただきますと、画像付きで自動返信します(朝10時まで、フォロワー様限定)。"
    try:
        response = twitter.update_status(status=text) 
    except TwythonError as e:
        logger.error(e.msg)
        sys.exit(0)
    else:
        logger.info("Tweetしました: \n{0}".format(text))
        with open(savefilename, mode='w') as f:     
            f.write(response['id_str'])     #挨拶ツィートのIDを記録
        with open(replylog, mode='w') as f:
            pass                            #返信リストを空にする


if __name__ == '__main__':
    main()

このコードが実行されるとツィートが行われるとともに、そのツィートのIDを記録したaisatsu-id.txtというファイルと、reply-list.txtという空のファイルが生成されます。
上記コードのtextに代入された文字列がツィート内容になるので、各自の好みに合わせて文字列を変更してください。

##画像生成と自動返信を行うプログラムの作成
もらった返信に対して、画像生成と自動返信を行うプログラムを作成します。
以下にコードを示します。

autoreply.py
import unicodedata
import os
import sys
import datetime
import re
from PIL import Image, ImageDraw, ImageFont, ImageFilter, ImageEnhance
import random
import numpy as np
from logging import getLogger, StreamHandler, FileHandler, DEBUG, Formatter

from twython import Twython, TwythonError
from auth import (
    consumer_key,
    consumer_secret,
    access_token,
    access_token_secret
)

twitter = Twython(
    consumer_key,
    consumer_secret,
    access_token,
    access_token_secret
)

logfilename = "./autoreplylog.txt"
savefilename = "aisatsu-id.txt"
replylog = "reply-list.txt"

key_word = "おはよ"

time_reduntance = 5     #時間余裕の設定(分)
time_start = 7          #返信開始時刻
time_end = 10           #返信終了時刻

logger = getLogger(__name__)
handler1 = StreamHandler()
handler1.setFormatter(Formatter("%(asctime)s %(levelname)8s %(message)s"))
handler2 = FileHandler(logfilename)
handler2.setLevel(DEBUG)
handler2.setFormatter(Formatter("%(asctime)s %(levelname)8s %(message)s"))
logger.setLevel(DEBUG)
logger.addHandler(handler1)
logger.addHandler(handler2)
logger.propagate = False

def main():
    dt_now = datetime.datetime.now()
    dt_start = datetime.datetime(dt_now.year, dt_now.month, dt_now.day, hour=time_start)
    dt_end = datetime.datetime(dt_now.year, dt_now.month, dt_now.day, hour=time_end, minute=time_reduntance)

    if dt_start <= dt_now <= dt_end:  #返信時間内かどうかの判定
        with open(savefilename, mode='r') as f:     #定期挨拶ツィートのIDをファイルから取得
            tweetid = f.readline()
        try:
            responses = twitter.get_mentions_timeline(count=30)     #メンションを取得
        except TwythonError as e:
            logger.error(e.msg)
            sys.exit(1)
        try:
            followers_ids = twitter.get_followers_ids(stringify_ids=True)     #フォロワーIDリストを取得
        except TwythonError as e:
            logger.error(e.msg)
            sys.exit(1)
        with open(replylog, mode='r') as f:         #返信リストを取得
            alreadylist = f.readlines()
        alreadylist = [name.rstrip('\n') for name in alreadylist]
        replylist = alreadylist
        for response in responses:
            dt = datetime.datetime.strptime(response['created_at'], '%a %b %d %H:%M:%S +0000 %Y')
            dt = dt + datetime.timedelta(hours=9)
            if dt_start < dt < dt_end and response['in_reply_to_status_id_str'] == tweetid:     #指定時間内かつ指定ツィートへの返信か判定
                usr = response['user']['id_str']
                if usr in followers_ids['ids'] and not usr in replylist:        #まだ返信していないフォロワーか確認
                    if key_word in response['text']:                            #返信ツィートにキーワードが含まれているか確認
                        doReply(response)
                        replylist.append(usr)
        with open(replylog, mode='w') as f:     #返信リストのファイルを更新
            f.write('\n'.join(replylist))


def doReply(response):
    usr_screen_name = response['user']['screen_name']
    name = nameProcess(response['user']['name'])
    if name == "":
        name = usr_screen_name
    imgpath = genImg(name)
    text = "@" + usr_screen_name
    try:
        image = open(imgpath, "rb")
        responseimg = twitter.upload_media(media=image)
        twitter.update_status(status=text, in_reply_to_status_id=response['id'], auto_populate_reply_metadata=True, media_ids=[responseimg['media_id']])
        logger.info("返信しました: \n{0}\n添付画像: {1}".format(text, imgpath))
    except TwythonError as te:
        logger.error(te.msg)
        sys.exit(1)
    except FileNotFoundError as fe:
        logger.error(fe)
        sys.exit(1)
    
def nameProcess(name):
    name = name.rstrip('\n')
    r = r"[\((\[<\{<「『“【[〈{《〔‘].*?$"
    if re.sub(r, "", name) == "":
        r = r"\(.+?\)|(.+?)|\[.+?\]|<.+?>|\{.+?\}|<.+?>|「.+?」|『.+?』|“.+?”|【.+?】|[.+?]|〈.+?〉|{.+?}|《.+?》|〔.+?〕|‘.+?’"
    p = re.sub(r, "", name)
    r = r"[@@].*?$"
    if re.sub(r, "", p) == "":
        r = r"[@@]"
    p = re.sub(r, "", p)
    r = r"[//\||::].+?$"
    if re.sub(r, "", p) == "":
        r = r"[//\||::]"
    p = re.sub(r, "", p)
    r = r"[\u0000-\u007F\uFF01-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF40\uFF5B-\uFF65\u3000-\u303F\u3041-\u309F\u30A1-\u30FF\uFF66-\uFF9F\u2E80-\u2FDF\u3005-\u3007\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\U00020000-\U0002EBEF]+"
    m = re.search(r, p)
    if m:
        p = m.group()
    else:
        p = ""
    r = r"[!!]+"
    p = re.sub(r, "", p)
    r = r"bot$|Bot$|BOT$"
    p = re.sub(r, "", p)
    return p

def genImg(name):
    imgpath = "./生成画像/"
    
    #キャラ画像素材のパス
    imgmat1_path = "./素材画像/挨拶−閉口ノーマル.png"
    imgmat2_path = "./素材画像/挨拶−閉口微笑み.png"
    imgmat3_path = "./素材画像/挨拶−閉口ウインク.png"
    imgmat4_path = "./素材画像/挨拶−開口ノーマル.png"
    imgmat5_path = "./素材画像/挨拶−開口微笑み.png"
    imgmat6_path = "./素材画像/挨拶−開口ウインク.png"

    #背景画像素材のパス
    bgmat1_path = "./素材画像/背景_グリーン.png"
    bgmat2_path = "./素材画像/背景_ピンク.png"
    bgmat3_path = "./素材画像/背景_ブルー.png"

    #吹き出し画像素材のパス
    baloon_path = "./素材画像/吹き出し.png"

    #フォントのパス
    font1_path = "./Fonts/はなぞめフォント.ttf"
    font2_path = "./Fonts/アプリ明朝.otf"
    font3_path = "./Fonts/やさしさゴシック手書き.otf"
    font4_path = "./Fonts/azuki.ttf"
    font5_path = "./Fonts/UtsukushiFONT.otf"
    font6_path = "./Fonts/えり字.otf"

    imgmat_list = [imgmat1_path, imgmat2_path, imgmat3_path, imgmat4_path, imgmat5_path, imgmat6_path]
    bgmat_list = [bgmat1_path, bgmat2_path, bgmat3_path]
    font_list = [font1_path, font2_path, font3_path, font4_path, font5_path, font6_path]

    character = Image.open(random.choice(imgmat_list)).copy()
    character = character.resize(size=(440, 704), resample=Image.ANTIALIAS)     #画像を縮小
    bgimg = Image.open(random.choice(bgmat_list)).copy()
    baloon = Image.open(baloon_path).copy()

    bgimg.paste(baloon, (0,0), baloon)
    bgimg.paste(character, (24, 64), character)

    draw = ImageDraw.Draw(bgimg)    #drawインスタンス生成
    font = ImageFont.truetype(random.choice(font_list),40)

    r = r"たん$|さん$|ちゃん$|くん$|君$|様$"
    if re.search(r, name):
        title = ""
    else:
        title = np.random.choice([u"さん", u""], p=[0.9, 0.1])
    text = np.random.choice([u"おはようございます", u"ごきげんよう"], p=[0.9, 0.1])
    draw.text((550, 200), text, fill = (0, 0, 0), font = font)
    draw.text((550, 280), name + title, fill = (0, 0, 0), font = font)

    dt_now = datetime.datetime.now()
    imgname = imgpath + dt_now.strftime('%Y%m%d%H%M%S%f') + ".png"
    bgimg.save(imgname)
    return imgname

if __name__ == '__main__':
    main()

上記のコードが実行されると、先のaisatsu.py実行時のツィートに対して、日本時間で7時00分から10時05分までにフォロワーから「おはよ」の文字を含むツィートが返信されていたら画像を生成して返信を返します。一度返信したフォロワーのIDはreply-list.txtに記録され、このファイルがクリアされるまでは同じ人には返信されないようになります。

##ファイル構成とcrontab設定
上記のファイルとディレクトリに加え、生成画像と名付けた空のディレクトリを用意してautoreply_botという名前のディレクトリにまとめて置きます。以下のようなファイル構成になっていることを確認します。

ファイル構成
/home/pi/autoreply_bot
├── Fonts
│   ├── UtsukushiFONT.otf
│   ├── azuki.ttf
│   ├── えり字.otf
│   ├── アプリ明朝.otf
│   ├── はなぞめフォント.ttf
│   └── やさしさゴシック手書き.otf
├── aisatsu-id.txt(実行後に生成)
├── aisatsu-log.txt(実行後に生成)
├── aisatsu.py
├── auth.py
├── autoreply.py
├── autoreplylog.txt(実行後に生成)
├── reply-list.txt(実行後に生成)
├── 生成画像
└── 素材画像
    ├── 背景_ピンク.png
    ├── 背景_ブルー.png
    ├── 背景_グリーン.png
    ├── 吹き出し.png
    ├── キャラ−閉口微笑み.png
    ├── キャラ−開口微笑み.png
    ├── キャラ−閉口ウインク.png
    ├── キャラ−閉口ノーマル.png
    ├── キャラ−開口ウインク.png
    └── キャラ−開口ノーマル.png

プログラムを定期実行するためにcrontabを設定します。以下のコマンドを実行してcrontabの編集に入ります。

crontab -e

末尾に以下の記述します。

crontab
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
LANG=ja_jp.UTF-8
LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH
00 07 * * * cd /home/pi/autoreply_bot; python3 ./aisatsu.py
*/2 7-11 * * * cd /home/pi/autoreply_bot; python3 ./autoreply.py >> /home/hisan/Twitter_bot/replybot-log.log 2>&1

変更を保存してcrontabを閉じます。
この設定では、aisatsu.pyは毎朝7時に1回実行され、autoreply.pyは毎日7時〜11時まで2分間隔で実行されます。
実行時間の設定は各自の好みに合わせて変更してください。

#実行例
自動返信の実行例としては、以下のツィートを参照してください。

定期ツィート:

自動返信:

基本的には相手の名前に敬称("さん"または"様")をつけて呼びますが、学術たんのように最初から名前に敬称がある場合はそのまま呼んでいます。

#コードの説明
本プログラムで使用しているコードのうち、Twitter APIの応答を利用する部分について簡単に解説します。

###ツィートのID取得
Twythonでツィートを行う時に使用するupdate_statusは、ツィートの情報を持ったJSON形式の応答を戻り値として渡すことができます。

sample.py
from twython import Twython, TwythonError
from auth import (
    consumer_key,
    consumer_secret,
    access_token,
    access_token_secret
)

twitter = Twython(
    consumer_key,
    consumer_secret,
    access_token,
    access_token_secret
)

text = "テスト"
response = twitter.update_status(status=text) 
print(response)
実行結果(例)
{'created_at': 'Sat Aug 31 14:12:07 +0000 2019', 'id': 1167802249529085952, 'id_str': '1167802249529085952', 'text': 'テスト', 'truncated': False, 'entities': {'hashtags': [], 'symbols': [], 'user_mentions': [], 'urls': []}, 'source': '<a href="http://hisan-twi.blog.so-net.ne.jp/" rel="nofollow">ヒサン@デバイスbot</a>', 'in_reply_to_status_id': None, 'in_reply_to_status_id_str': None, 'in_reply_to_user_id': None, 'in_reply_to_user_id_str': None, 'in_reply_to_screen_name': None, 'user': {'id': 2938468536, 'id_str': '2938468536', 'name': 'ヒサン@電子材料・デバイスbot', 'screen_name': 'Hisan_twi', 'location': '茨城県', 'description': 'エレクトロニクスに関するあらゆるものが好きです。勉強したことをまとめて呟いています。たまに物性の基礎とかも。光るものや鉱物も好き。イラスト初心者。ご指摘・ご質問は気軽にどうぞ。', 'url': None, 'entities': {'description': {'urls': []}}, 'protected': False, 'followers_count': 932, 'friends_count': 405, 'listed_count': 24, 'created_at': 'Sun Dec 21 17:35:35 +0000 2014', 'favourites_count': 1718, 'utc_offset': None, 'time_zone': None, 'geo_enabled': True, 'verified': False, 'statuses_count': 8252, 'lang': None, 'contributors_enabled': False, 'is_translator': False, 'is_translation_enabled': False, 'profile_background_color': '000000', 'profile_background_image_url': 'http://abs.twimg.com/images/themes/theme1/bg.png', 'profile_background_image_url_https': 'https://abs.twimg.com/images/themes/theme1/bg.png', 'profile_background_tile': False, 'profile_image_url': 'http://pbs.twimg.com/profile_images/1123687859297370112/LNw7iGv-_normal.jpg', 'profile_image_url_https': 'https://pbs.twimg.com/profile_images/1123687859297370112/LNw7iGv-_normal.jpg', 'profile_banner_url': 'https://pbs.twimg.com/profile_banners/2938468536/1531925826', 'profile_link_color': '080808', 'profile_sidebar_border_color': '000000', 'profile_sidebar_fill_color': '000000', 'profile_text_color': '000000', 'profile_use_background_image': False, 'has_extended_profile': False, 'default_profile': False, 'default_profile_image': False, 'following': False, 'follow_request_sent': False, 'notifications': False, 'translator_type': 'none'}, 'geo': None, 'coordinates': None, 'place': None, 'contributors': None, 'is_quote_status': False, 'retweet_count': 0, 'favorite_count': 0, 'favorited': False, 'retweeted': False, 'lang': 'ja'}

要素のうち、'id' 及び'id_str' がそのツィートを識別するIDです(この2つは整数型か文字列型かの違いだけで、内容は同じです)。

###フォローされているかの判別
自分のアカウント宛ての返信(メンション)はget_mentions_timelineで得ることができます。その応答はupdate_statusで得られるものと同じ形式です。'user' の要素内にそのツィートを行ったユーザーアカウントの情報が記載されていますが、そのアカウントにフォローされているかどうかの情報はありません。
そこで、フォロワーIDリストを取得して、リスト内に対象のユーザーIDがあるかどうかで判別することができます。

responses = twitter.get_mentions_timeline(count=30)
followers_ids = twitter.get_followers_ids(stringify_ids=True)
for response in responses:
    usr = response['user']['id_str']
    if usr in followers_ids['ids']:
        print(usr + ": フォローされています")
    else:
        print(usr + ": フォローされていません")

ちなみに、1回のfollowers_idsで得られるフォロワーIDの上限は5000件です。フォロワーが5000人を超えている場合はcursorを使用する必要があります(詳しくは公式ドキュメントを参照してください)。

###返信の指定
ツィートの返信はupdate_statusin_reply_to_status_idオプションにツィートIDを指定することでできます。

sample.py
import datetime
from twython import Twython, TwythonError
from auth import (
    consumer_key,
    consumer_secret,
    access_token,
    access_token_secret
)

twitter = Twython(
    consumer_key,
    consumer_secret,
    access_token,
    access_token_secret
)

dt_now = datetime.datetime.now()
text = dt_now.strftime('%Y-%m-%d %H:%M:%S') + u"\nテスト"
response = twitter.update_status(status=text)
text = "返信テスト"
twitter.update_status(status=text, in_reply_to_status_id=response['id'], auto_populate_reply_metadata=True)

#課題
絵文字や括弧書き、名前 @ 〜の@以降を削除するなどして相手の名前を短く呼ぶように設定していますが、それでもアカウント名が長い人を呼ぶときは画像からはみ出してしまいます。

自動で改行するようにしたいのですが、まだやり方がわからないので、分かり次第改善したいと思います。

#おわりに
Pythonで画像生成して自動返信を行うbotを作ることができました。
普段あまり会話しないフォロワーさんも挨拶したりしてくれたりしてTLが楽しくなりました。

皆様も是非試してみてください。

以上、お読みいただきましてありがとうございました。
ご意見やご指摘がございましたらコメントいただければ幸いです。

#参考

22
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?