体重管理アプリを作りながらLine botとAWSについて学ぶ -前編-

Line botの作成を行います。こちらは前編となっておりLine botとAWSのサービス(API Gateway, Lambda, S3, IAMによる権限管理)を使用して、体重管理botの仕組みを作成します。
2020.06.04

こんにちは、データアナリティクス事業本部の下地です。

コロナの影響でリモートワークになり4ヶ月目になりました。おかげさまで家で仕事をすることにも慣れてきました。慣れてくると在宅での仕事は移動がない分楽だなとも思える様になってきました。しかし、移動がないのは良いのですが運動不足を実感するとともに、体重の増加にも悩み始めました。毎日体重計に乗って確認はしているのですが簡易的に記録を取りたいなと思いましたので、日常的に使用しているlineとAWSのサービスを使用して下記図のようなイメージで体重管理のbotを作成したいと思います。

せっかく作成するのでAWSの様々な機能を使用したいなと思いましたので、前編・後編でまとめます。 前編で上図の仕組みを構築し、後編では記録した体重データを使用して体重推移の可視化やデータの暗号化などを試します。後半のリンクも載せますので合わせて参照していただけると幸いです。

体重管理アプリを作りながらLine botとAWSについて学ぶ -後編-

環境

環境は以下の様になります。

  • MacOS: Catalina 10.15.4
  • Python(ローカル): 3.6.10
  • Python(Lambda): 3.6

全体像(前編)

前編では、Line botで体重を入力すると前回との比較結果を返す仕組みを作ります。作成する全体像のイメージとしては下記図になります。

実装のメインは、送信された体重を元に前回の体重との比較を返すプログラムの部分になります。作成手順としては以下のような流れで行っていきます。

  1. Line botの登録
  2. おうむ返しをするLine botの作成
  3. 体重の結果を返すLine botへの改良

1. Line botの登録

Line botを使用するにはLINE Developers にてアカウント登録を行い設定する必要があります。登録と設定に関してはこちらのサイト(PythonでLine botを作ってみた)を参考に行いました。 AWSと連携して使用するための設定とチャネルシークレットとチャネルアクセストークンを取得するため以下の流れで行いました。

  1. アカウント登録
  2. プロバイダー作成
  3. チャンネル作成
  4. チャンネルを作成するとこのような画面がでますのでチャンネル基本設定Messaging API設定で設定を行います。

    まず、チャンネル基本設定項目にてチャネルシークレットを取得します。
    次に、Messaging API設定項目にて、応答メッセージ、あいさつメッセージが有効になっていますので無効に変更し、Webhookを有効にします。そして、チャネルアクセストークン(ロングターム)を取得します。

2. おうむ返しをするLine botの作成

Line botの設定が完了したので次にAWS側のLambdaとAPI Gatewayを設定しておうむ返しをするbotの作成を行います。 おうむ返しの実装に関しては、こちら(Lambdaでline-bot-sdk-pythonを使用してオウム返しBOTを作成する)を参考に行っております。

Lambdaの設定

lambdaでライブラリを使用するために、zipでまとめてアップロードする必要があります。

  1. ライブラリの導入と圧縮
  2. 作業ディレクトリを作成し移動します。そして下記コマンドでライブラリをインストールします。

    python -m pip install line-bot-sdk -t .
  3. lambda_function.pyを作成する
  4. 以下の内容で作業ディレクトリにlambdaで使う関数ファイル(lambda_function.py)を作成します。

    import os, sys
    from linebot import (LineBotApi, WebhookHandler)
    from linebot.models import (MessageEvent, TextMessage, TextSendMessage,)
    from linebot.exceptions import (LineBotApiError, InvalidSignatureError)
    
    def lambda_handler(event, context):
        signature = event["headers"]["X-Line-Signature"]
        body = event["body"]
    
        ok_json = os.environ["ok_json"]
        error_json = os.environ["error_json"]
    
        @handler.add(MessageEvent, message=TextMessage)
        def message(line_event):
            text = line_event.message.text
            line_bot_api.reply_message(line_event.reply_token, TextSendMessage(text=text)) 
    
        try:
            handler.handle(body, signature)
        except LineBotApiError as e:
            logger.error("Got exception from LINE Messaging API: %s\n" % e.message)
            for m in e.error.details:
                logger.error("  %s: %s" % (m.property, m.message))
            return error_json
        except InvalidSignatureError:
            return error_json
        return ok_json
    
  5. ファイルを圧縮する
  6. ディレクトリ内のファイルをすべて選択し圧縮します。

  7. Lambda関数にzipファイルをuploadする
  8. ランタイムPython 3.6を指定したlambda関数を作成し、zipファイルを選択後アップロードして保存します。

  9. 環境変数を設定する
  10. Line botの設定時に取得したチャネルシークレットとチャネルアクセストークンと正常終了時、異常終了時の戻り値を設定します。

