電車の運行情報を通知してくれ

# はじめに
私は普段の通勤で湘南新宿ラインや埼京線を使用しています。
首都圏近郊では有名な話(だと思っているの)ですが、この電車とにかくよく遅れます。(あと混雑します)

朝通勤前に電車の遅延情報を乗り換えアプリなどで調べておくのができる社会人なのかもしれませんが、ズボラな私は大体駅に着いてから遅延を知ることになります。

### そうだ、ダイヤ乱れを通知する仕組みを作ろう。

# 要件
ざっくりとした要件は以下のようにしました。

### どうやって取得する？
無料で使える良い感じのAPIなど色々探したのですが、なかなかありませんでした。

結局運行情報を掲載しているHPからスクレイピングで取得することにしました。

https://transit.yahoo.co.jp/diainfo/area/4

※スクレイピングは実行の仕方によっては攻撃として扱われることもあるので、実施の際には自己責任にてお願いします

ちなみにこの記事の執筆時点で知りましたが、以下のような公式で運行状況をポストしてくれるXアカウントがあったんですね。リサーチ不足。
XのAPIとかでポスト内容取得できたりするんでしょうか。気が向いたら調べてみたいと思います。

https://x.com/jre_f_toho_taka?s=21&t=EtpVOR63km2OZgrQCAER4Q

### いつ取得する？
家を出るくらいの時間に通知されるようにしたいと思います。

### どこから取得する？
意外と悩んだのが実行環境です。
自宅のPCは電源が必ずついているわけではないので難しい。
常時稼働しているようなサーバーも持ち合わせていない。

仕方がないのでひとまずLambda関数で作ることにしました。EventBridge Schedulerにて定期実行させる形で動作させます。
が、結局実行環境変更しました。(後述します)

### なにで取得する？
スクレイピングができればなんでも良いのですが、今回はPythonでコードを作成します。

# 作成
概要は固まったのであとは実際に作っていくだけです。

## 通知先
まずは通知先であるSlackチャンネルの作成と、通知用Slackアプリの作成です。

自分だけに通知する想定なので、チャンネルの作成については割愛します。

また、Slack通知はWebhookを使用する方法もあるようですが、今は非推奨とのことでSlackアプリを作成する方法を選びました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581035/9fc7ec5e-46a2-4b56-9cf0-434e843e14a9.png)
manifestが定義ファイルでの作成、scratchが手作業での構築です。
マニフェストファイルの書き方を調べるのが面倒だったので、スクラッチで作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581035/25d80360-8230-472e-9d7e-a12f5e60132b.png)
左側メニューからOAuth & Permissionsを選択し、Bot Token Scopesに`chat:write`を選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581035/0b33b957-f918-4d4a-bf17-1bd62044f4df.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581035/7ec1eed8-ba59-4c74-949c-33e8881a1e5b.png)

作成したアプリをワークスペースにインストールして
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581035/9f4b7ebd-4d38-4acf-80fb-11fec80f3628.png)

Oauth tokenを控えておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581035/e07c939a-ceb4-4395-82cc-2e8e858e6293.png)
あとはお好みでアイコンなんかを設定してもいいかもしれません。

作成したアプリをチャンネルに招待しておきましょう。
対象のチャンネル内で`/`と打つとチャンネルへのアプリ追加のメニューが出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581035/944e9c8e-4701-4a63-ae74-d2ad799eccf2.png)


## Lambda関数作成
とりあえずコードはこんな感じ。

