バスハニャーン時刻表変更通知サービスの制作

*この記事は韓国語から翻訳したものです。不十分な部分があれば、いつでもフィードバックをいただければありがたいです！ ([オリジナル記事](https://blog.chungjungsoo.dev/dev-posts/pdf-email-cron-job/), 同じく私が作成しました。)*


バスハニャーンチームメンバーがより快適に時刻表の変更を知ることができるように、時刻表が変更されると通知するシステムを構築してみました。

## 導入背景

バスハニャーンサービスは漢陽大学ERICAシャトルバスの時刻表を提供するサービスです。しかし、私たちのサービスは時刻表を手動で入力した値を提供しています。APIでJSON値で提供してサービスに適用していますが、時刻表が変更されると、変更された時刻表を見て人が手動でAPIに入るJSON値を修正してくれます。これは学校側で提供する時刻表の形式のため自動化が難しい部分があるためですが、この部分は次の章で説明する予定です。 学校はまた、シャトルバスの時刻表が変更されることも特に告知をしないので、間違えると、シャトルバスの時間が変更されたにもかかわらず、サービスでは変更されず、学生に誤解を与えることができる部分でもあります。 このような問題を解決するため、時刻表が変更されるとお知らせするシステムを構築することにしました。


## シャトルバス時刻表の告知方法


![pdf_homepage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/c2849a00-1404-ef8b-ac26-5d4dbcf13713.png)


当校では、シャトルバスの時刻表を以下のように[ホームページ](https://www.hanyang.ac.kr/web/www/shuttle_bus_timetable)からPDFファイルを提供しています。PDFファイルをダウンロードすると以下のようになります。


![bus_timetable_pdf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/e626456c-8394-7727-58e7-0b37a84ccaa4.png)


まず、PDFの可読性が非常に悪いです...。画像には出てきませんが、PDF全体を確認してみると、最後にPDFが変更された日付が右上にあり、これによって時刻表が更新されたかどうかを確認することができます。しかし、これは学生が直接確認しなければならない部分なので、これを自動化する方法を見つけなければならないと思いました。

## 通知自動化の構築

私はまず、ハッシュを利用して時刻表の変更有無を検証することにしました。PDFファイルのハッシュを保存しておき、定期的にサイトのPDFハッシュをチェックし、変更された部分があればハッシュも変更されるので、ハッシュが変更されたら、ウェブフックを通じてバスハニャーンチームメンバーにメールを送るようにしました。 そのために次のような過程を経ました。

### Cron 作業用スクリプト

一定周期で運営される作業なのでCron作業で構築しました。PDFのハッシュをチェックし、変更された部分があれば通知するスクリプトを周期的に実行するようにしました。


```python
def main():
    BASE_URL = "https://www.hanyang.ac.kr"

    btn = find_download_button(f"{BASE_URL}/web/www/shuttle_bus_timetable")

    if not btn:
        # No button found
        print(f"Button not found error! - {get_current_time()}")
        sys.exit(1)
    
    pdf_path = parse_pdf_path(btn)

    if pdf_path == '':
        # Invalid url
        print(f"Invalid URL in button - {get_current_time()}")
        sys.exit(1)
    
    pdf_url = f"{BASE_URL}{pdf_path}"
    pdf_hash = get_pdf_hash(pdf_url)

    # Check previous hash
    API_URL = "<SECRET_API_URL>"

    response = requests.get(API_URL)
    prev_hash = response.text

    if pdf_hash == prev_hash:
        print(f"Same hash value. - {get_current_time()}")
        return
    
    # Hash different, update to new hash
    password = os.environ.get('API_PW')
    param = {
        "pw": password,
        "hash": pdf_hash
    }

    # Update Hash
    response = requests.post(API_URL, json=param)

    if response.status_code == 200:
        print(f"Hash updated. - {get_current_time()}")
        email_param = {
            "pw": password,
            "previous_hash": prev_hash,
            "new_hash": pdf_hash
        }

        # Send email
        email_response = requests.post("<SECRET_EMAIL_API_URL>", json=email_param)
        print(email_response.text)
    else:
        print(f"Hash update failed. Status: {response.status_code}")
        sys.exit(1)
```

上の告知方法画像で提供したように、PDFファイルのダウンロードボタンがありますが、これをPython bs4ライブラリを利用して探した後、PDFファイルのURLからPDFをダウンロードしてPDFのハッシュを取得します。その後、以前保存しておいたハッシュと比較して違う場合はハッシュをアップデートして、これをAPIを通じて保存します。その後、以前のハッシュと異なる場合、新しいハッシュをメールで送ってくれるAPIを呼び出します。

私たちバスハニャーンチームは別途サーバーを運営していません。 そのため、Cron作業が常に動くサーバーが必要でしたが、私たちは[GitHub Actions](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule)を利用してこれを実装しました。

```yaml
name:
 Cron Job - Timetable

on:
  schedule:
    # Schedule to every hour, 12:00AM - 10:00AM which is equivalent to 09:00AM - 07:00PM in KST.
    - cron: "0 0-10 * * *"
 
jobs:
  cron:
    runs-on: ubuntu-latest
    steps:
      - name: 🍽️ Checkout
        uses: actions/checkout@v3.4.0
        
      - name: 🐍 Set Python environment
        uses: actions/setup-python@v4
        with:
          python-version: '3.10'
          cache: 'pip' # Cache pip packages in GitHub Actions to speed up the process
          cache-dependency-path: '**/requirements.txt'
        
      - name: 🛠️ Install Requirements
        run: pip install -r requirements.txt
        
      - name: 🏃‍♂️ Run Python Script
        run: python parse.py
        env:
          API_TOKEN: ${{ secrets.API_PW }}
```

Cron作業は韓国時間基準午前9時から午後7時まで1時間に1回、毎日駆動するように設定しました。 これは、学校職員が平均的に勤務する時間帯以外には時間割が更新される可能性がほとんどないため、不要なAPI呼び出しを減らすためです。
また、GitHub Actionsでサポートする `cache` オプションを追加して毎時間Cron作業用のインスタンスが生成された後、キャッシュしておいた `pip` パッケージを追加して `pip install` が実行される時間を大幅に減らしました。


### APIサーバー構築

上のスクリプトを見ると、二つのAPIを使いました。 一つはハッシュを確認した後、ハッシュを更新するAPIで、もう一つはメールを送るAPIです。このAPIはバスハニャーンAPIと同じようにCloudflare Workersを利用して構築しました。Cloudflare WorkersではMailChannelsを利用して別途の設定なしで無料でメールを送ることができるので、簡単に構築することができました。 ([リンク](https://blog.cloudflare.com/sending-email-from-workers-with-mailchannels/))



## 結果


![cron_list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/ec33da04-70ae-6d1a-ee78-f208dc09048d.png)


上の画像を見ると、Cronタスクが正常に実行されることが確認できます。


![cron_log.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/96befe3c-9cff-2a76-c7fe-04ce5741223c.png)


また、Cronジョブが実行されたログを見ると、新しく生成されたインスタンスにもかかわらず、キャッシュされたpipパッケージの影響で実行時間がとても短いことが確認できます。変更されてないPDF値を確認した時は上記のように表示されます。


![timetable_email.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3896189/a4cabca9-ad4e-4053-a5b3-0f9a63d10f5d.png)


メールがうまく来るか確認もしてみました。 APIサーバーに保存されたハッシュ値を任意に変更し、Cronタスクを実行させた結果、メールが正常に送信されることを確認することができました。