初めまして、キャスレーコンサルティング LS(リーディングサービス)部の駒井です。
今回は、HerokuとAmazonS3を用いて、
関数のグラフ画像を返してくれる「LINEBOT」を作成してみようと思います。
(基本的なHerokuを利用したLINEBOTについては、こちらをご覧ください。)

グラフ画像の描画には、Pythonライブラリであるmatplotlibを、
LINEBOT開発には、LINEが提供する公式のLINEBOT SDKを用います。

 

LINEBOTは、公式SDKを始め各種ライブラリやクラウドサービスを利用して、
簡単に作ることができるので、是非一緒に作ってみてください!!

目次

  1. 動作環境
  2. 事前準備
  3. コーディング
  4. Heroku app作成
  5. デプロイ
  6. Webhook URLの設定
  7. LINEBOTにメッセージを送信してみる
  8. まとめ

1. 動作環境

  • macOS 10

2. 事前準備

各種アカウントの作成・設定

まず、利用する各種サービスのアカウントを作成し、それぞれに必要な設定・準備を行います。

LINE Developers

以下から、LINEアカウントでログインすることができます。
https://developers.line.me/ja/

LINE Developersでプロバイダーを作成したら、新規チャネルを作成します。
「Messaging APIでチャネル作成する」を選択してください。

プランについては、今回は作ってみようというだけなので、「Developer Trial」で十分です。
その他、アプリ名などを設定して、チャネルを作成します。

作成が完了すると、以下のような(一部)チャネル設定画面を開くことができます。

「アクセストークン」を発行し、「Webhook送信」を「利用する」に設定しておいてください。
「Channel Secret」と「アクセストークン」は、あとでHerokuとの連携に利用します。
また、現段階では「Webhook URL」は未入力で問題ないです。

これで、LINE Developersの初期設定は完了です。

Heroku

以下から、アカウントを作成します。
https://jp.heroku.com/home

Herokuアカウント作成時にクレジット情報が必要ですが、
今回のHeroku利用は、無料枠に収まるので安心してください。

アカウントの作成が完了したら、Heroku CLIもインストールしておきましょう。

以下から、インストーラーがダウンロードできます。
https://devcenter.heroku.com/articles/heroku-cli

また、macOSで、Homebrewが使える環境であれば、以下のコマンドでもインストールできます。

$ brew install heroku/brew/heroku

AWS

以下から、アカウントを作成します。
https://aws.amazon.com/jp/

AWSアカウントの作成が完了したら、
まず、今回のLINEBOTで生成したグラフ画像をアップロードする、S3バケットを作成します。

  1. コンソール画面左上の「サービス」から、「S3」を選択する。
  2. 「バケットを作成する」をクリックする。
  3. 「バケット名」を入力する。
  4. 「リージョン」を「アジアパシフィック (東京)」に設定する。
  5. あとは、デフォルトで構いません。

作成が完了すると、以下のようにS3バケットが作成されます。

次に、LINEBOTでS3を操作する用のユーザーを作成します。

  1. コンソール画面左上の「サービス」から、「IAM」を選択する。
  2. 画面左のメニュー欄から「ユーザー」を選択する。
  3. 「ユーザーを追加」をクリックする。
  4. 「ユーザー名」を入力する。
  5. 「アクセスの種類」を「プログラムによるアクセス」に設定する。
  6. 「次のステップ: アクセス権限」をクリックする。
  7. 「既存のポリシーを直接アタッチ」から「AmazonS3FullAccess」のみを選択する。
  8. 「次のステップ: 確認」をクリックする。
  9. 「ユーザーの作成」をクリックする。
  10. ユーザーのセキュリティ認証情報を、csv形式で取得する。
  11. 「閉じる」をクリックする。

作成が完了すると、以下のようにS3の参照編集権限を持ったユーザーが作成されます。

また、ダウンロードしたcsv形式の認証情報は、
あとでHerokuとの連携に利用するので厳重に管理しておいてください。

以上で、各種アカウントの作成と設定は完了です。

ローカル開発環境の構築

次に、LINEBOT開発をする際のローカル環境を構築していきます。
今回は、pyenv-virtualenvを用いてPython-3.6.6の仮想環境を構築します。

以下のコマンドが使えることを、確認してください。

  • pyenv
  • pyenv virtualenv

まず、各自のローカル環境のお好みの場所に、
LINEBOT開発用のディレクトリを作成し、移動します。

$ mkdir linebot
$ cd linebot

