初投稿です。
本記事はFlask+gunicornを使ってTwitterに自動投稿を行うPythonアプリをHerokuにデプロイした際の備忘録的な内容になっています。
ほぼ知識ゼロから始めたこともあり色々とハマったので、同じ境遇の方の助けになればと思い、本記事を書こうと思った次第です。
本稿は下記構成となっています。
#前提条件
以下がインストール済みである前提で話を進めます。
- Python3.7系
- git
#Twitter API keyの取得
下記ページを参考にAPIkey & secret
, Access token & secret
を取得してください。
TwitterAPIを申請して一発で承認されるまでの手順まとめ(例文あり)+APIキー、アクセストークン取得方法
自分の場合ですが、利用目的はbotの仕様を詳細に書いたところ承認されました。
#各種パッケージのインストール
- requests : Rest API操作を簡単に行うことのできるライブラリ
- requests-oauthlib : PythonでOAuth認証を簡単に行うためのライブラリ
- Flask : PythonのWebフレームワーク
- gunicorn : WSGIアプリ(今回の場合Flask)を動かすサーバー
下記コマンドでインストールします。
pip install requests
pip install requests-oauthlib
pip install Flask
pip install gunicorn
#ファイル構成
作業ディレクトリ以下のファイル構成は下記とします。
test # 作業ディレクトリ
├── Procfile # Heroku上で起動するコマンドを記述するファイル
├── requirements.txt # 依存ライブラリを記述するファイル
├── runtime.txt # Heroku上でどのバージョンのPythonを使用するのかを記述するファイル
├── config.py # 認証情報
├── test_app.py # アプリ本体
├── common_func.py # アプリ本体で使用する関数を外出し
└── json # JSONファイルを置くディレクトリ
└── tweet_contents.json # ツイートする内容を記述したファイル
{
"contents":
{
"img_url" : "画像URL",
"tweet_text" : "ツイート送信のテスト"
}
}
上記ファイルのツイート送信のテスト
にツイートの本文を記載します。
画像URL
には投稿したい画像のURLを指定してください。(おまけその1で使用します)
ちなみに、今回はこちらの画像を用意しました。
import os
from requests_oauthlib import OAuth1Session
CK = os.environ["CONSUMER_KEY"] # 環境変数の「CONSUMER_KEY」を設定
CS = os.environ["CONSUMER_SECRET"] # 環境変数の「CONSUMER_SECRET」を設定
AT = os.environ["ACCESS_TOKEN"] # 環境変数の「ACCESS_TOKEN」を設定
ATS = os.environ["ACCESS_TOKEN_SECRET"] # 環境変数の「ACCESS_TOKEN_SECRET」を設定
twitter = OAuth1Session(CK, CS, AT, ATS) # 認証処理
#エンドポイント
update_url = "https://api.twitter.com/1.1/statuses/update.json" #ツイート送信エンドポイント
upload_media_url = "https://upload.twitter.com/1.1/media/upload.json" #メディアアップロードエンドポイント
認証情報とエンドポイントをグローバル変数で定義しています。
認証処理に必要な各種APIkey,アクセストークンは、os.environ["環境変数名"]
で環境変数から取得することができます。
こうすることで、トークンの再生成等で値が変更になった場合にソースコードを修正しないで済みます。
import os, json, common_func
from flask import Flask
app = Flask(__name__)
@app.route("/")
def tweet_test():
common_func.post_tweet()
return "Result OK."
if __name__ == "__main__":
port = os.environ.get("PORT", 3333)
app.run(host='0.0.0.0', port=port)
アプリ本体の処理を記述しています。
アプリケーション起動後、適当なWebブラウザを用いて下記URLにアクセスしてください。
http://0.0.0.0:3333/
上記アドレスにアクセスすると、tweet_test()
が実行されます。
正常終了した場合、ページ上にResult OK.
と表示されます。
※上記URLのページにアクセスできない場合
ブラウザによっては、アドレスに0.0.0.0
を使用するとエラーになる場合があるようです。
その場合、URLの0.0.0.0
をlocalhost
に置き換えてアクセスしてみてください。
http://localhost:3333/
import os, json, config
def get_tweet_content(): #jsonファイルからツイート本文を取得
json_file_path = os.path.join(os.path.dirname(__file__), "json/tweet_contents.json")
try:
f = open(json_file_path, "r", encoding="cp932")
except FileNotFoundError as e:
print(e)
exit() #ファイルが見つからなかった場合は終了
json_data = json.load(f)
return json_data["contents"]["tweet_text"]
def post_tweet():
tweet_text = get_tweet_content()
params = {"status": tweet_text}
res = config.twitter.post(config.update_url, params=params)
if res.status_code == 200: #正常投稿出来た場合
print("Success.")
else: #正常投稿出来なかった場合
res_text = json.loads(res.text)
print("Failed. : %d, %s"% (res_text["errors"][0]["code"], res_text["errors"][0]["message"]))
- get_tweet_content()
get_tweet_content()
はjsonファイルから読み込んだツイート本文を戻り値として返却する関数です。
json.loads()
によってjsonファイルの内容を連想配列で取得します。 - post_tweet()
post_tweet()
はツイートを投稿するための関数です。
params
を設定することで、ツイート内容やリプライ先を指定することができます。
twitter.post()
の第1引数にはエンドポイントのURL、第2引数には連想配列param
を指定します。
実行結果は画像のようになります。
#アプリケーションのデプロイ
###Herokuの登録・Heroku CLIのインストール
下記ページを参考にHerokuの登録・Heroku CLIのインストールを行ってください。
【Heroku】WindowsでHerokuを使ってPythonアプリをデプロイするメモ【Python】
###Herokuアプリケーションの作成
下記コマンドでHerokuにログインします。
heroku login
次に、下記コマンドでアプリケーションを作成します。
今回はtest_app
という名前でアプリケーションを作成します。
heroku create test_app
アプリケーションを作成後作業ディレクトリに移動し、Gitリポジトリの作成とリモート接続を行います。
(今回は作業ディレクトリをCドライブ直下に作成したと仮定します)
cd /C/test
git init
heroku git:remote -a test_app
###設定ファイルの作成
下記設定ファイルを作成します。
- runtime.txt
Pythonのバージョンを記載します。
Herokuがサポートしているバージョンを記載してください。
python-3.7.9
ローカル環境のPythonのバージョンは下記コマンドで確認できます。
python --version
- requirements.txt
インストールするモジュールを記載します。
Flask==1.1.2
gunicorn==20.0.4
requests==2.25.0
requests-oauthlib==1.3.0
ローカル環境にインストールされたモジュール一覧は下記コマンドで確認できます。
インストールされたモジュールがすべて表示されるので、必要なモジュールのみ記載します。
pip freeze
- Procfile
アプリケーションを起動するために実行するコマンドを記載します。
web: gunicorn test_app:app --log-file -
###デプロイ
下記コマンドを実行します。
git add .
git commit -m "First deployment."
git push heroku master
コマンドgit push heroku master
のmaster
はご自身の環境に合わせて適宜読み替えてください。
###トラブルシューティング
デプロイ時に発生したエラーの原因とその解決方法を以下にまとめました。
- buildpackエラー
エラーログ:remote: ! No default language could be detected for this app.
原因:buildpackの設定漏れ
解決方法:下記コマンドを実行し、ビルドパックを設定します。
参考:herokuで悩んだところ
heroku buildpacks:set heroku/python
-
Procfile
が見つからない
エラーログ:remote: No Procfile detected, using the default web server.
原因:ファイル名を間違えて「Profile」としていたため。
解決方法:ファイル名をProcfile
にリネームした。
(補足)
Procfile
に拡張子はありません。
テキストエディタ等でファイルを作成すると、特に指定が無ければテキスト形式(.txt)で作成されるため注意が必要です。
下記コマンドでファイルを作成すると無難です。
echo "起動コマンド" > Procfile
###環境変数の設定
下記コマンドでアプリケーションで使用する環境変数を設定します。
設定値は"Twitter API keyの取得"で取得した値を使用してください。
heroku config:set CONSUMER_KEY="********" # API key
heroku config:set CONSUMER_SECRET="********" # API key secret
heroku config:set ACCESS_TOKEN="********" # Access token
heroku config:set ACCESS_TOKEN_SECRET="********" # Access token secret
###デプロイの確認
下記コマンドでHerokuアプリにアクセスし、アプリが正常に動作していることを確認します。
heroku open
###ログの表示
下記コマンドでコンソール上にログを表示することができます。
-t
はログをストリーミング状態で表示するためのオプションです。
heroku logs -t
#おまけその1:画像付きでツイートする
common_func.py
を以下のように修正します。
import os, json, config, urllib.request
def get_tweet_content(): #jsonファイルからツイート内容を取得
json_file_path = os.path.join(os.path.dirname(__file__), "json/tweet_contents.json")
try:
f = open(json_file_path, "r", encoding="cp932")
except FileNotFoundError as e:
print(e)
exit() #ファイルが見つからなかった場合は終了
return json.load(f) #ツイート内容のオブジェクトを返却
def upload_media(img_url): #画像をTwitterにアップロードし、media_idをリターン
res = urllib.request.urlopen(img_url)
img_data = res.read()
img_files = {"media": img_data}
res_media = config.twitter.post(config.upload_media_url, files=img_files)
if res_media.status_code == 200:
return json.loads(res_media.text)["media_id"]
else:
print("Image upload failed: %s", res_media.text)
exit() #画像のアップロードに失敗した場合は終了
def post_tweet():
tweet_content = get_tweet_content()
media_id = upload_media(tweet_content["contents"]["img_url"])
params = {"status": tweet_content["contents"]["tweet_text"], "media_ids" : media_id}
res = config.twitter.post(config.update_url, params=params)
if res.status_code == 200: #正常投稿出来た場合
print("Success.")
else: #正常投稿出来なかった場合
res_text = json.loads(res.text)
print("Failed. : %d, %s"% (res_text["errors"][0]["code"], res_text["errors"][0]["message"]))
#おまけその2:リプライツイートに返信する
common_func.py
を以下のように修正します。
import os, json, config
def get_tweet_content(): #jsonファイルからツイート本文を取得
json_file_path = os.path.join(os.path.dirname(__file__), "json/tweet_contents.json")
try:
f = open(json_file_path, "r", encoding="cp932")
except FileNotFoundError as e:
print(e)
exit() #ファイルが見つからなかった場合は終了
json_data = json.load(f)
return json_data["contents"]["tweet_text"]
def post_tweet():
tweet_text = get_tweet_content()
params = {"status": tweet_text, "in_reply_to_status_id" : "XXXXXXXXXXXXXXXXXXX"}
res = config.twitter.post(config.update_url, params=params)
if res.status_code == 200: #正常投稿出来た場合
print("Success.")
else: #正常投稿出来なかった場合
res_text = json.loads(res.text)
print("Failed. : %d, %s"% (res_text["errors"][0]["code"], res_text["errors"][0]["message"]))
in_reply_to_status_id
には、返信したいツイートのIDを入力してください。
ツイートのIDは、ブラウザ版twitterでツイートを表示した際のURLの末尾に記載されている、19桁の数字です。
https://twitter.com/ユーザ名(スクリーンネーム)/status/XXXXXXXXXXXXXXXXXXX
in_reply_to_status_id
に返信したいツイートのIDを指定しただけでは、返信を行うことはできません。
ツイート本文の文頭に@付きでユーザー名(スクリーンネーム)を追加する必要があります。
そのため、tweet_contents.json
を以下のように修正します。
{
"contents":
{
"img_url" : "画像URL",
"tweet_text" : "@XXXXXXXX 返信のテスト"
}
}
tweet_text
の文頭に@XXXXXXXX
を追加します。
※XXXXXXXX
には、返信したいアカウントのユーザ名(スクリーンネーム)を記載してください。