LoginSignup
36

More than 1 year has passed since last update.

heroku + Python で Vtuber Twitter bot 作る

Last updated at Posted at 2020-04-15

herokuが基本有料利用になるようです。
有料(おそらく7 USD / 月)でもherokuでtwitter bot運用したい方向けにこの記事は残しておきます。
現在AWSでの運用を検討していますので、うまくいけばAWS版の記事を書くかも知れません。

目的

 任意のワード・ハッシュタグがトレンドにあったときに通知をするTwitter botを作ります。
  (たぶん)無料で運用できるはず。

  • YouTube
    • チャンネル登録者数のキリ番突破通知
  • Twitter
    • フォロワ数のキリ番突破通知
    • プロフィール変更通知
      • アイコン
      • バナー
      • 名前欄
      • 概要欄
      • 場所欄
  • ツイキャス
    • フォロワ数のキリ番突破通知
    • レベルアップ通知

本文

注意:説明を簡単にするために、上記のbotのコードをかなり簡略化したものを紹介しています。そのため変なエラーを吐くかもしれません。ご了承ください。

準備

botのtwitterアカウント作成

 つくってください。

ディレクトリ準備

 botのファイルを保存するディレクトリを作成してください。
 以下で作成するファイルは全てここに保存すること。
 ここではvtuber-twitter-botとします。最終的にはこんな感じの構成。

vtuber-twitter-bot
├── tc_access_token.env
├── dics.py
├── tools.py
├── get_data.py
├── main.py
├── index.py
├── runtime.txt
├── requirements.txt
├── Procfile
├── template.html
├── template_style.css
└── .fonts
    └── ipag.ttc (その他日本語フォント)

heroku

 こちらの記事が詳しいです。登録とCLIのインストールをしましょう。
 無料で運用するのでクレカの登録は不要。

Python 3.x

 Python 3.xでbotのコードを書いていきます。インストールするライブラリは以下。抜けがあったら教えてください。たぶん全部pipでインストールできます。

Twitter API

 こちらの記事が詳しいです。
 API key, Consumer_secret, Access_token, Access_secretを控えておいてください。

YouTube API

 最近の画面での最近の画面での操作を画像多めで説明した記事を書きました。
 API keyを控えておいてください。