続いて、Python-3.6.6の仮想環境を作成し、カレントディレクトリ下に適用します。

$ pyenv install 3.6.6

※pyenvによる、Python-3.6.6のインストールをしていない場合

$ pyenv virtualenv 3.6.6 heroku_linebot_3.6.6
$ pyenv local heroku_linebot_3.6.6

最後に、作成した環境でgitリポジトリを新規作成します。

$ git init

以上で、ローカル開発環境の構築は完了です。

3. コーディング

「事前準備」で構築した環境下に、具体的なソースコード類を作成していきます。

まず、必要なライブラリをインストールしていきます。

・Python Web framework
$ pip install flask
・Web Server Gateway Interface
$ pip install gunicorn
・数値計算、グラフ描画ライブラリ
$ pip install numpy
$ pip install matplotlib
・LINEBOT SDK
$ pip install line-bot-sdk
・AWS SDK
$ pip install boto3

以上で、ライブラリのインストールが完了です。

次に、各種設定ファイルを作成していきます。

・Pythonのバージョン情報をruntime.txtに記録します。
$ echo python-3.6.6 > runtime.txt
・Webサーバの起動コマンドをProcfileに記録します。
$ echo web: gunicorn app:app --log-file - > Procfile
・.gitignoreの設定をします。
$ echo .python-version > .gitignore
・インストールライブラリ情報を、requirements.txtに記録します。
$ pip freeze | tee requirements.txt

実際に使用した、requirements.txtの中身は以下です。

requirements.txt
boto3==1.9.4
botocore==1.12.4
certifi==2018.8.24
chardet==3.0.4
click==6.7
cycler==0.10.0
docutils==0.14
Flask==1.0.2
future==0.16.0
gunicorn==19.9.0
idna==2.7
itsdangerous==0.24
Jinja2==2.10
jmespath==0.9.3
kiwisolver==1.0.1
line-bot-sdk==1.8.0
MarkupSafe==1.0
matplotlib==2.2.3
numpy==1.15.1
pyparsing==2.2.0
python-dateutil==2.7.3
pytz==2018.5
requests==2.19.1
s3transfer==0.1.13
six==1.11.0
urllib3==1.23
Werkzeug==0.14.1

以上で、各種設定ファイルの作成は完了です。
次に、ソースコードを作成します。

まず、関数のグラフを描画するにあたって、
最低限必要となる描画範囲と関数の種類を、送信メッセージから取得するために、
送信メッセージのフォーマットを以下のようにし、正規表現で有効なメッセージがどうかをチェックします。

送信メッセージフォーマット
[<xの開始値>:<xの終了値>]
<関数名>(x)

プロットする際の刻み幅は、上記フォーマットで指定した範囲を100分割した1つ分の幅に固定します。

また、LINEBOTに画像を返す場合、画像のURLの他に、
プレビュー画像のURLも指定する必要があり、どちらもhttpsでなければなりません。

さらに、今回、対応関数は一次関数の他、numpy標準の20個ほどの初等関数のみとし、
いくつかの関数を組み合わせたり、係数をつけたりといったことには対応していません。

それでは、app.pyというソースファイルを作成し、以下を入力します。

app.py
import os
import re

# Python Web framework
from flask import Flask, request, abort

# グラフ描画関連
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

# LINE API
from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage, ImageSendMessage
)

# AWS連携関連
import boto3

# インスタンス化
app = Flask(__name__)

channel_access_token = os.environ['LINE_CHANNEL_ACCESS_TOKEN']
channel_secret       = os.environ['LINE_CHANNEL_SECRET']
line_bot_api         = LineBotApi(channel_access_token)
handler              = WebhookHandler(channel_secret)
aws_s3_bucket        = os.environ['AWS_BUCKET']

# デプロイ確認用ルーティング
@app.route("/")
def hello_world():
    return "hello world!"

