東京都の新型コロナ陽性者数をSlackに投稿（Herokuでデプロイ編）

[前回の記事][1]にて、[東京都の新型コロナウイルス陽性者数][2]をSlackに投稿するbotを紹介しました。

[1]:https://qiita.com/tanao/items/ef2748d7e38791ba3b62
[2]:https://catalog.data.metro.tokyo.lg.jp/dataset/t000010d0000000068

本記事はその続編です。

## 前回からの追加開発箇所

+ データが更新された後に自動でSlackに投稿
    + [前回][1]はCrontabで定期実行
+ Herokuでデプロイ
    + [前回][1]は自宅マシンで実行

以下のように、本日のデータ更新後に自動投稿されました。

![c19bot_28.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600317/63d7f9bb-dfe3-8005-c2db-40fd62e23f81.png)




## 開発1: データが更新された後に自動でSlackに投稿

[東京都がデータを公開しているページ][2]から取得したcsvデータを確認し、差分があれば投稿をするように実装しました。

[前回][1]作成した、Slackへの投稿をするメソッドを`fdap.main()`とします。
そして、`fdap.main()`を、[東京都のデータ][2]が更新された後に実行するソースコードを、`run2.py`として用意します。

`run2.py`の中身です。

```run2.py
import run as fdap
import post
import plot
import time
import datetime

def check_data(last_no):
    csv_tail_no = plot.fetch_csv().iat[-1,0] # "No"列の末尾行を取得
    if int(last_no) == 0:
        return False, csv_tail_no # initialize
    elif int(csv_tail_no) > int(last_no):
        return True, csv_tail_no
    else:
        return False, csv_tail_no

def main():
    while True:
        if "csv_tail_no" in locals(): # ループ2回目以降
            updated, csv_tail_no = check_data(csv_tail_no)
        else: # ループ1回目
            updated, csv_tail_no = check_data(0)
        if updated:
            fdap.main() # Slackに投稿
        time.sleep(60*5) # 5分

if __name__ == "__main__":
    main()
```

`check_data(last_no)`で、[東京都のデータ][2]の末尾行の`No`列を確認し、前回確認時と差分があれば、Slackへの投稿を実施するように処理を記述しています。

差分判定方法ですが、今回は実装のしやすさでひとまずこのような方法を取っていますが、ファイルの更新日時をスクレイピングするほうが、よりスマートかもしれません。
ただ、何度か[東京都のデータが公開されているページ][2]を見たのですが、最終更新日時とデータの更新タイミングが一致していないようにみえたため、更新日時をスクレイピングする以外の方法を取る必要がありそうだと感じました。

データの判定は、一定間隔で実行するように記述しています。データへのアクセス回数を必要以上に多くしないため、今回は適当に5分スリープを入れました。

## 開発2: Herokuでデプロイ

`python3 run2.py`を常に実行するため、実行環境をHerokuで用意します。

### 必要なファイルを用意

`runtime.txt`, `requirement.txt`, `Procfile` を用意します。

`runtime.txt`にはHerokuでデプロイする際のバージョンを記述します。

```txt:runtime.txt
python-3.6.10
```

`requirements.txt`には必要なモジュールを記述します。
`pip freeze > requirements.txt`で作成しました。

（明らかに不要そうなモジュールは手作業で消しましたが、まだ残っているかも…）

```txt:requirements.txt
jsonpatch==1.10
jsonpointer==1.9
python-dotenv==0.12.0
requests==2.9.1
slackbot==0.5.6
matplotlib==3.0.3
numpy==1.18.1
pandas==0.24.2
scipy==1.4.1
```

`Procfile`はプロセス名と実行したいコマンドを記述します。

``` txt:Procfile
c19bot: python3 run2.py
```

### 設定

Herokuのアカウント作成は事前に済ませておきます。

ブラウザからHerokuにサインインし、適当なアプリ名で新規アプリケーションを作成（画面右上のNew -> Create new appを選択）します。

Settingタブで、BuildpacksでPythonを選択します。
また、Config Varsで必要な環境変数（SlackのToken、チャンネル名など）を入力します。

残りの作業は、Heroku CLIでおこないます。
`Heroku login`でログインし、ソースコードをリモートに反映させたあと、`heroku scale [プロセス名]=1`でデプロイ完了です。

### 気を付けた点（環境変数）

ローカルで開発する際、環境変数は`.env`ファイルから読み込むように実装しています。

以下、ソースコードの例です。

``` python
from dotenv import load_dotenv
load_dotenv(dotenv_path=".env") # load local .env
import os

SLACK_TOKEN = os.environ["SLACK_TOKEN"] 
USER_NAME = os.environ["USER_NAME"]
SLACK_CHANNEL = os.environ["SLACK_CHANNEL"]
CHANNEL_ID = os.environ["CHANNEL_ID"]
PNG = os.environ["PNG"]
PNG_FILE = PNG+".png"
```

今回、Slackに投稿する画像（陽性者数のグラフ）を一時ファイルとして保存する必要があります。
画像名を`hist.png`とする場合、ローカルで開発する際は、`PNG="./hist"`のようにカレントディレクトリ上に保存できますが、Herokuでデプロイする際はファイル名を`"/tmp/hist"`と設定する（`/tmp`配下に保存する）ことで、うまく動作します。
なので、ローカルで開発する際にあらかじめ画像のファイル名も環境変数に加えておき、Herokuにてファイル名を別途指定しています。


### 気を付けた点（Dynoのライフサイクル）

Herokuのプロセスは、定期的に自動で再起動されるようです。

参考: https://jp.heroku.com/dynos/lifecycle (Heroku Dynoのライフサイクル)

当初、`python3 run2.py`の起動時に最新の情報をSlackへ一度投稿するようにプログラムを記述していました。
しかし、Herokuでデプロイした約1日後、なぜか[東京都のデータ][2]が更新されていないタイミングでも、Slackへ投稿してしまうというバグが発生しました。

調べた結果、Herokuのプロセスが自動で再起動された可能性に気づき、プログラムを修正しました。
現在は、`python3 run2.py`の起動時にはSlackへの投稿をせず、起動後に東京のデータが更新されたタイミングで初めてSlackへデータを投稿するという仕様にしています。

## まとめ

[前回の記事][1]の続きとして、東京都の新型コロナウイルス陽性者数をSlackに投稿するbotを自動化し、Herokuでデプロイ（常駐化）しました。

Slackへの投稿した情報はあくまでも補助的な情報としてとらえ、1日に1度は[東京都の公式サイトで公開されている都内の最新感染動向][3]を確認するように心がけたいと思います。

[3]:https://stopcovid19.metro.tokyo.lg.jp/

以上です。