ツイキャス API

 APIドキュメント
 Twitterアカウントが必要です。

  • アプリ作成(ここから
    • name, description
      • 指定通り
    • Callback URL
      • 使用したTwitterのURL(https://twitter.com/********)にする。多分他のでもいい?
    • WebHook URL
      • 空欄
    • Scope
      • Read only
    • 上記のように記入したら「create」
  • ClientID, ClientSecretを取得
    • 作ったアプリを開くと「Details」が現れます。
    • 「ClientID」「ClientSecret」の項を控えておくこと。
  • Access Tokenを取得

 こうして、ClientID, ClientSecret, Access Tokenが得られます。
 

実装

 以下では、例として夏色まつりさん、赤井はあとさんの2名のためのbotを作ります。
 たぶんdics.pyだけ書き換えればほかのVTuberさんのbotになるはず。

 理解のため、それぞれのコードをローカルで実行してみることをオススメします。
 os.environ['ENV_NAME']ENV_NAMEという環境変数の値を受け取るコードなので、ローカル実行の際は注意。

dics.py

dics.py
#botが見るVTuberのリスト
members = ['赤井はあと', '夏色まつり']


#YouTube Ch の Channel ID の辞書
#夏色まつりさんのチャンネルのURLは、https://www.youtube.com/channel/UCQ0UDLQCjY0rmuxCDE38FGg
#channel/ 以降の部分を書く
YT_ID_dic  = {  '赤井はあと':'UC1CfXB_kRs3C-zaeTG3oGyg', \
                '夏色まつり':'UCQ0UDLQCjY0rmuxCDE38FGg', \
             }



#TwitterのURL
#mobile版のURLにすること
tw_url_dic = {  '赤井はあと' : 'https://mobile.twitter.com/akaihaato', \
                '夏色まつり' : 'https://mobile.twitter.com/natsuiromatsuri', \
             }

#解説1で取得したtwitter idの辞書
tw_id_dic      = {  '赤井はあと' : 998336069992001537, \
                    '夏色まつり' : 996645451045617664, \
                 }

#https://bitly.com で短縮したチャンネルURL
#https://www.youtube.com/channel/***********
bitly_yt_dic = { '赤井はあと' : 'https://bit.ly/2vSwfee', \
                 '夏色まつり' : 'https://bit.ly/2vKgcPp', \
               }


#解説ツイキャス
tc_id_dic = {   '赤井はあと': '998336069992001537', \
                '夏色まつり': '996645451045617664', \
            }


#https://bitly.com で短縮したツイキャスURL
#https://twitcasting.tv/********
bitly_tc_dic = {    '赤井はあと': 'https://bit.ly/2VaLclQ', \
                    '夏色まつり': 'https://bit.ly/2yi3fOh', \
               }

解説①
 以下のコードを実行すれば、twitterアカウントのIDを取得できます。
 twitter_idの引数はアカウントの@blahblahのblahblahです。

import tweepy


def twitter_api():
    CONSUMER_KEY    = 'YOUR API KEY'
    CONSUMER_SECRET = 'YOUR CONSUMER SECRET'
    ACCESS_TOKEN    = 'YOUR ACCESS TOKEN'
    ACCESS_SECRET   = 'YOUR ACCESS SECRET'
    auth            = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
    auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET)
    api             = tweepy.API(auth)

    return api


def twitter_id(at_name):
    api = twitter_api()
    id_ = api.get_user(at_name).id

    return id_



#赤井はあとさん
twitter_id('akaihaato')
#>> 998336069992001537

#夏色まつり
twitter_id('natsuiromatsuri')
#>> 996645451045617664

解説②
 以下のコードを実行すれば、ツイキャスアカウントのIDを取得できます。
 引数はアカウントの@blahblahのblahblahです。

from pytwitcasting.auth import TwitcastingApplicationBasis
from pytwitcasting.api import API


def twitcas_id(at_name):
    client_id       = 'YOUR CLIENT ID'
    client_secret   = 'YOUR CLIENT SECRET'
    app_basis       = TwitcastingApplicationBasis(client_id=client_id, client_secret=client_secret)
    api             = API(application_basis=app_basis)
    user            = api.get_user_info(at_name)
    id_             = user.id
    
    print(id_)


#赤井はあとさん
twitcas_id('akaihaato')
#>> 998336069992001537

#夏色まつりさん
twitcas_id('natsuiromatsuro')
#>> 996645451045617664

tools.py

tools.py
# -*- coding: utf-8 -*-
from __future__ import print_function
import os
import time
import unicodedata, re
import urllib.request
import urllib.error
from PIL import Image, ImageDraw
import tweepy

sl_time = 3


#Twitterでツイートしたりデータを取得したりする準備
def twitter_api():
    CONSUMER_KEY    = os.environ['API_KEY']
    CONSUMER_SECRET = os.environ['API_SECRET_KEY']
    ACCESS_TOKEN    = os.environ['ACCESS_TOKEN']
    ACCESS_SECRET   = os.environ['ACCESS_TOKEN_SECRET']
    auth            = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)
    auth.set_access_token(ACCESS_TOKEN, ACCESS_SECRET)
    api             = tweepy.API(auth)

    return api



#画像と一緒にツイートする
#tweetはツイートする文章、filesは画像のパスのlist
def tweet_with_imgs(tweet, files):
    api = twitter_api()
    media_ids = []
    for ii in range(len(files)):
        img = api.media_upload(files[ii])
        media_ids.append(img.media_id_string)

    time.sleep(sl_time)
    api.update_status(status=tweet, media_ids=media_ids)