# LINEからPOSTリクエストが届いたときの処理
@app.route("/callback", methods=['POST'])
def callback():
    signature = request.headers['X-Line-Signature']
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)
    return 'OK'

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):

    # LINEメッセージのフォーマットチェック
    # 正しくない場合はテキストメッセージをLINEに返す
    if not valid_message_format(event.message.text):
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text = '入力が正しくないよ!!\n[<xの開始値>:<xの終了値>]\n<関数名>(x)\nの形で入力してください。')
        )
        return

    axis_range, func = event.message.text.split('\n')
    xmin_str, xmax_str = axis_range.strip('[]').split(':')

    xmin = float(xmin_str)
    xmax = float(xmax_str)

    # LINEメッセージの範囲指定チェック(大小関係)
    # 正しくない場合はテキストメッセージをLINEに返す
    if xmin > xmax:
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text='範囲指定が正しくないよ!!')
        )
        return
    
    # x軸の刻みは描画範囲を100等分で固定
    dx = (xmax-xmin) * 0.01
    x  = np.arange(xmin, xmax, dx)

    # 送られてきた関数文字列に応じてnumpy標準の関数オブジェクトを取得
    y = func_generator(func, x)

    # 関数が取得できたかチェック
    # 取得できていない場合はテキストメッセージをLINEに返す
    if y is None:
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text='その関数は描画できません。'))
        return
    
    # 描画
    plt.figure()
    plt.plot(x, y)
    
    # ファイルを一時的にHeroku Dynoに保存
    file_name = func + '.png'
    plt.savefig(file_name)

    # S3へグラフ画像をアップロードする
    s3_resource = boto3.resource('s3')
    s3_resource.Bucket(aws_s3_bucket).upload_file(file_name, file_name)

    # S3へアップロードした画像の署名付きURLを取得する
    s3_client = boto3.client('s3')
    s3_image_url = s3_client.generate_presigned_url(
        ClientMethod = 'get_object',
        Params       = {'Bucket': aws_s3_bucket, 'Key': file_name},
        ExpiresIn    = 10,
        HttpMethod   = 'GET'
    )
    
    # 画像URLを指定してLINEに返す
    line_bot_api.reply_message(
        event.reply_token,
        ImageSendMessage(
            original_content_url = s3_image_url,
            preview_image_url    = s3_image_url
        )
    )

def valid_message_format(msg):
    pattern = '^\[-?[0-9]+.?[0-9]*:-?[0-9]+.?[0-9]*\]\n[a-zA-Z0-9()]+$'
    return re.match(pattern, msg)

def func_generator(func, x):
    if func == 'x':
        return x
    elif func == 'sin(x)':
        return np.sin(x)
    elif func == 'cos(x)':
        return np.cos(x)
    elif func == 'tan(x)':
        return np.tan(x)
    elif func == 'cos(x)':
        return np.cos(x)
    elif func == 'arcsin(x)':
        return np.arcsin(x)
    elif func == 'arccos(x)':
        return np.arccos(x)
    elif func == 'arctan(x)':
        return np.arctan(x)
    elif func == 'exp(x)':
        return np.exp(x)
    elif func == 'log(x)':
        return np.log(x)
    elif func == 'log2(x)':
        return np.log(x)
    elif func == 'log10(x)':
        return np.log10(x)
    elif func == 'sinh(x)':
        return np.sinh(x)
    elif func == 'cosh(x)':
        return np.cosh(x)
    elif func == 'tanh(x)':
        return np.tanh(x)
    elif func == 'arcsinh(x)':
        return np.arcsinh(x)
    elif func == 'arccosh(x)':
        return np.arccosh(x)
    elif func == 'arctanh(x)':
        return np.arctanh(x)
    elif func == 'floor(x)':
        return np.floor(x)
    elif func == 'round(x)':
        return np.round(x)
    elif func == 'fix(x)':
        return np.fix(x)
    else:
        return None

if __name__ == "__main__":
    app.run()

os.environで指定している環境変数や、AWSユーザー認証情報はあとで設定します。
最終的に、作成したファイルは以下の4つです。

  • runtime.txt
  • Procfile
  • requirements.txt
  • app.py

コミットします。

$ git add -A
$ git commit -m "initial commit"

4. Heroku app作成

コミットしたファイルをデプロイする、Herokuアプリの作成・設定をします。
Heroku CLIが使えることを、確認してください。

・Herokuログイン
$ heroku login
・Herokuアプリ作成
$ heroku apps:create <your_heroku_app_name>

<your_heroku_app_name>には好きな名前を設定できますが
「linebot」など単純なものはすでに使われている可能性が高いので、
もう少しひねって一意になるような名前をつけてください。
ちなみに、名前を指定しないとHeroku側に適当に命名されます。
試しにやってみると分かると思いますが、命名センスはなかなか独特です。

・環境変数設定

ここで、事前準備で作成したLINEチャネル情報やAWSユーザー認証情報、
S3バケット情報をHerokuに登録していきます。