API Gatewayの設定

Lmabdaの設定が完了したので、Line botからのWebhookを受け取るAPI Gatewayの設定を行います。

  1. APIの作成を行う
  2. コンソール画面でAPI Gatewayを選択し、APIを作成を選択します。図の画面がでますので、REST APIの構築をクリックします。

  3. リソースの作成を行う
  4. アクションの項目からリソースの作成を選択し、リソース名(今回はline)を入力し作成します。

  5. メソッドの作成を行う
  6. アクションの項目からメソッドの作成を選択します。POSTを選択しチェックを入れると図の画面がでますのでLambdaプロキシ統合の使用にチェックを入れ、使用するLambda関数を指定し保存します。権限追加のポップアップでででますのでOKを押します。

  7. メソッドリクエストの設定
  8. リクエストの検証でクエリ文字列パラメータおよびヘッダーの検証を選択しチェックを入れます。
    HTTP リクエスヘッダーにてX-Line-Signatureという名前でヘッダーを追加し、必須にチェックを入れます。

  9. デプロイ作業
  10. アクションからAPIのデプロイを選択します。ステージ名をdevとして新しいステージを作成します。

  11. URLの確認とLine botへの登録
  12. デプロイ作業が完了すると図の画面がでますので、URLの呼び出しに書かれているURLをコピーします。

    Line DevelopersのMessaging API設定に移動し、取得したURLをWebhook URLに貼り付け更新します。

確認

これでおうむ返しをする設定が完了しました。Line botにて文字を入力すると同じ文字が返ってくることを確認することができました。

3.体重の結果を返すLine botへの改良

Line botで入力した文字をおうむ返しするプログラムの作成ができましたので、打ち込んだ体重データを保存して、比較結果を返す処理の実装を行います。

先ほど作成したlambda_function.pyの15行目を確認すると、入力した文字はline_event.message.textで取得できることがわかります。 このデータを使用してS3のCSVファイルへの保存と体重の比較を行っていきます。

操作に必要な権限の追加