#urlにある画像をdst_pathで指定したパスにダウンロードする
def download_image(url, dst_path):
    try:
        data = urllib.request.urlopen(url).read()
        with open(dst_path, mode="wb") as f:
            f.write(data)
    except urllib.error.URLError as e:
        print(e)




#2つの画像をつなげた画像を作る
#im1_path, im2_pathでパスを指定した2画像を矢印でつなげ、gen_img_nameというパスに保存
def concatenate_img(im1_path, im2_path, gen_img_name):
    im1 = Image.open(im1_path)
    im2 = Image.open(im2_path)

    void_pix   = 30
    dst_width  = im1.width + im2.width + void_pix
    dst_height = max(im1.height, im2.height)
    dst        = Image.new('RGBA', (dst_width, dst_height))
    dst.paste(im1, (0, 0))
    dst.paste(im2, (im1.width + void_pix, 0))

    draw       = ImageDraw.Draw(dst)
    line_xm    = int(im1.width - 5)
    line_xp    = int(dst_width - im2.width - 5)
    line_y     = int(dst_height /2)
    line_width = 20
    arrow_x    = line_xp - 5
    line_coor  = (line_xm, line_y, line_xp, line_y)
    arrow_coor = (line_xp+line_width/2, line_y, \
                    arrow_x, line_y+line_width, \
                    arrow_x, line_y-line_width)
    line_c    = (70, 170, 255)
    draw.line(line_coor, fill=line_c, width=line_width)
    draw.polygon(arrow_coor, fill=line_c)
    dst.save(gen_img_name)

get_data.py

get_data.py
# -*- coding: utf-8 -*-
'''
reference  YouTube API
    http://www.spemi.org/article/youtubeapi/
'''
from __future__ import print_function
import os
import time
import requests
import json
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
from pytwitcasting.auth import TwitcastingApplicationBasis
from pytwitcasting.api import API

from tools import twitter_api
api = twitter_api()
from dics import YT_ID_dic, tw_url_dic, tw_id_dic, tc_id_dic

sl_time = 3






#YouTube チャンネル登録者数を返す。
def get_subscriber(name):
    time.sleep(sl_time)

    API_KEY = os.environ['YT_API_KEY']
    options = { 'key': API_KEY,
                'id': YT_ID_dic[name],
                'part': 'statistics'}

    r     = requests.get('https://www.googleapis.com/youtube/v3/channels', params=options)
    data  = r.json()
    subsc = data['items'][0]['statistics']['subscriberCount']
    subsc = int(subsc)

    return subsc



#Twitter フォロワー数を返す。
def get_follower(name):
    id          = tw_id_dic[name]
    tw_user     = api.get_user(id)
    time.sleep(1)
    follower    = tw_user.followers_count

    return follower






#Twitterプロフィールの内容を返す。
#tw_icon、tw_bannerは画像のURLが入っている。
def get_twitter_profile(name):
    time.sleep(sl_time)

    id          = tw_id_dic[name]
    tw_user     = api.get_user(id)
    tw_name     = tw_user.name
    tw_desc     = tw_user.description.replace('\n', ' ')
    tw_place    = tw_user.location
    tw_url      = tw_user.url
    tw_icon     = tw_user.profile_image_url.replace('normal', '400x400')
    tw_banner   = tw_user.profile_banner_url

    contents    = { '名前' : tw_name, \
                    '概要欄' : tw_desc, \
                    '場所' : tw_place, \
                    'URL' : tw_url, \
                    'アイコン' : tw_icon, \
                    'バナー' : tw_banner}

    return contents