$ heroku config:set LINE_CHANNEL_SECRET=<your_LINE_Channel_Secret>
$ heroku config:set LINE_CHANNEL_ACCESS_TOKEN=<your_LINE_Channel_access_token>
$ heroku config:set AWS_ACCESS_KEY_ID=<your_AWS_Access_key_ID>
$ heroku config:set AWS_SECRET_ACCESS_KEY=<your_AWS_Secret_access_key>
$ heroku config:set AWS_BUCKET=<your_AWS_S3_bucket_name>

5. デプロイ

作成したHerokuアプリに、先ほどコミットしたファイルをデプロイします。

$ git push heroku master

ログを確認します。

$ heroku logs


Build succeededとなっていればデプロイ成功です。

次にhttps://<your_heroku_app_name>.herokuapp.com/へアクセスしてみます。
ソースコードの、

# デプロイ確認用ルーティング
@app.route("/")
def hello_world():
    return "hello world!"

より、「hello world!」が表示されるはずです。

$ heroku open

期待通りの表示ですね。

6. Webhook URLの設定

作成したHerokuアプリのcallback URLを
LINE DevelopersのWebhook URLに設定します。
再び、LINE Developersのチャネル設定画面を開いてください。

「Webhook URL」に
https://<your_heroku_app_name>.herokuapp.com/callback
を設定します。

ここで、「https://」部はすでに書かれているので、入力の際はそれ以降を入れるようにしてください。
また設定後、「接続確認」を行うと上記のようなエラーになりますが、問題ありません。

7. LINEBOTにメッセージを送信してみる

以上で、LINEBOTの作成は終了です。
作成したLINEBOTにメッセージを送信してみましょう。

LINE Developersのチャネル設定画面下部にある「LINEアプリへのQRコード」を使って、
自分のLINEアプリに作成したLINEBOTを友達登録します。
「トーク」画面からメッセージを送信します。

・フォーマットに合わないメッセージを送信した場合

・指定範囲の大小が逆だった場合

・指定関数が存在しなかった場合

・関数のグラフが描画できた場合

また、関数の描画が成功し画像が生成されると、
<関数名>.pngという名前でS3にアップロードされているはずです。

S3の作成したバケット内を見てみると、

アップロードされていましたね。

8. まとめ

いかがでしたでしょうか。
私自身、AWSなどのクラウド知識はまだまだ乏しいので、LINEBOT作成を通して色々と勉強になりました。

また、LINEBOTは当たり前ですが、
UIとしてあの慣れ親しんだLINEトーク画面を提供してくれていますので、
フロント側を気にする必要がないという面でも、
クラウドサービスやWebサーバの処理を学ぶのに適していると感じました。

今回は、単純に1つの初等関数のグラフを返すだけでしたが、
ここまで土台ができれば、メッセージの解析をさらに改造して
複数の関数を組み合わせたりと色々と応用することができそうです。

LINEBOT SDKについては、Githubにソースコードや使用方法が公開されています。
Python版は、こちらですので、合わせて見てみると良いと思います。
ここを見ても分かる通り、LINEBOT SDKはテキストや画像だけでなく、
動画やリッチメニューにも対応しているということで、色々やってみると面白いかもしれませんね。

補足

ブログ執筆開始当初、LINEBOTを作成するにあたり、
AWS Lambdaを利用する案もありましたが、今回はHerokuを利用しました。
というのも、少し調べてみたところ、Lambda上でPythonの外部ライブラリを使うには、
ライブラリのモジュール一式をソースコードとともに
zipファイルにまとめてアップロードする必要があります。

また、numpyやmatplotlibなど一部OS依存なライブラリの場合には、
Lambdaが実行される環境である、AmazonLinux上でインストールされたものである必要があります。

そして、そのためには、AmazonLinuxのDockerイメージを持ってきて、
Dockerコンテナ内でインストールする必要があるということでした。

今回は、とりあえずクラウドサービスやWebサーバの勉強も兼ねて、
LINEBOTを作成してみるという目的だったので、Lambdaは利用せず、
手軽にWebアプリ構築ができる、Herokuを利用しました。

また今後、機会があれば、
上記のような外部ライブラリを使った、
AWS LambdaでのLINEBOT作成などにも挑戦してみたいと思います。

以上です。
最後までご覧いただきありがとうございました。

駒井
CSVIT事業部 LS(リーディングサービス)部 駒井
2018新卒入社。AWSやSalesforceなど、クラウドサービスを活用したシステム開発に携わっています。