```python
import json
import os
import requests
from bs4 import BeautifulSoup

def lambda_handler(event, context):

    SHONAN_SHINJUKU = os.environ['SHONAN_SHINJUKU']
    SAIKYO_KAWAGOE = os.environ['SAIKYO_KAWAGOE']
    YOKOSUKA = os.environ['YOKOSUKA']
    MARUNOUCHI = os.environ['MARUNOUCHI']

    dia_url_list = [ SHONAN_SHINJUKU, SAIKYO_KAWAGOE, YOKOSUKA, MARUNOUCHI ]

    SLACK_URL = "https://slack.com/api/chat.postMessage"
    SLACK_OAUTH_TOKEN = os.environ['SLACK_OAUTH_TOKEN']

    headers = {
        "Authorization": f"Bearer {SLACK_OAUTH_TOKEN}",
        "Content-type": "application/json"
    }
    notifications = []

    for url in dia_url_list:
        # HTMLの取得(GET)
        req = requests.get(url, timeout=10)

        # HTMLの解析
        soup = BeautifulSoup(req.text,"html.parser")

        # 要素の抽出
        head = soup.find("head")
        description = head.find("meta", property="og:description")
        title = head.find("meta", property="og:title")

        # 遅延情報があった場合のみ表示
        if description:
            description_content = description['content']
            titel_content = title['content']
            line_name = titel_content.split()
            if "に関する情報はありません" not in description_content:
                notifications.append(f":train:{line_name[0]}\n {description_content}")
    if notifications:
        data = {
            "channel": "#info-train",
            "text": "\n\n".join(notifications)
        }
        response = requests.post(SLACK_URL, headers=headers, data=json.dumps(data), timeout=10)

        if response.status_code != 200:
            print(f"Slack通知失敗: {response.status_code}, {response.text}")
```

遅延がない時は何も通知してこないようにしています。
Slackの情報と対象路線のURLは環境変数を使用します。

ランタイムは3.12で、使用するライブラリのrequestsとbeautifulsoup4はインポートが必要なため、Lambdaレイヤーを作成します。
Lambdaレイヤーの作成方法も今度別記事でまとめたいと思っています。

タイムアウトだけ少し伸ばして、あとはデフォルトです。

## EventBridge Scheduler
EventBridge Schedulerのcron式はちょっと変わった書き方なので注意ですね。

https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-scheduled-rule-pattern.html

朝8:00の電車に乗ると仮定して、平日の朝7:40に通知するようにしました。

# 完成？
ひとまず完成しました。
EventBridge Schedulerで指定した時間になるとSlackに通知が来ます。

正直、Lambda関数の実行時間はたいしたことないですし、1日1回の実行なので課金額なんてたかが知れています。
とはいえ ~~ケチ~~ コスト意識が高い身としては節約したいところ。

そう思って調べたところ、一定範囲で実行環境を自由に使える環境があるようでした。

# Github Actions
そうGithub Actionsです。
CI/CDのトリガーとしてpushやPRを指定することが多いかなと思いますが、cron式でjobsを実行することもできるんですね。

やってみましょう。

# 見直し
まず実行環境が変わりました。
このためコードも当然ですがちょっと変更します。

環境変数で管理していたSlackのOauth tokenは、Githubのシークレットを利用して管理します。

さらに外部ライブラリはrequirements.txtに記載しておきます。

コードは以下に貼っておきます。

https://github.com/Yoshiya-H/train_dia/tree/main#

## Github Actionsをcronで動かす
ワーキングディレクトリ直下`.github/workflows/`に`cron-job.yml`を作成

```yaml
name: Scheduled Task

on:
    schedule:
        # 平日の毎日午前7:40に実行（UTCで記述）
        - cron: '40 22 * * 0-4'
    workflow_dispatch:

jobs:
    run-task:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v3

            - name: Set up Python
              uses: actions/setup-python@v4
              with:
                python-version: '3.12'

            - name: Create auth.json from secret
              run: echo "$AUTH_JSON" > auth.json
              env:
                AUTH_JSON: ${{ secrets.SLACK_AUTH }}          

            - name: Install dependencies
              run: |
                python -m pip install --upgrade pip
                pip install -r requirements.txt

            - name: Run script
              run: |
                python main.py
```

cron式はUTCなのでお気を付けください。
ポイントはシークレットに保存した`SLACK_AUTH`を呼び出して`auth.json`というファイルを作成している点でしょうか。

あとは必要な実行環境を整えてスクリプトを実行しているだけです。

# 実行
来ました！（アプリの名前は異なっています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3581035/15af5a40-687d-4c16-a627-3e5031197e74.png)

遅延情報なんて普段はうれしくもなんともないんですが、この時だけは少しうれしかったですね。
運行情報サイトの文言をほぼそのまま引っ張ってきている点や、実行回数など加えたい修正もいくつかありますが、とりあえずは形になったのでよしとします。

# さいごに
定期処理の実行環境としてGithub Actionsを使用できるというのが個人的には新しい発見でした。
無料利用枠の範囲での利用になりますが、普段個人開発でガツガツCI/CDを回していなければ余っている人も多いと思うので、選択肢の一つとしてはアリかなと思いました。