#スクショのための準備
def chrome_driver():
    time.sleep(sl_time)
    options = Options()
    options.binary_location = os.environ.get('GOOGLE_CHROME_BIN')
    #options.binary_location = './bin/headless-chromium'
    options.add_argument('--headless')
    options.add_argument('--disable-gpu')
    options.add_argument('--no-sandbox')
    options.add_argument("--single-process")
    options.add_argument('window-size=1440x1440')
    #browser = webdriver.Chrome(executable_path='./bin/chromedriver', \
    #                            options=options)
    browser = webdriver.Chrome(executable_path=str(os.environ.get('CHROMEDRIVER_PATH')), \
                                options=options)
    #browser = webdriver.Chrome(options=options)
    browser.implicitly_wait(10)
    #print('build browser')

    return browser






#スクショ実行
#twitterを直接スクショするのは不安定なので、local_screenshotで代替することを推奨します
#browser = chrome_driver(), nameはVTuberの名前、png_pathはスクショ画像のパス
#def get_twitter_profile_ss(browser, name, png_path):
#    time.sleep(sl_time)
#    browser.get(tw_url_dic[name])
#    time.sleep(5)
#    #xpath   = '/html/body/div/div/div/div/main/div/div/div/div/div/div/div/div/div[1]'
#    xpath   = '/html/body/div/div/div/div[2]/main/div/div/div/div[1]/div/div[2]/div/div/div[1]'
#    element = browser.find_element_by_xpath(xpath)
#    browser.get_screenshot_as_file(png_path)
#    left    = int(element.location['x'])
#    top     = int(element.location['y'])
#    right   = int(element.location['x'] + element.size['width'])
#    bottom  = int(element.location['y'] + element.size['height'])
#    im      = Image.open(png_path)
#    im      = im.crop((left, top, right, bottom))
#    im.save(png_path)
#
#    return browser


def local_screenshot(browser, name, png_path, contents):
    time.sleep(sl_time)
    html_file = 'local_ss_{}.html'.format(name)
    with open('template.html', 'r') as f:
        htmls = f.read()
    tmp_icon    = contents['アイコン']
    tmp_banner  = contents['バナー']
    tmp_tw_name = contents['名前']
    tmp_sc_name = contents['スクリーン']
    tmp_desc    = contents['概要欄'].replace('\n', '<br>')
    tmp_place   = contents['場所']
    new_htmls   = htmls.format(tmp_icon, tmp_banner, tmp_tw_name, tmp_sc_name, tmp_desc, tmp_place)
    with open(html_file, 'w') as f:
        f.write(new_htmls)

    browser.get('file://' + os.path.abspath(html_file))
    time.sleep(3)
    element = browser.find_element_by_xpath('/html/body/div/div/div/div')
    png = element.screenshot_as_png
    with open(png_path, "wb") as f:
        f.write(png)
    #im.save(png_path)

    return browser




#ツイキャスのサポーター数、レベルを返す。
def get_twitcas_data(name, print_flag=False):
    time.sleep(sl_time)
    client_id       = os.environ['TC_CLIENT_ID']
    client_secret   = os.environ['TC_CLIENT_SECRET']
    app_basis       = TwitcastingApplicationBasis(client_id=client_id, client_secret=client_secret)
    api             = API(application_basis=app_basis)
    tc_id           = tc_id_dic[name]
    user            = api.get_user_info(tc_id)
    level           = user.level
    supporter       = user.get_supporter_list()['total']

    if print_flag:
        print(name, level, supporter)

    return {'supporter' : supporter, 'level' : level}

main.py

  • number_notification
    • rn_time分ごとに数値を取得し、条件を満たしたときにツイート
  • tw_log_notification
    • rn_time分ごとにTwitterプロフを取得し、前回と変わっていればツイート
main.py
# -*- coding: utf-8 -*-
from __future__ import print_function
import time, datetime
import pickle
import os
from glob import glob

import tweepy
from apscheduler.schedulers.blocking import BlockingScheduler
sched = BlockingScheduler()

