厚生労働省オープンデータからCOVID-19 PCR検査陽性者数をツィートするDockerコンテナを作ってみた

# この記事の目的

厚生労働省が公開しているオープンデータをフェッチして、今日のPCR
PCR検査陽性者数をTwitterにツィートするDockerコンテナを作成します。

# 開発・実行環境
* Windows ホストPC
  * エディション：Windows 10 Home
  * バージョン：20H2
  * OS ビルド：19042.867
  * エクスペリエンス：Windows Feature Experience Pack 120.2212.551.0
* Docker Desktop
  * 3.2.2
* Docker
  * 20.10.5 build 55c4c88

# 厚生労働省のオープンデータをフェッチする

## データをフェッチするスクリプトを開発する

厚生労働省のオープンデータの保存場所を調査します。このページを眺めるとオープンデータは"https://www.mhlw.go.jp/content/"にcsv形式で保存されていることがわかります。

https://www.mhlw.go.jp/stf/covid-19/open-data.html

Pythonのrequestsモジュールを使ってオープンデータをフェッチしましょう。今回はこんなコードを書きました。

```python:mhlw/uri_list.py
# coding: UTF-8

uri_list = [
    'pcr_positive_daily.csv',
    'pcr_tested_daily.csv',
    'cases_total.csv'
]

if __name__ == '__main__':
    from sys import stdout

    for uri in uri_list:
        stdout.write(f'{uri}\n')
```

オープンデータはもっとあるのですが、今回は`uri_list`で格納した３ファイルをフェッチします。実際に使うのは`pcr_positive_daily.csv`だけです。

```python:mhlw/fetch.py
# coding: UTF-8

if __name__ == '__main__':
    from sys import argv, path
    from urllib.parse import urljoin

    path.append('http')

    import http_client

    endpoint_url = argv[1]
    uri_list = argv[2:]

    for uri in uri_list:
        url = urljoin(endpoint_url, uri)
        http = http_client.HttpClient(url)
        res = http.sync_get()
        
        with open(uri, 'w') as f:
            f.write(res)
```

```python:http/http_client.py
# coding: UTF-8

class HttpClient:
    def __init__(self, url):
        self._url = url

    def sync_get(self):
        import requests

        self._res = requests.get(self._url)

        body = None
        if self._res.status_code == 200:
            body = self._res.text
        else:
            raise RuntimeError('The http request is failed.')
        
        return body
```

次のように実行しました。lsコマンドを実行すると3つのcsvファイルがフェッチできていることがわかります。

```bash
(base) root@251c769e696d:/mnt/docker/anaconda3/covid-19_mhlw_open_data# python mhlw/uri_list.py | xargs python mhlw/fetch.py "https://www.mhlw.go.jp/content/"
(base) root@251c769e696d:/mnt/docker/anaconda3/covid-19_mhlw_open_data# ls
http  mhlw  pcr_positive_daily.csv  pcr_tested_daily.csv
```

もともとのcsvの文字コードがutf-8ではないため（たぶんShift-JISかな）、ヘッダの日本語が文字化けしちゃいました。今回は気にせずこのまま進めます。

## 実行するDockerfileを作成する

次に開発したスクリプトを動作させるDockerコンテナを作ります。

```dockerfile:Dockerfile
FROM python:3.9.4-slim-buster

ENV WORKDIR_NAME "/app"
ENV DIR_HTTP "http"
ENV DIR_MHLW "mhlw"
ENV ENDPOINT_URL "https://www.mhlw.go.jp/content/"

WORKDIR $WORKDIR_NAME

RUN mkdir $DIR_HTTP
RUN mkdir $DIR_MHLW

COPY ./${DIR_HTTP}/*.py ${WORKDIR_NAME}/${DIR_HTTP}/
COPY ./${DIR_MHLW}/*.py ${WORKDIR_NAME}/${DIR_MHLW}/

RUN pip install requests

CMD python ${DIR_MHLW}/uri_list.py | xargs python ${DIR_MHLW}/fetch.py ${EHLW_ENDPOINT_URL}
```

ビルドして実行します。今回は期待値が得られているか確認したいので、次のコマンドでイメージのビルド、コンテナの作成・実行を行います。

```powershell
PS > docker build --tag=covid19 .
PS > docker run covid19
```

残念ながらこのままだとPythonスクリプトは期待通りに動作しません。なぜならDockerコンテナからエンドポイントのドメイン `mhlw.go.jp` の名前解決ができないからです。この辺はDockerコンテナ・ネットワークを解説しないとうまく説明できないので別記事で解説しようと思いますが、一言で説明するとDockerコンテナが属しているネットワーク・サブネットとホストPCが属しているネットワーク・サブネットが異なります。なのでコンテナが属しているネットワークからインターネットに直接アクセスできません。

