【Lambda】毎朝天気を教えてくれるLINEBot！【サーバーレス】

# 前提

- **AWSアカウントを持っている**
- AWSの基礎知識
- Pythonの基礎知識
- APIの基礎知識

# 作ったもの

天気を取得し毎朝7時にLINEに通知するbot！
※ 母が「毎朝天気知りたい」と言っていたので作成することにしました。

<img width="457" alt="weather_forecast_LINE_bot.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549894/2ac176ec-8b23-86bc-5ea0-4deb80b974b9.png">
※ テスト中の画像

# 技術選定（この記事で学べること）

- 天気を取得し → OpenWeatherMapのAPI
- 毎朝7時に → CloudWatch Events + Lambda
- LINEに通知する → LINE Notify

# いざ、実装

実装上、APIキーやアクセストークンの取得が先になるため、説明の順番が前後します。説明の順番は以下の通りです。

1. OpenWeatherMapのAPIによる天気の取得
2. LINE NotifyによるLINEへの通知
3. Lambda関数の作成
4. Lambda関数の編集
5. CloudWatch Eventsの設定

### 天気の取得

OpenWeatherMapのAPIの利用には、OpenWeatherMapに登録することで得られるAPIキーが必要！

1. **[ここ](https://home.openweathermap.org/users/sign_up)**から登録
2. サインインして`API keys`タブでAPIキーを確認
3. あとで必要となるのでコピーしておく

参考：[無料お天気APIで簡単スクレイピング](https://qiita.com/Brutus/items/861a165a211b3963719d)

### LINEへの通知

LINE Notifyの利用には、LINE Notifyにログインすることで得られるアクセストークンが必要！

また、LINE Notifyと友達になっておく必要があります。
（このLINE Notifyからメッセージが送られてくる。）

1. [LINE Notify](https://notify-bot.line.me/ja/)にログインしマイページへ
2. ページ下部の`トークンを発行する`からトークンを発行
3. あとで必要となるのでコピーしておく

参考：[PythonでLINEにメッセージを送る](https://qiita.com/moriita/items/5b199ac6b14ceaa4f7c9)

### Lambda関数の作成

AWSマネジメントコンソールから`Lambda`を開き、 `関数の作成` → `一から作成`。
以下のように設定し、`関数の作成`。

- 関数名：**weather_forecast_LINE_bot**
- ランタイム：Python 3.8
- アクセス権限：`AWS ポリシーテンプレートから新しいロールを作成`
  - ロール名：**lambdaLogRole**
  - ポリシーテンプレート：`基本的な Lambda@Edge のアクセス権限`

※ `基本的な Lambda@Edge のアクセス権限`にはCloudWatchへのアクセス権限が含まれていて、これをアタッチしておくことでCloudWatchにログを記録できるようになります。（例えば`print`等の標準出力はCloudWatchにログとして記録されます。）

参考：[API Gateway + LambdaでREST API開発を体験しよう [10分で完成編]/Lambda関数の作成](https://qiita.com/tamura_CD/items/46ba8a2f3bfd5484843f#-lambda%E9%96%A2%E6%95%B0%E3%81%AE%E4%BD%9C%E6%88%90)

### Lambda関数の編集

説明上、先にソースコードをお見せします。ソースコードは以下の通りです。

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

```python:lambda_function.py
from bs4 import BeautifulSoup
import datetime as dt
import requests


def send_line_a_(message):
    URL = "https://notify-api.line.me/api/notify"
    ACCESS_TOKEN = "<コピーしておいたあなた自身のアクセストークン>"
    HEADERS = {"Authorization": "Bearer " + ACCESS_TOKEN}
    requests.post(
        URL,
        headers=HEADERS,
        params={"message": message},
    )


def get_weather_in_(region):
    CORE_URL = "http://api.openweathermap.org/data/2.5/weather"
    REGION = f"?q={region},jp"
    CUSTOM = "&units=metric&lang=ja&mode=xml"
    API_KEY = "&appid=<コピーしておいたあなた自身のAPIキー>"
    URL = CORE_URL + REGION + CUSTOM + API_KEY
    response = requests.get(URL)
    return response.text


def utc2jst_and_parse(lastupdate):
    timestamp_utc = lastupdate[:10] + " " + lastupdate[11:]

    datetime_utc = dt.datetime.strptime(timestamp_utc + "+0000", "%Y-%m-%d %H:%M:%S%z")
    datetime_jst = datetime_utc.astimezone(dt.timezone(dt.timedelta(hours=+9)))
    timestamp_jst = dt.datetime.strftime(datetime_jst, '%Y-%m-%d %H:%M:%S')

    year = timestamp_jst[:4] + "年"
    month = timestamp_jst[5:7] + "月"
    day = timestamp_jst[8:10] + "日"
    hour = timestamp_jst[11:13] + "時"
    minute = timestamp_jst[14:16] + "分"

    return year + month + day + " " + hour + minute


def parse_of_(weather_data):
    soup = BeautifulSoup(weather_data, "html.parser")

    city_name = soup.find("city").get("name")
    min_temp = soup.find("temperature").get("min")
    max_temp = soup.find("temperature").get("max")
    clouds = soup.find("clouds").get("name")
    weather = soup.find("weather").get("value")
    lastupdate = soup.find("lastupdate").get("value")
    date = utc2jst_and_parse(lastupdate)

    message1 = f"\n{date}\n現在の{city_name}の気象情報を\nお知らせします。"
    message2 = f"\n最低気温: {min_temp}度\n最高気温: {max_temp}度"
    message3 = f"\n空模様: {clouds}\n天気: {weather}"
    return [message1, message2, message3]


def lambda_handler(event, context):
    weather_data = get_weather_in_("<気象情報を知りたい場所>")
    messages = parse_of_(weather_data)
    for message in messages:
        send_line_a_(message)
    return {"statusCode": 200, "body": {}}
```
</div></details>

※ <気象情報を知りたい場所>は「 http://bulk.openweathermap.org/sample/city.list.json.gz 」からDLできるJSONに記載がある場所ならどこでも大丈夫です。（世界各地の天気を知れるなんてすごいAPIだなぁ）

さて、Lambdaでは通常、外部ライブラリ（今回でいう`bs4`、`requests`）の`import`はできません。標準ライブラリ（今回でいう`datetime`）の`import`は問題なく行えます。

Lambdaで外部ライブラリを使用したい場合、外部ライブラリを実行ファイル（`lambda_function.py`）と一緒にzipして、`アクション ▼`から`.zip ファイルをアップロード`をクリックしてアップロードする必要があります。

```
for_upload
    ┣━ bs4
    ┣━ certifi
    ┣━ chardet
    ┣━ idna
    ┣━ urllib3
    ┣━ requests
    ┣━ soupsieve
    ┗━ lambda_function.py
```
とにかくLambda上でこのようなディレクトリ構造になっていればOKです。

参考：[【Python】AWS Lambdaで外部モジュールを使用する](https://qiita.com/SHASE03/items/16fd31d3698f207b42c9)

### CloudWatch Eventsの設定

AWSマネジメントコンソールから`CloudWatch`を開き、 左ペインの`イベント` → `今すぐ始める`から以下のように設定し、`詳細の設定`。

- イベントソース：スケジュールでCron式は`0 22 * * ? *`と設定
- ターゲット：`+ ターゲットの追加 *`から先ほど作ったLambda関数（**weather_forecast_LINE_bot**）を指定

遷移先で以下のように設定し、`ルールの作成`。

- 名前：**EveryMorning7**
- 説明：毎日7時にトリガー

参考：[Amazon CloudWatch Events で cron 式を使う場合は時差に気をつける/具体例](https://qiita.com/niwasawa/items/6e0b191d6aa21213c7ec#%E5%85%B7%E4%BD%93%E4%BE%8B)

# 完成！

これにて実装完了となります、お疲れ様でした！
ボリュームを抑えるために、ところどころ説明を端折っており、少々分かりづらいかも知れません。
ご感想、ご質問等があればぜひコメントをお寄せください😊