台湾の地震情報をLINEに通知する

# はじめに
つい最近、台湾で大きな地震がありましたね。
一人でも多くの無事と一日も早い復興を願ってます。

さて、今回は台湾の最新の地震情報を取得できるAPIについてご紹介したいと思います。
台湾には**中央気象署**という気象業務を行う公的機関があり、日本でいう気象庁のような役割を果たしています。

https://www.cwa.gov.tw/V8/C/

中央気象署は、台湾国内の最新の気象情報を入手できる**オープンデータ**を提供しており、**会員登録**をすれば<INS>誰でも無料で</INS>利用することができます。

まずは会員登録を済ませましょう。

# 中央気象署会員になる

https://pweb.cwa.gov.tw/emember/register/authorization

1\. 上記のサイトにジャンプし、利用規約を確認したら「同意」をクリックします。
![regi.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3720276/e7f07243-7fcb-a6ab-874b-bed21e493e1a.png)
2\. 登録情報を入力します。 Google翻訳を掛ければ読めると思いますが、下記に解説します。
* **電子郵件** - メールアドレス
* **密碼** - パスワード（6文字以上、12文字以下）（スペース、「+」「%」「&」は使用不可）
* **再次輸入密碼** - パスワードの再入力
* **訂閱** - 受け取りたいメールの種類をチェック
  * **每日氣象電子報** - 天気日報
  * **天氣警、特報** - 気象警報
  * **地震報告** - 地震情報

そして最後の「**驗證碼**」が確認コードです。
全て入力し終わったら「送出」を押しましょう。

:::note warn
パスワードを自動生成に頼っていて、パスワード欄を<INS>コピー&ペーストで入力した場合</INS>は認証に失敗する場合があります。
最後まで入力したのに「驗證碼失效」が出る場合は、パスワードを**手入力**してみてください。または、別のパスワードも試してみてください。
:::

3\. この段階では仮会員です。
入力したメールアドレス宛に確認メールが届くので、URLをクリックします。


これで本登録は完了です！

# アクセスキーの発行
APIにアクセスするには、まずアクセスキーを入手する必要があります。

https://opendata.cwa.gov.tw/index

1\. 上記のオープンデータ用HPにアクセスし、右上の「登入/註冊」をクリックします。
2\. 「**氣象會員登入**」を選択し、<INS>メールアドレス</INS>と<INS>パスワード</INS>を入力、最後に<INS>CAPTCHAチェック</INS>してログインします。
3\. 自動でアクセスキーを入手するページに飛ぶので、「**取得授權碼**」をクリックして、アクセスキーを発行します。右に出てくる「CWA-」で始まる文字がアクセスキーです。

![スクリーンショット 2024-04-06 10.38.19.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3720276/7e1a43b3-4c21-565d-df37-7a34baab90c3.jpeg)

発行したアクセスキーは忘れずにメモしておきましょう。

:::note
もしアクセスキーを紛失した場合は、「**更新授權碼**」で再発行できます。
しかし発行済みのアクセスキーは無効となる上、7日間は再発行できなくなるので、注意しましょう。
:::

# APIにアクセスする

https://opendata.cwa.gov.tw/dataset/earthquake?page=1

1\. 上記リンクをクリックすると、地震津波情報のオープンデータ一覧が確認できます。
今回は震度3級以上の地震情報を取得したいので、「**顯著有感地震報告**」を選択します。
2\. 「資料擷取API服務說明網址」項目の「**API**」をクリックします。

![スクリーンショット 2024-04-06 11.01.47.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3720276/0cead087-f16f-60ff-fe97-5547a940ee01.jpeg)

3\. Swaggerの選択した項目にジャンプしました。右に「**Try it out**」をクリックします。
4\. 各パラメータを入力します。一番上の「**Authorization**」が必須項目で、先ほど入手したアクセスキーをコピペしておきます。
5\. 最後に「**Execute**」をクリックすると結果が返されます。あとは煮るなり焼くなり好きにしましょう。

![スクリーンショット 2024-04-06 11.05.11.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3720276/49fc4d4e-6fec-76f7-a5a6-dcb764b1323c.jpeg)


# LINEに通知する

https://notify-bot.line.me/ja/

**LINE Notify**を使って通知します。

<details><summary>ソースコード</summary>

```Python
pip install requests
pip install googletrans==3.1.0a0
```