from tools import twitter_api, tweet_with_imgs, download_image, concatenate_img
api = twitter_api()
from get_data import get_subscriber, get_follower, get_twitter_profile, get_twitter_profile_ss, chrome_driver, get_twitcas_data, local_screenshot
from dics import members, bitly_yt_dic, bitly_tc_dic


sl_time         = 3
rn_time         = 5   #何分ごとに登録者数とかの数値を取得するか

num_log_file    = './num_log_file_{}.pickle'
data_log_file   = './data_log_file_{}.pickle'
icon_img        = './{}_icon_{}.png'
banner_img      = './{}_banner_{}.png'
prof_img        = './{}_prof_{}.png'




member_all = [members]

all_num  = len(member_all)



#数値を取得して、条件を満たせばツイートする
def number_notification(members_, num):
    start_          = time.time()
    num_log_file_   = num_log_file.format(num)
    tweet_head      = '【達成通知】\n\n'

    if os.path.exists(num_log_file_):
        with open(num_log_file_, 'rb') as pi:
            old_contents = pickle.load(pi)

    contents = {}
    for member in members_:
        follower    = get_follower(member)
        subscriber  = get_subscriber(member)
        tc_data     = get_twitcas_data(member)
        contents_in = { 'follower': follower, 'subscriber': subscriber, \
                        'tc_supporter': tc_data['supporter'], 'tc_level': tc_data['level']}
        contents.update({member : contents_in})

    if not os.path.exists(num_log_file_):
        with open(num_log_file_, 'wb') as pi:
            pickle.dump(contents, pi)


    with open(num_log_file_, 'rb') as pi:
        old_contents = pickle.load(pi)

    #th_val人刻みでツイートするための条件設定。th_val = 10000 なら10000人刻み
    def _judge(member, key, th_val=10000):
        if key in contents[member].keys():
            if contents[member][key] // th_val > old_contents[member][key] // th_val:
                return True
            else:
                return False
        else:
            return False

    #botが過去50ツイートと同じツイートしようとしたらキャンセルする(はず)
    recent_tweets = api.user_timeline(count=50)
    recent_tweets = [tweet.text for tweet in recent_tweets]
    def _tw_cancel(tweet):
        flags = [r_tweet.split('(')[0] in tweet for r_tweet in recent_tweets]
        if sum(flags) == 0:
            return True
        else:
            print('CANCELLED : ', tweet)
            return False

    for member in members_:
        member_ = member + 'さん'

        if _judge(member, 'follower'):
            tw_url = api.get_user(id_dic[member]).screen_name
            tweet  = tweet_head + '{}のTwitterフォロワー数が\n  {:.1f}万人を達成しました。\n'\
                                    .format(member_, contents[member]['follower']/10000)
            tweet += '(Twitter : twitter.com/{})\n'.format(tw_url)
            if _tw_cancel(tweet):
                time.sleep(sl_time)
                api.update_status(tweet)
                print('number_notification')
                print('Tweeted.')

        if _judge(member, 'subscriber'):
            tweet  = tweet_head + '{}のチャンネル登録者数が\n  {:.1f}万人を達成しました。\n'\
                                    .format(member_, contents[member]['subscriber']/10000)
            tweet += '(YouTube : {})\n'.format(bitly_yt_dic[member])
            if _tw_cancel(tweet):
                time.sleep(sl_time)
                api.update_status(tweet)
                print('number_notification')
                print('Tweeted.')

        if _judge(member, 'tc_supporter'):
            tweet  = tweet_head + '{}のTwitCastingサポーター数が\n  {:.1f}万人を達成しました。\n'\
                                    .format(member_, contents[member]['tc_supporter']/10000)
            tweet += '(TwitCasting : {})\n'.format(bitly_tc_dic[member])
            if _tw_cancel(tweet):
                time.sleep(sl_time)
                api.update_status(tweet)
                print('number_notification')
                print('Tweeted.')

        if contents[member]['tc_level'] > old_contents[member]['tc_level']:
            tweet  = tweet_head + '{}のTwitCastingレベルが\n  {}に上がりました。\n'\
                                    .format(member_, contents[member]['tc_level'])
            tweet += '(TwitCasting : {})\n'.format(bitly_tc_dic[member])
            if _tw_cancel(tweet):
                time.sleep(sl_time)
                api.update_status(tweet)
                print('number_notification')
                print('Tweeted.')


    with open(num_log_file_, 'wb') as pi:
        pickle.dump(contents, pi)

    measure_time = time.time() - start_
    if measure_time > 60 * rn_time:
        print('Too hard work on number_notification')




