山梨県版 新型コロナウイルス感染症対策サイトのデータ更新の自動化


はじめに

Stay Home週間の期間で自分も昨今の状況に何か貢献したいと考え、
自身の地元である山梨県のコロナウィルス対策サイトのデータの自動更新にチャレンジしてみました。

作成したスクリプトのリポジトリはこちら↓
https://github.com/daisuke19891023/covid19-yamanashi-scraping

スクリプトの作成にあたり、以下の記事を参照させていただきました。
長野県版 新型コロナウイルス感染症対策サイト データ更新を自動化した話
※↑の記事を見かけ、自分も取り組もうと思いました・・・!この場を借りて感謝申し上げます!

スクリプトの概要

スクリプトの実行の流れは以下の通りです
1. GitHub Actionsにて定期的にスクリプトを実行
2. Pythonにて山梨県の新型コロナウイルス感染症に関する総合情報サイトから必要な情報をスクレイピング
3. スクレイピングしたPDFからサイト表示用のdata.jsonを作成
4. 山梨県版感染症対策サイトのrepository宛てにdispatch eventを送信
5. スクレイピングrepositoryからdata.jsonを取得し、pull requestを作成

Pythonによるデータ取得

山梨県は各種情報が県のサイトに公開されているので、requestsBeautifulSoupでスクレイピングを行いました。

PDFデータの読み取り

山梨県では個々の患者情報がPDFで公開されているため、PDFから情報の読み取りを行います。

PDFMiner.sixを使用し、PDFをテキストに変換したのち、テキストの内容を読み取り、データを作成しました。

※参考:【PDFMiner】PDFからテキストの抽出

テキスト変換の際、不要なスペースが入ってしまったり、全角半角が入り混じってしまったので、正規表現でのスペース削除と、mojimojiというライブラリで全角半角の統一を行いました。

参考:Pythonで半角・全角の変換を高速に行う

from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfpage import PDFPage
from io import StringIO
import glob
import os
import re
import mojimoji

for path in glob.glob('./pdf/*'):
    input_path = path
    output_file = os.path.splitext(os.path.basename(input_path))[0]
    output_path = os.path.join('./text', output_file + '.txt')

    rsrcmgr = PDFResourceManager()
    codec = 'utf-8'
    params = LAParams()
    text = ""
    with StringIO() as output:
        device = TextConverter(
            rsrcmgr, output, codec=codec, laparams=params)
        with open(input_path, 'rb') as input:
            interpreter = PDFPageInterpreter(rsrcmgr, device)
            for page in PDFPage.get_pages(input):
                interpreter.process_page(page)

            text += output.getvalue()
        device.close()
    output.close()
    text = re.sub(r' | ', '', text.strip())
    text = mojimoji.zen_to_han(text)
    # output text
    with open(output_path, "wb") as f:
        f.write(text.encode('utf-8', "ignore"))

HTMLテーブルデータの読み取り

疑似症例の検査件数、電話相談窓口の相談件数についてはHTMLのテーブル形式で記載されているので、そこから抽出しました。
難しい処理は不要で、BeautifulSoupfindによる要素抽出で必要な情報を取得することが出来ました。

※参考:10分で理解する Beautiful Soup

jsonの作成

上記で取得したデータを加工し、サイト表示用のdata.jsonを作成します。
※証跡管理のためdata.jsonもGitHubの管理対象としています

データが更新されたときのみdispatch eventを送信したいため、データの更新有無をチェックした上で、差分があった場合のみdata.jsonを更新するという仕様にしました。

注意点として、data.jsonを作成する際、あまりに内容が変わりすぎるとmergeする際にConflictが起きてしまいます。

連想配列は基本的には順序は担保されない認識でしたが、python3.7以降は標準で連想配列の順序は担保されるとのことでしたので、Conflictが起きない程度の更新に留まるようでした。

参考:pythonのdictのキーの順番の話

ここまでがpythonスクリプトの内容になります

GitHubActionsによる自動更新

スクレイピングrepositoryのymlファイル

ベースはGitHub Actionsのテンプレートから作成します。
ActionsタブからNew workflowを選択し、

Python ApplicationのSet up this workflowを選択します。

python自体とライブラリのインストールが記述されたymlとなりますので、これを修正することでお望みのActionを実行させることが出来ます。

実行タイミング

一時間ごとにGitHubActionsを定期実行します。cron形式で実行タイミングを記述できます。

on:
  schedule:
    - cron: "0 * * * *"

pythonスクリプトの実行

pythonスクリプトを実行し、data.jsonを更新します。
山梨県の公開情報はCSVではないため、規定外のデータが入りやすかったこともあり、そういったデータが入ってきた場合はここで失敗してくれます。
※間違った情報を出さないためには大事

DispatchイベントによるPull Requestの作成

以下のように記述することで、git statusで更新を確認し、更新があった場合にスクレイピングrepositoryへのコミット情報サイトへのdispatch eventの送信を行います。

- name: Commit files
run: |
  git config --local user.email secrets.EMAIL
  git config --local user.name "Scraping Bot"
  git status | grep modified && git add data.json && git commit -v -m "[Bot] GitHub Actions - auto run. Update at $(date +'%Y-%m-%d')" \
    && curl -X POST \
      -H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \
      -H "Accept: application/vnd.github.everest-preview+json" \
      -H "Content-Type: application/json" \
      https://api.github.com/repos/{repositoryのパス}/dispatches --data '{"event_type": "{イベント名}"}' \
    || true

secretsにはあらかじめメールアドレスとパーソナルアクセストークンを設定しておきます。
※上記手順は本家↓に詳しく記載されています
長野県版 新型コロナウイルス感染症対策サイト データ更新を自動化した話

なお、dispatchイベントを飛ばす際に、受信側repositoryのAdmin権限が必要とのことでしたが、今回はWrite権限のみでdispatch eventを飛ばすことが出来るようになりました。
参考:repository_dispatch response unpredictable

受信側のymlファイル

上記で発生させたdispatch eventを受信するために以下のようなymlを作成します。
起動条件のイベント名を送信側と合わせておけば、イベント送信を契機に起動させることが出来ます。

name: {Action Name}

on:
  repository_dispatch:
    types: [{イベント名}]

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v2

      - name: Copy data.json
        run: curl -o {保存先パス} https://raw.githubusercontent.com/{スクレイピングrepository}/master/{元データのパス}

      - name: Commit files
        run: |
          git config --local user.name "Scraping Bot"
          git status | grep modified && git add . && git commit -v -m "[Bot] GitHub Actions - update data.json at $(date +'%Y-%m-%d')"
      - name: Create Pull Request
        uses: peter-evans/[email protected]

このアクションが起動することでdata.jsonが更新された状態で、defaultブランチ宛てにpull requestが作成されます。

後は、管理者側で内容を確認した上でmergeしてもらうことで、サイトにデータ更新を反映させることが出来ます。

終わりに

GitHub Actionsを活用することで感染症対策サイトのデータ更新の自動化(正確には更新データの自動取得)を行いました。

今回初めてGitHub Actionsを使いましたが、ここまで簡単に使えるとは思っていなくて、感動しました。(CI環境の用意もいらず、しかも無料)

新型感染症に対する情報開示は各自治体ごとで方針やフォーマットは異なっているので、今回のスクリプトをそのまま流用できるわけではありませんが、少しでも参考になればと思っています。

積み残し

公開にあたり、以下の点が不十分なままでしたので、順次直していけたらと思います・・・

  • mypyによる型チェック
  • BeautifulSoupやrequestsなどのライブラリを用いたテスト(mockなどテストダブルが必要になる)
  • 命名など含めた全般的なリファクタリング