余談ですが、デバッグしたい場合はコンテナにインタラクティブモードで接続するのがおススメです。

```powershell
PS > docker run --interactive --tty covid19 bash
```

## コンテナからホスト経由でインターネットにアクセスする

真面目に対応するのであれば、ホストPCのNATの設定を変更したりするのでしょうが（すみません、ちゃんと調査していません）、今回は開発環境兼実行環境のWindowsホストPCで実行するので、Dockerコンテナを`host`ネットワークに紐づけて実行します。

こんな感じで実行しました。

```powershell
PS > docker run --net host covid19
cases_total.csv
fetch.py
pcr_positive_daily.csv
pcr_tested_daily.csv
uri_list.py
PS >
```

`host`ネットワークって何ぞや、ということですが、簡単に説明しますとdockerをインストールするとデフォルトで3つの仮想ネットワークを作成します。そのうちの１つが`host`ネットワークでDockerエンジンが動作しているホストPCと同じサブネットの仮想ネットワークにコンテナが接続されます。Dockerの仮想ネットワークは`docker network ls`コマンドで確認できます。

```powershell
PS > docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
e9f868af4a34   bridge    bridge    local
4b2fd62d429a   host      host      local
b475f904ff07   none      null      local
```

詳しくは公式ページをご覧ください。僕もそのうち解説します。（３年くらいにすごくまじめに調べた記憶があるのですが、今はうるおぼえ故に。すみません。）

## 補足

オープンデータのフェッチのためにPythonスクリプト書いたけれども、curlでもよかったなぁ。

# 今日のPCR検査陽性者数を求める

## PCR検査陽性者数を分析するスクリプトを開発する

まずは分析のスタートポイントを実装します。

```python:mhlw/analysis.py
# coding: UTF-8

scripts = {
    'pcr_positive_daily.csv': 'analysis_pcr_positive_daily.py',
    'pcr_tested_daily.csv' : None,
    'cases_total.csv' : None
}

if __name__ == '__main__':
    from sys import argv, path
    from os.path import dirname, join
    from subprocess import Popen, PIPE

    import binascii

    file_name = argv[1]
    script = scripts.get(file_name)

    if script != None:
        data = None
        with open(argv[1], 'rb') as input:
            script = join(dirname(argv[0]), script)
            with Popen(['python', script], stdin=input, stdout=PIPE) as pipe:
                data = pipe.stdout.read()

        res = data.decode(encoding='utf-8')
        print(f'\"{res}\"')
```

次に`pcr_positive_daily.csv`を解析して、本日のPCR検査陽性者数を分析するスクリプトを作成します。Pandaを使えば簡単に詳細な解析できると思いますが、今回は割愛します。（Pandaはちょっと触ったぐらいなので今後調査したいです。）

```python:analysis_pcr_positive_daily.py
# coding: UTF-8

if __name__ == '__main__':
    from sys import argv, path
    from sys import stdin, stdout
    from csv import reader
    from datetime import datetime

    data = list(reader(stdin))
    header = data[:1][0]
    body = data[1:]

    now = datetime.now()
    key = f'{now.year}/{now.month}/{now.day}'

    item = [ item for item in body if item[0] == key ]

    stdout.write(f'As of {key}, the number of positive PCR tests in Japan is {item[0][1]}.')
```

開発環境で実行します。こんな感じです。

```bash
(base) root@251c769e696d:/mnt/docker/anaconda3/covid-19_mhlw_open_data# ls -1 *.csv | xargs -l python mhlw/analysis.py
"As of 2021/4/15, the number of positive PCR tests in Japan is 4570."
(base) root@251c769e696d:/mnt/docker/anaconda3/covid-19_mhlw_open_data#
```

## Dockerfileを更新する

開発環境で動作したのでDockerfileを更新して動作確認をします。

```dockerfile
FROM python:3.9.4-slim-buster

ENV WORKDIR_NAME "/app"
ENV DIR_HTTP "http"
ENV DIR_MHLW "mhlw"
ENV ENDPOINT_URL "https://www.mhlw.go.jp/content/"

WORKDIR $WORKDIR_NAME

RUN mkdir $DIR_HTTP
RUN mkdir $DIR_MHLW

COPY ./${DIR_HTTP}/*.py ${WORKDIR_NAME}/${DIR_HTTP}/
COPY ./${DIR_MHLW}/*.py ${WORKDIR_NAME}/${DIR_MHLW}/

RUN pip install requests

CMD python ${DIR_MHLW}/uri_list.py | xargs python ${DIR_MHLW}/fetch.py ${EHLW_ENDPOINT_URL} && ls *.csv | xargs -l python ${DIR_MHLW}/analysis.py
```