#Twitterプロフの変更を検知してツイートする
def tw_log_notification(members_, num):
    start_      = time.time()
    now_tw      = datetime.datetime.utcnow() + timedelta(hours=9)
    now_tw      = '{}'.format(now_tw.time())[:-7]
    contents    = {}
    for member in members_:
        contents.update({member : get_twitter_profile(member)})

    data_log_file_ = data_log_file.format(num)
    if not os.path.exists(data_log_file_):
        with open(data_log_file_, 'wb') as pi:
            pickle.dump(contents, pi)

    with open(data_log_file_, 'rb') as pi:
        old_contents = pickle.load(pi)

    browser = chrome_driver()
    for name in contents.keys():
        if not os.path.exists(icon_img.format('old', name)):
            download_image(contents[name]['アイコン'], icon_img.format('old', name))
        if not os.path.exists(banner_img.format('old', name)):
            download_image(contents[name]['バナー'], banner_img.format('old', name))
        if not os.path.exists(prof_img.format('old', name)):
            #browser = get_twitter_profile_ss(browser, name, prof_img.format('old', name))
            browser = local_screenshot(browser, name, prof_img.format('old', name), contents[name])


    def _make_tweet(browser, name, key):
        name_ = name + 'さん'

        tweet  = '【twitter プロフィール変更通知】\n\n'
        tweet += '{}が{}を変更しました。\n'.format(name_, key)
        if contents[name][key] != old_contents[name][key]:
            if key == 'アイコン':
                print('tw_log_notification')
                download_image(contents[name][key], icon_img.format('new', name))
                concatenate_img(icon_img.format('old', name), icon_img.format('new', name), icon_img.format('tw', name))
                tweet_with_imgs(tweet, icon_img.format('tw', name))
                os.rename(icon_img.format('new', name), icon_img.format('old', name))

            elif key == 'バナー':
                print('tw_log_notification')
                tweet += '1枚目 → 2枚目'
                download_image(contents[name][key], banner_img.format('new', name))
                tweet_with_imgs(tweet, [banner_img.format('old', name), banner_img.format('new', name)])
                os.rename(banner_img.format('new', name), banner_img.format('old', name))

            elif key in ['お気に入り数', 'URL']:
                pass

            else:
                print('tw_log_notification')
                if key in ['名前', '場所']:
                    tweet += '(左) {}\n  ↓\n(右) {}'.format(old_contents[name][key], contents[name][key])
                print(old_contents[name][key])
                print(contents[name][key])
                #browser = get_twitter_profile_ss(browser, name, prof_img.format('new', name))
                browser = local_screenshot(browser, name, prof_img.format('new', name), contents[name])
                concatenate_img(prof_img.format('old', name), prof_img.format('new', name), prof_img.format('tw', name))
                tweet_with_imgs(tweet, prof_img.format('tw', name))

        return browser


    for name in contents.keys():
        for key in contents[name].keys():
            browser = _make_tweet(browser, name, key)
        if os.path.exists(prof_img.format('new', name)):
            os.rename(prof_img.format('new', name), prof_img.format('old', name))
        if os.path.exists(prof_img.format('new', name+'sub')):
            os.rename(prof_img.format('new', name+'sub'), prof_img.format('old', name+'sub'))
    browser.quit()


    with open(data_log_file_, 'wb') as pi:
        pickle.dump(contents, pi)

    measure_time = time.time() - start_
    if measure_time > 60 * rn_time:
        print('Too hard work on tw_log_notification')