実装に入る前に、lambda関数にS3ファイルへの読み込み(GetObject)・書き込み(PutObject)の権限を追加する必要があるので、下記のポリシーを追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::バケット名/*"
        }
    ]
}

CSVデータ詳細

S3の使用するバケット配下に以下の内容のCSVファイルをアップロードします。今回は新規作成などの機能は実装していませんので最低1日分のデータは入力する必要があります。

date time weight
2020/5/8 9:00 58.4
2020/5/9 9:00 58.7

lambdaレイヤーにpandasをimportする

S3に保存しているCSVファイルの読み込みや書き込みを行うためにPandasを使用したいと思います。PandasをLambdaで使用するためには、圧縮したライブラリをLambdaレイヤーにアップロードする必要があります。こちらのサイト(pandasをLambdaのLayerとして追加する)を参考にすると圧縮したPandasのzipファイルをgithubにアップロードしてくれていたので、git cloneして利用させていただきます。

Lambdaのレイヤーを選択し、レイヤーの作成を行います。

レイヤー名の入力、cloneしてきたpandasのzipファイルのアップロードを行い、ランタイムでPython3.6を選択しレイヤーを作成します。

レイヤーを作成したので、Lambda関数で呼び出す設定を行います。図のLayersを選択しレイヤーの追加をクリックします。

作成したレイヤー名とバージョンを選び追加します。

lambdaからS3のCSVファイルを変更する

レイヤーの追加を行い、Pandasが利用できるようになりましたので新たに必要になる関数をまとめてlambda_function.pyに追加します。

import pandas as pd
import boto3, datetime, pytz
from io import StringIO

入力した体重データを引数に、前回との体重の比較とデータの保存を行うget_weight_insert関数を作成しlambda_function.pyに追加します。

BUCKET_NAME = バケット名
KEY = ファイル名
dt_now = datetime.datetime.now(pytz.timezone('Asia/Tokyo'))

def get_weight_insert(weight):
    #前回の体重を取得する
    s3 = boto3.client('s3')
    obj = s3.get_object(Bucket=BUCKET_NAME, Key=KEY)
    body = obj['Body']
    csv_string = body.read().decode('utf-8')
    df = pd.read_csv(StringIO(csv_string))
    
    #取得した体重を元にcsvを更新
    s3_resource = boto3.resource('s3')
    new_dt = pd.DataFrame( [[dt_now.strftime('%Y/%m/%d'),dt_now.strftime('%H:%M'), weight]], columns=['date', 'time', 'weight'])
    df2 = df.append(new_dt, ignore_index=True)
    
    s3_object = s3_resource.Object(BUCKET_NAME, KEY)
    s3_object.put(Body=df2.to_csv(None,index=False).encode('utf_8_sig'))
    
    #前回の体重との比較を行う
    last_time_weight = df.iloc[-1]['weight'] #前回の体重を取得
    result = round((float(weight) - last_time_weight), 1)
    if result == 0:
        return_value =  "前回と同じ!その調子!"
    elif result > 0:
        return_value =  str(result) + "kg太ったよ。頑張ろう!"
    elif result < 0:
        return_value =  str(abs(result)) + "kg痩せたよ!お疲れさん!"
    return return_value

先ほどの15行目のtext = line_event.message.textのあとに get_weight_insertをtextを引数に呼び出せるように追加します。

def message(line_event):
        text = line_event.message.text
        text = get_weight_insert(text) #追加
        line_bot_api.reply_message(line_event.reply_token, TextSendMessage(text=text)) 

結果

Line botに体重を入力した後のCSVの追加の確認とLine botの応答を確認します。前回の体重との比較によって返答が変わったことがわかります。 では入力していきたいと思います。先ほどの結果より、現在の最終行のデータは58.2kgとなっています。

では、入力データによって想定通りの結果を返すか確認します。

前回のデータとの計算結果を元に前回より、太った痩せたのレスポンスができる様になりました!

S3 Selectによる確認

Line botで入力する前後のCSVデータの変化をS3 Selectを使用し確認します。以下の図のように設定します。

S3の該当のファイル(今回、私はmy_weight.csvとしています)を選択し、S3 Selectを押します。そうすると下記図のようにでますので、ファイル形式をCSV、区切り記号をカンマに設定します。データだけ見たいので、ヘッダー行にチェックを入れます。

プレビューの 次へ を選択し、SQLを実行することでデータの確認ができます。

  1. 入力前
  2. Line bot入力前に確認すると2件のデータが入っていることがわかります。

  3. 入力後
  4. Line bot入力後に確認すると入力した3件のデータが入っていることがわかります。

前半まとめ

Line botの設定とLambdaやAPI Gatewayの設定、S3の使用など設定が多くなってしまいましたが、体重を入力すると前回との比較の結果を返す機能の作成は無事に完了しました。後半では、取得したデータを元にデータの可視化や暗号化などに挑戦したいと思いますので後半も合わせて見ていただけますと幸いです。

体重管理アプリを作りながらLine botとAWSについて学ぶ -後編-

参考リンク