#はじめに
前回定期ツィート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の作成
##画像素材の準備
まず絵を描きます。
手間をかけられない場合はフリー素材等を利用しても大丈夫です。
背景画像以外は透過PNGで作成します。
画像ファイルはたくさん用意した方が生成画像のパターンが多くなって楽しくなります。
今回は背景3種類、キャラ6種類、吹き出し1種類の計10枚を用意しました。これらのファイルを画像素材
と名付けたディレクトリに入れておきます。
##Twitter APIキーファイルの作成
auth.py
というファイルを作成して4つのキーを記述します。
(''
には自分のアカウントで取得したAPIキーを入れてください。)
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"と名付けたディレクトリにまとめて入れておきます。
##定期ツィートするプログラムの作成
返信をもらう元となる定期ツィートを行うプログラムを作成します。
以下にコードを示します。
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に代入された文字列がツィート内容になるので、各自の好みに合わせて文字列を変更してください。
##画像生成と自動返信を行うプログラムの作成
もらった返信に対して、画像生成と自動返信を行うプログラムを作成します。
以下にコードを示します。
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
末尾に以下の記述します。
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分間隔で実行されます。
実行時間の設定は各自の好みに合わせて変更してください。
#実行例
自動返信の実行例としては、以下のツィートを参照してください。
定期ツィート:
おはようございます。
— ヒサン@電子材料・デバイスbot (@Hisan_twi) September 12, 2019
自動返信bot正式稼働しました。
このツィートに朝の挨拶を返信していただきますと、画像付きで自動返信します(朝10時まで、フォロワー様限定)。
自動返信:
@ElekiTan pic.twitter.com/f6zX4ldZ5V
— ヒサン@電子材料・デバイスbot (@Hisan_twi) September 12, 2019
基本的には相手の名前に敬称("さん"または"様")をつけて呼びますが、学術たんのように最初から名前に敬称がある場合はそのまま呼んでいます。@Turquoise_study pic.twitter.com/Pk7wJGxDjf
— ヒサン@電子材料・デバイスbot (@Hisan_twi) September 12, 2019
#コードの説明
本プログラムで使用しているコードのうち、Twitter APIの応答を利用する部分について簡単に解説します。
###ツィートのID取得
Twythonでツィートを行う時に使用するupdate_status
は、ツィートの情報を持ったJSON形式の応答を戻り値として渡すことができます。
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)
テスト
— ヒサン@電子材料・デバイスbot (@Hisan_twi) August 31, 2019
{'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_status
でin_reply_to_status_id
オプションにツィートIDを指定することでできます。
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)
@Hisan_twi
— ヒサン@電子材料・デバイスbot (@Hisan_twi) September 7, 2019
返信テスト
#課題
絵文字や括弧書き、名前 @ 〜の@以降を削除するなどして相手の名前を短く呼ぶように設定していますが、それでもアカウント名が長い人を呼ぶときは画像からはみ出してしまいます。
自動で改行するようにしたいのですが、まだやり方がわからないので、分かり次第改善したいと思います。@GYG4y pic.twitter.com/FeDPS8E2P6
— ヒサン@電子材料・デバイスbot (@Hisan_twi) September 14, 2019
#おわりに
Pythonで画像生成して自動返信を行うbotを作ることができました。
普段あまり会話しないフォロワーさんも挨拶したりしてくれたりしてTLが楽しくなりました。
皆様も是非試してみてください。
以上、お読みいただきましてありがとうございました。
ご意見やご指摘がございましたらコメントいただければ幸いです。
#参考
- 正規表現モジュールの使い方はhttps://note.nkmk.me/python-re-match-search-findall-etc/ およびhttps://note.nkmk.me/python-re-regex-character-type/ を参考にしました。
- Pythonでの画像編集のやり方はhttps://qiita.com/xKxAxKx/items/2599006005098dc2e299 を参考にしました。