if __name__ == '__main__':
    start_   = time.time()
    print('Start work : {}'.format(datetime.datetime.utcnow()))

    for ii, mem_ in enumerate(member_all):
        print('test number_not : {} : {}'.format(ii, mem_))
        number_notification(mem_, ii+1)
        sched.add_job(number_notification, 'interval', minutes=rn_time, args=[mem_, ii+1])
    for ii, mem_ in enumerate(member_all):
        print('test tw_log_not : {} : {}'.format(ii, mem_))
        tw_log_notification(mem_, ii+1)
        sched.add_job(tw_log_notification, 'interval', minutes=rn_time, args=[mem_, ii+1])

    print(time.time() - start_)

    print('old_prof  ', len(glob('old_prof*')))
    print('old_banner', len(glob('old_banner*')))
    print('old_icon  ', len(glob('old_icon*')))

    print('sched work start')
    sched.start()
index.py
#空っぽ。要らない気もする。
template.html
<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="template_style.css">
  </head>
  <body>
    <!-- Twitter Profile Card-->
    <div class="container">
      <div class="row">
        <div class="col-md-4 pb-3">
          <div class="card">
            <img class="img-fluid rounded-circle card-avatar-img" src="{}">
            <img class="card-img-top card-header-img" src="{}" alt="Card image cap">
            <div class="card-body">
              <h5 class="card-title">{}<br>@{}</h5>
              <p class="card-text">{}</p>
              <span class="css-901oao css-16my406 r-m0bqgq r-4qtqp9 r-1tl8opc r-1b7u577 r-bcqeeo r-qvutc0">
                <svg viewBox="0 0 24 24" class="r-m0bqgq r-4qtqp9 r-yyyyoo r-1xvli5t r-1d4mawv r-dnmrzs r-bnwqim r-1plcrui r-lrvibr">
                  <g>
                    <path d="M12 14.315c-2.088 0-3.787-1.698-3.787-3.786S9.913 6.74 12 6.74s3.787 1.7 3.787 3.787-1.7 3.785-3.787 3.785zm0-6.073c-1.26 0-2.287 1.026-2.287 2.287S10.74 12.814 12 12.814s2.287-1.025 2.287-2.286S13.26 8.24 12 8.24z">
                    </path>
                    <path d="M20.692 10.69C20.692 5.9 16.792 2 12 2s-8.692 3.9-8.692 8.69c0 1.902.603 3.708 1.743 5.223l.003-.002.007.015c1.628 2.07 6.278 5.757 6.475 5.912.138.11.302.163.465.163.163 0 .327-.053.465-.162.197-.155 4.847-3.84 6.475-5.912l.007-.014.002.002c1.14-1.516 1.742-3.32 1.742-5.223zM12 20.29c-1.224-.99-4.52-3.715-5.756-5.285-.94-1.25-1.436-2.742-1.436-4.312C4.808 6.727 8.035 3.5 12 3.5s7.192 3.226 7.192 7.19c0 1.57-.497 3.062-1.436 4.313-1.236 1.57-4.532 4.294-5.756 5.285z">
                    </path>
                  </g>
                </svg>
                <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0">
                  <span class="css-901oao css-16my406 r-1tl8opc r-bcqeeo r-qvutc0">
                    {}
                  </span>
                </span>
              </span>
            </div>
        </div>
        </div>
      </div>
    </div>
  </body>
</html>
template_style.css
.container {
  width: 450px;
  object-fit: cover;
}

.card-header-img {
  width: 450px;
  object-fit: cover;
}

.card-avatar-img {
  position: absolute;
  top: 116px;
  left: 20px;
  height: 65px;
  z-index: 1;
}