```Python
import requests
import time
import datetime
from googletrans import Translator
import re
import os
import urllib.request


def cst_to_jst(dt):  # 台湾時間から日本時間への変換
    cst = datetime.datetime.strptime(dt, "%Y-%m-%d %H:%M:%S")
    jst = cst + datetime.timedelta(hours=1)
    return jst


def translate(str_zh):  # 翻訳
    translator = Translator()
    trans_jp = translator.translate(str_zh, dest="ja")
    return trans_jp.text


def notifysend(message, img):  # 通知系APIへのPOST
    notify_url = "https://notify-api.line.me/api/notify"
    if img:
        with open(img, "rb") as f:
            r = requests.post(notify_url,
                              headers={"Authorization": "Bearer " + token},
                              data={"message": message},
                              files={"imageFile": f.read()},
                              params={"notificationDisabled": True}
                              )
    else:
        r = requests.post(notify_url,
                          headers={"Authorization": "Bearer " + token},
                          data={"message": message},
                          params={"notificationDisabled": True}
                          )
    return r


def notify_postlimit(p):  # APIレート制限確認用
    ratelimit = p.headers.get("X-RateLimit-Limit")
    ratelimit_re = p.headers.get("X-RateLimit-Remaining")
    imgratelimit = p.headers.get("X-RateLimit-ImageLimit")
    imgratelimit_re = p.headers.get("X-RateLimit-ImageRemaining")
    ratereset = p.headers.get("X-RateLimit-Reset")
    reset_time = int(ratereset) - int(time.time())
    m, s = divmod(reset_time, 60)

    print(f"LINE Notify送信完了。 {datetime.datetime.now()}")
    print(f"残りAPIコール回数：{ratelimit_re} / {ratelimit}")
    print(f"残りImage Upload回数；{imgratelimit_re} / {imgratelimit}")
    print(f"上限リセットまで、あと{m}m{s}s\n")

token = input("LINE Notifyのトークンを入力")
checkNum = 0

url = "https://opendata.cwa.gov.tw/api/v1/rest/datastore/E-A0015-001?Authorization={Authkey}&limit=1&format=JSON&AreaName="
jsonData = requests.get(url).json()

eqNo1 = jsonData["records"]["Earthquake"][0]["EarthquakeNo"]

while True:
    time.sleep(10)
    jsonData = requests.get(url).json()
    eqNo2 = jsonData["records"]["Earthquake"][0]["EarthquakeNo"]

    if eqNo2 != eqNo1:
        eqNo1 = eqNo2
        checkNum += 1
        print(f"EarthQuakeNoの変化を検知。メッセージを送信します。 日時：{datetime.datetime.now()} 回数：{checkNum}\n")

        reportType = jsonData["records"]["Earthquake"][0]["ReportType"]
        reportColor = jsonData["records"]["Earthquake"][0]["ReportColor"]  # 程度の強さ
        # 色の区分は仮のもの。エラーが出たら疑うべき
        if reportColor == "綠色":  # 下の基準に満たない
            eqDeal = ""  # 表現の度合い
            expression = "。"
        elif reportColor == "黃色":  # M5.5かつ震度4級以上
            eqDeal = "やや強い"
            expression = "。"
        elif reportColor == "橙色":  # M6.0かつ震度5弱以上
            eqDeal = "強い"
            expression = "！"
        elif reportColor == "紅色":  # M6.5かつ震度6弱以上
            eqDeal = "非常に強い"
            expression = "！！"
        else:
            raise ValueError("想定外のreportColor")

        riURI = jsonData["records"]["Earthquake"][0]["ReportImageURI"]  # 震度地図
        imgName = riURI.split("/")[-1]
        os.makedirs("eqmap", exist_ok=True)
        dl_path = f"./eqmap/{imgName}"
        urllib.request.urlretrieve(riURI, dl_path)

        eqInfo = jsonData["records"]["Earthquake"][0]["EarthquakeInfo"]

        originTime = eqInfo["OriginTime"]  # 発生時刻
        otFormat = f"{cst_to_jst(originTime):%d日 %H時%M分頃、}"  # フォーマット済み発生時刻（JST）

        source = eqInfo["Source"]  # 情報元

        epiCenter = eqInfo["Epicenter"]["Location"]  # 震源地
        epiCenter = translate(epiCenter)

        depth = str(eqInfo["FocalDepth"]) + "km"  # 深さ

        mType = eqInfo["EarthquakeMagnitude"]["MagnitudeType"]  # マグニチュード
        mValue = eqInfo["EarthquakeMagnitude"]["MagnitudeValue"]
        if mType == "芮氏規模":  # ローカルマグニチュード
            mUnit = "ML"
        else:
            mUnit = "M"

        shakingArea = jsonData["records"]["Earthquake"][0]["Intensity"]["ShakingArea"]
        shakingArea = sorted(shakingArea, key=lambda a: a["AreaIntensity"], reverse=True)
        maxInt = shakingArea[0]["AreaIntensity"]
        skaList = []

        for ska in shakingArea:
            if re.findall("最大震度.*地區", ska["AreaDesc"]):
                areaInt = ska["AreaIntensity"]
                county = translate(ska["CountyName"])
                res = f"【震度{areaInt}】\n{county}"
                skaList.append(res)

        mainHead = f"\n【🇹🇼CWA{reportType}】\n{otFormat}台湾で{eqDeal}地震があったピヨ{expression}"
        mainBody = f"震源地：{epiCenter}\n震源の深さ：{depth}\n最大震度：{maxInt}\n推定マグニチュード：{mUnit}{mValue}"
        mainFoot = f"ソース：{source}"

        mainAns = f"{mainHead}\n\n{mainBody}\n\n{mainFoot}"
        notifysend(mainAns, dl_path)

        skaHead = "\n各地の震度は次の通りピヨ🐤"
        skaBody = "\n".join(skaList)

        skaAns = f"{skaHead}\n\n{skaBody}"
        post = notifysend(skaAns, None)
        notify_postlimit(post)

    else:
        checkNum += 1
        print(f"更新完了。 更新日時：{datetime.datetime.now()}　回数：{checkNum}\n")

```

</details>

![IMG_2320-3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3720276/ac94122c-d8e9-c839-297e-8039e0dca43c.png)


# 最後に
気象庁の防災情報もJSON化してほしい（切実）

# 出典
* [交通部中央氣象署](https://www.cwa.gov.tw/V8/C/)
* [【Python】Google翻訳APIを使って、自動翻訳機能を追加してみた](https://yonesuke0716.hatenablog.com/entry/2023/02/07/112154)