0
0

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 3 years have passed since last update.

Flask+gunicornを使って作成したTwitter連携アプリをHerokuにデプロイした話

Last updated at Posted at 2021-01-04

初投稿です。
本記事は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)を動かすサーバー

下記コマンドでインストールします。

command
pip install requests
pip install requests-oauthlib
pip install Flask
pip install gunicorn

#ファイル構成
作業ディレクトリ以下のファイル構成は下記とします。

tree
test                        # 作業ディレクトリ
├── Procfile                # Heroku上で起動するコマンドを記述するファイル
├── requirements.txt        # 依存ライブラリを記述するファイル
├── runtime.txt             # Heroku上でどのバージョンのPythonを使用するのかを記述するファイル
├── config.py               # 認証情報
├── test_app.py             # アプリ本体
├── common_func.py          # アプリ本体で使用する関数を外出し
└── json                    # JSONファイルを置くディレクトリ
    └── tweet_contents.json # ツイートする内容を記述したファイル

tweet_contents.json
{
  "contents":
  {
    "img_url" : "画像URL",
    "tweet_text" : "ツイート送信のテスト"
  }
}

上記ファイルのツイート送信のテストにツイートの本文を記載します。
画像URLには投稿したい画像のURLを指定してください。(おまけその1で使用します)
ちなみに、今回はこちらの画像を用意しました。

test.jpg


config.py
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["環境変数名"]で環境変数から取得することができます。
こうすることで、トークンの再生成等で値が変更になった場合にソースコードを修正しないで済みます。


test_app.py
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.0localhostに置き換えてアクセスしてみてください。
http://localhost:3333/
web_result.jpg


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}
    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を指定します。

    実行結果は画像のようになります。
tweet_result.jpg

#アプリケーションのデプロイ

###Herokuの登録・Heroku CLIのインストール
下記ページを参考にHerokuの登録・Heroku CLIのインストールを行ってください。
【Heroku】WindowsでHerokuを使ってPythonアプリをデプロイするメモ【Python】

###Herokuアプリケーションの作成

下記コマンドでHerokuにログインします。

command
heroku login

次に、下記コマンドでアプリケーションを作成します。
今回はtest_appという名前でアプリケーションを作成します。

command
heroku create test_app

アプリケーションを作成後作業ディレクトリに移動し、Gitリポジトリの作成とリモート接続を行います。
(今回は作業ディレクトリをCドライブ直下に作成したと仮定します)

command
cd /C/test
git init
heroku git:remote -a test_app

###設定ファイルの作成
下記設定ファイルを作成します。

  • runtime.txt

    Pythonのバージョンを記載します。

    Herokuがサポートしているバージョンを記載してください。
runtime.txt
python-3.7.9

ローカル環境のPythonのバージョンは下記コマンドで確認できます。

command
python --version
  • requirements.txt

    インストールするモジュールを記載します。
requirements.txt
Flask==1.1.2
gunicorn==20.0.4
requests==2.25.0
requests-oauthlib==1.3.0

ローカル環境にインストールされたモジュール一覧は下記コマンドで確認できます。
インストールされたモジュールがすべて表示されるので、必要なモジュールのみ記載します。

command
pip freeze
  • Procfile

    アプリケーションを起動するために実行するコマンドを記載します。
Procfile
web: gunicorn test_app:app --log-file -

###デプロイ
下記コマンドを実行します。

command
git add .
git commit -m "First deployment."
git push heroku master

コマンドgit push heroku mastermasterはご自身の環境に合わせて適宜読み替えてください。

###トラブルシューティング
デプロイ時に発生したエラーの原因とその解決方法を以下にまとめました。

  • buildpackエラー

    エラーログ:remote: ! No default language could be detected for this app.

    原因:buildpackの設定漏れ

    解決方法:下記コマンドを実行し、ビルドパックを設定します。

    参考:herokuで悩んだところ
command
heroku buildpacks:set heroku/python
  • Procfileが見つからない

    エラーログ:remote: No Procfile detected, using the default web server.

    原因:ファイル名を間違えて「Profile」としていたため。

    解決方法:ファイル名をProcfileにリネームした。

    (補足)

    Procfile拡張子はありません
    テキストエディタ等でファイルを作成すると、特に指定が無ければテキスト形式(.txt)で作成されるため注意が必要です。

    下記コマンドでファイルを作成すると無難です。
command
echo "起動コマンド" > Procfile

###環境変数の設定
下記コマンドでアプリケーションで使用する環境変数を設定します。
設定値は"Twitter API keyの取得"で取得した値を使用してください。

command
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アプリにアクセスし、アプリが正常に動作していることを確認します。

command
heroku open

###ログの表示
下記コマンドでコンソール上にログを表示することができます。
-tはログをストリーミング状態で表示するためのオプションです。

command
heroku logs -t

#おまけその1:画像付きでツイートする
common_func.pyを以下のように修正します。

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"]))

実行結果は画像のようになります。
image_tweet_result.jpg

#おまけその2:リプライツイートに返信する
common_func.pyを以下のように修正します。

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を以下のように修正します。

tweet_contents.json
{
  "contents":
  {
    "img_url" : "画像URL",
    "tweet_text" : "@XXXXXXXX 返信のテスト"
  }
}

tweet_textの文頭に@XXXXXXXX を追加します。
XXXXXXXXには、返信したいアカウントのユーザ名(スクリーンネーム)を記載してください。

実行結果は画像のようになります。
reply_tweet_result.jpg

0
0
1

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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?