`CMD`が長くなってきましたね。シェルスクリプトにまとめたほうが良いかもしれません。イメージからコンテナを作成して実行します。

```powershell
PS > docker build --tag=covid19 .
PS > docker run --net host covid19
"As of 2021/4/15, the number of positive PCR tests in Japan is 4570."
PS >
```

# ツィートする

本日の陽性者数が分析できたので最後にツィートします。Python Twitter Toolsを使います。

https://mike.verdone.ca/twitter/

`pip`でインストールできるようです。

``` bash
# pip install twitter
```

https://pypi.org/project/twitter/

今回はこんなコードを書きました。

```python:twitter/twitter_client.py
# coding: UTF-8

if __name__ == '__main__':
    from sys import argv, path
    from datetime import datetime
    from requests_oauthlib import OAuth1Session
    from twitter import *

    path.append('.')
    import runtime_env

    tweet = (argv[1]).replace('"', '')

    endpoint_url    = runtime_env.twitter_endpoint_url()
    token           = runtime_env.twitter_access_token()
    token_secret    = runtime_env.twitter_access_token_secret()
    consumer_key    = runtime_env.twitter_api_key()
    consumer_secret = runtime_env.twitter_api_secret_key()

    t = Twitter(auth=OAuth(token, token_secret, consumer_key, consumer_secret))
    t.statuses.update(status=tweet)
```

Twitterにツィートするにはアカウントのトークンが必要になりますが、スクリプトにコーディングするのは危険なので環境変数から読み込むようにしています。環境変数を読みだすスクリプトを記述しました。

```python:runtime_env.py
# coding: UTF-8

def twitter_endpoint_url():
    import os
    return os.environ['TWITTER_POST_ENDPOINT_URL']

def twitter_access_token():
    import os
    return os.environ['TWITTER_ACCESS_TOKEN']

def twitter_access_token_secret():
    import os
    return os.environ['TWITTER_TOKEN_SECRET']
    
def twitter_api_key():
    import os
    return os.environ['TWITTER_API_KEY']

def twitter_api_secret_key():
    import os
    return os.environ['TWITTER_API_SECRET_KEY']
```

Dockerfileを更新します。こんな感じで記述しました。

```Dockerfile
FROM python:3.9.4-slim-buster

ENV WORKDIR_NAME "/app"
ENV DIR_HTTP "http"
ENV DIR_MHLW "mhlw"
ENV DIR_TWITTER "twitter"

ENV EHLW_ENDPOINT_URL "https://www.mhlw.go.jp/content/"
ENV TWITTER_POST_ENDPOINT_URL "https://api.twitter.com/1.1/statuses/update.json"

ENV TWITTER_ACCESS_TOKEN "<アクセストークン>"
ENV TWITTER_TOKEN_SECRET "<アクセストークン・シークレット>"
ENV TWITTER_API_KEY "<APIキー>"
ENV TWITTER_API_SECRET_KEY "<APIキー・シークレット>"

WORKDIR $WORKDIR_NAME

RUN mkdir $DIR_HTTP
RUN mkdir $DIR_MHLW
RUN mkdir $DIR_TWITTER

COPY ./*.py ${WORKDIR_NAME}/
COPY ./${DIR_HTTP}/*.py ${WORKDIR_NAME}/${DIR_HTTP}/
COPY ./${DIR_MHLW}/*.py ${WORKDIR_NAME}/${DIR_MHLW}/
COPY ./${DIR_TWITTER}/*.py ${WORKDIR_NAME}/${DIR_TWITTER}/

RUN pip install requests
RUN pip install twitter
RUN pip install requests-oauthlib

CMD python ${DIR_MHLW}/uri_list.py | xargs python ${DIR_MHLW}/fetch.py ${EHLW_ENDPOINT_URL} && ls *.csv | xargs -l python ${DIR_MHLW}/analysis.py | xargs -0 python ${DIR_TWITTER}/twitter_client.py
```

イマージからコンテナを作成して実行します。こんな感じです。

```powershell
PS > docker build --tag=covid19 .
PS > docker run --net host covid19
PS >
```

Twitterのタイムラインを見るとツィートできていました。

![2021_04_17_17_37_34.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1278980/58f4c854-6f44-b6d9-5351-41583ab99a7f.png)

# まとめ

結構おおらかなスクリプトになってしまったので真面目に作るならもうちょいちゃんとしなきゃな、と思います。`Dockerfile` の `CMD` 行が長くなってしまったのでシェルスクリプトかPythonスクリプト化してもよいかもですね。

やっぱり厚生労働省の `csv` ファイルは `curl` 使えばよかったと思います。