.card-avatar-back {
  position: absolute;
  top: 110px;
  left: 15px;

  background: #fff;
  border-radius: 50%;
  width: 80px;
  height: 80px;
}

.card-title {
  width: 450px;
  object-fit: cover;
}

.card-text {
  width: 450px;
  object-fit: cover;
}

.r-m0bqgq {
  width: 20px;
  height: 20px;
}

デプロイ

ファイル作成

 runtime.txt, requirements.txt, Procfileを作成。

runtime.txt
python-3.6.2
requirements.txt
Pillow==6.2.1
tweepy==3.6.0
APScheduler==3.0.3
selenium==3.141.0
pytwitcasting==1.0.0
Procfile
web: python index.py
clock: python main.py

 スクショ実行の際、日本語フォントを用意しないと文字化けしてしまいます。
 下のようにファイルと同じ階層に.fontsディレクトリを作成。
 その中に日本語フォント(例えばipag.ttc)を置くと、そのフォントがchromeに使用されます。

vtuber-twitter-bot
├── tc_access_token.env
├── dics.py
├── tools.py
├── get_data.py
├── main.py
├── index.py
├── runtime.txt
├── requirements.txt
├── Procfile
└── .fonts
    └── ipag.ttc (その他日本語フォント)

初デプロイ

 app-nameはコードを保存するディレクトリの名前にするとわかりやすいです。
 以下のコマンドはこのディレクトリ内で実行すること。

heroku login
heroku create app-name

環境変数設定

 heroku config:set ENV_NAME="VALUE"で環境変数を設定できます。最後の4つはそのまま実行。
 app-nameはさっきのやつ。

appname=app-name

heroku config:set ACCESS_TOKEN="YOUR TWITTER ACCESS TOKEN" --app $appname
heroku config:set ACCESS_TOKEN_SECRET="YOUR TWITTER ACCESS TOKEN SECRET" --app $appname
heroku config:set API_KEY="YOUR TWITTER API KEY" --app $appname
heroku config:set API_SECRET_KEY="YOUR TWITTER API SECRET KEY" --app $appname

heroku config:set YT_API_KEY="YOUR YouTube API KEY" --app $appname

heroku config:set TC_CLIENT_ID="YOUR TwitCasting CLIENT ID" --app $appname
heroku config:set TC_CLIENT_SECRET="YOUR TwitCasting CLIENT SECRET" --app $appname

heroku config:set CHROMEDRIVER_PATH="/app/.chromedriver/bin/chromedriver" --app $appname
# chromeとchrome driverのバージョンがあわないとき、この環境変数でchrome driverのバージョンを指定できる。普段はなくておk
# heroku config:set CHROMEDRIVER_VERSION="80.0.3987.106" --app $appname
heroku config:set GOOGLE_CHROME_BIN="/app/.apt/usr/bin/google-chrome" --app $appname
heroku config:set TZ="Asia/Tokyo" --app $appname

デプロイ

app-nameはさっきのやつ。

appname=app-name

git init
git add .
git commit -m "new commit"
git remote add heroku https://git.heroku.com/$appname.git
git push heroku master

buildpack

 botにスクショを実行させるためのツールを設定します。

appname=app-name

heroku buildpacks:add https://github.com/heroku/heroku-buildpack-google-chrome.git --app $appname
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-chromedriver.git --app $appname

メンテナンス

 app-nameディレクトリでheroku logs -tとコマンドを実行すれば、printされたログを確認できます。

 デバッグにはapp-nameディレクトリの1つ上のディレクトリに以下のシェルスクリプトをおいとくと楽です。
 コード修正などが終わったら、app-nameディレクトリで../heroku_deploy.shを実行。失敗したらchmod u+x ../heroku_deploy.shとかで実行可能に設定。

heroku_deploy.sh
git add .
git commit -m "new commit"
git push heroku master
heroku ps:scale web=0 clock=1
heroku ps
heroku logs --tail

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
36