材料から料理の提案をしてくれるLineBot作成（定期実行機能付き）

## はじめに
よくあるPythonでLineBotを作ってみたってやつを自分でもやってみたくて作りました。

仕組みは簡単で、クックパッドで検索をする作業をLineBotで出来るようにしただけです。
※いわゆる**スクレイピング**という技術を使うわけですが**自己責任**でお願いします。

あんまりこういう記事では見かけない
定期的にメッセージを送信してくれる方法も解説します。

## 準備
まずはPython + LineBot + Herokuの環境を作成します。
私は以下の記事を参考にしました。
https://qiita.com/shimajiri/items/cf7ccf69d184fdb2fb26
https://developers.line.biz/ja/docs/messaging-api/building-sample-bot-with-heroku/

## デプロイするファイル構成
以下を全て同じディレクトリに配置します。

| ファイル名 | 説明 |
|:-:|:-:|
| main.py | ソースコード |
| runtime.txt | Pythonのバージョンを記載 |
| requirements.txt | インストールするライブラリの記載 |
| Procfile | プログラムの実行方法を定義 |

## 実装
```:Procfile
web: python main.py
```

```requirements.txt
Flask==1.0.2
line-bot-sdk==1.8.0
beautifulsoup4==4.7.1
urllib3==1.24.1
soupsieve==1.6.1
```

```runtime.txt
python-3.9.0
```

```main.py
from flask import Flask, request, abort

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage, LocationMessage
)
from linebot.exceptions import LineBotApiError
from linebot.models import (
   CarouselColumn, CarouselTemplate, FollowEvent,
   LocationMessage, MessageEvent, TemplateSendMessage,
   TextMessage, TextSendMessage, UnfollowEvent, URITemplateAction
)
import os
import requests
from bs4 import BeautifulSoup
import re

app = Flask(__name__)

YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]
DATABASE_URL = os.environ['DATABASE_URL']

line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)

#WebhookからURLにイベントが送られるようにする
@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):
    #テキスト保持
    text = event.message.text
    
    #料理リスト
    list = []
    
    #クックパッドで検索
    url = "https://cookpad.com/search/" + text + "?order="

    #検索結果取得
    r = requests.get(url)
    soup = BeautifulSoup(r.text, 'html.parser')

    #レシピタイトル
    title = soup.find_all(class_="recipe-title")

    #レシピ画像
    image = soup.find_all(class_="recipe-image")

    #レシピ内容
    memo = soup.find_all(class_="recipe_description")

    #5つのレシピを料理リストに追加
    i=0
    for each in title[:5]:
        result_dict = {
                "thumbnail_image_url": image[i].find('img').get('src'),
                "title": title[i].get_text(),
                "text": memo[i].get_text().strip('\n'),
                "actions": {
                    "label": "料理を見る",
                    "uri": "https://cookpad.com" + each.get('href')
                }
        }
        list.append(result_dict)
        i+=1

    #カルーセルに渡す形に変換
    columns = [
        CarouselColumn(
            thumbnail_image_url=column["thumbnail_image_url"],
            title=column["title"],
            text=column["text"],
            actions=[
                URITemplateAction(
                    label=column["actions"]["label"],
                    uri=column["actions"]["uri"],
                )
            ]
        )
        for column in list
    ]

    #メッセージ作成
    messages = TemplateSendMessage(alt_text="料理について提案しました。",template=CarouselTemplate(columns=columns))

    #送信する
    try:
        line_bot_api.reply_message(event.reply_token, messages=messages)
    except Exception as e:
        line_bot_api.reply_message(event.reply_token, messages=str(e))

if __name__ == "__main__":
    port = int(os.getenv("PORT", 5000))
    app.run(host="0.0.0.0", port=port)
```

## 完成イメージ
ここまでで一旦以下のようにキーワードを送信すると料理を5つおすすめしてくれるbotが
出来上がりました。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">材料入れたら料理提案してくれるbot作ってみた<a href="https://t.co/Ft7yGclsgO">https://t.co/Ft7yGclsgO</a><a href="https://twitter.com/hashtag/%E7%8F%BE%E5%BD%B9%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#現役エンジニアと繋がりたい</a> <a href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#駆け出しエンジニアと繋がりたい</a> <a href="https://twitter.com/hashtag/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0?src=hash&amp;ref_src=twsrc%5Etfw">#プログラミング</a> <a href="https://twitter.com/hashtag/Python?src=hash&amp;ref_src=twsrc%5Etfw">#Python</a> <a href="https://twitter.com/hashtag/linebot?src=hash&amp;ref_src=twsrc%5Etfw">#linebot</a> <a href="https://twitter.com/hashtag/heroku?src=hash&amp;ref_src=twsrc%5Etfw">#heroku</a> <a href="https://t.co/QKQl0FTXia">pic.twitter.com/QKQl0FTXia</a></p>&mdash; そると (@jsalt_0525) <a href="https://twitter.com/jsalt_0525/status/1357705073128730632?ref_src=twsrc%5Etfw">February 5, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 定期実行機能追加
料理を提案してくれるbotは出来ましたが、こちらから材料を送信しないと提案してくれませんので
定期的に自動で料理を提案してくれる機能も追加してみましょう！

## 定期実行の仕組み
定期実行をするためにはHerokuのschedulerという機能を追加します。
また、定期で実行するPythonのファイルを上記で作成したファイルたちと同じディレクトリに作成します。
まずは以下の記事を参考に同じく定期実行する` scheduler.py` を作成してみましょう
https://qiita.com/eito_2/items/b313077e3860ee6a71b7

## DB用意
定期実行の仕組みがわかったところであと一つ必要な準備があります。
最初に作ったbotでは自分からbotに材料をテキストメッセージとして送ってあげることで
差出人の情報（ユーザーID）を取得し返信することが出来ましたが、
botの方から送信するためには宛先（ユーザーID）を保持していなければなりません。
ということでHerokuの機能である**postgres**を追加します。
私は以下の記事を参考に追加しました。
https://qiita.com/q_masa/items/6eb28f772157382aeac8

## 実装２
まずはDBに接続するために**psycopg2**というライブラリを追加します。

```requirements.txt
Flask==1.0.2
line-bot-sdk==1.8.0
beautifulsoup4==4.7.1
urllib3==1.24.1
soupsieve==1.6.1
psycopg2
```

次に**main.py**に友達追加時にDBにユーザーIDを保持する処理を追加します。
ここでは**＜Host＞****＜Database＞****＜User＞****＜Password＞**に
それぞれHerokuのpostgresのページにある**Database Credentials**に記載の情報を入れます。

```main.py

#~ 省略 ~

import psycopg2

#接続情報取得
def get_connection():
    dsn = "host=＜Host＞ port=5432 dbname=＜Database＞ user=＜User＞ password=＜Password＞"
    return psycopg2.connect(dsn)

#友達追加時のイベント
@handler.add(FollowEvent)
def handle_follow(event):

    #友達追加時DBにユーザーIDを登録する
    with get_connection() as conn:
        with conn.cursor() as cur:
            try:
                profile = line_bot_api.get_profile(event.source.user_id)
                s = 'INSERT INTO Users VALUES (%s)' % ("'" + str(profile.user_id) + "'")
                cur.execute(s)
                conn.commit()
            except:
                line_bot_api.reply_message(
                    event.reply_token,
                    TextSendMessage(text='exception')
                )

#~ 省略 ~
```

これで友達追加時にDBにユーザーIDが追加されます。
もしちゃんと追加されたか確認したい場合はターミナルに
`heroku pg:psql -a ＜アプリケーション名＞`
を入力すればSQLを実行できるようになるので確認してみてください。
詳しい使い方はここでは割愛します。

最後に` scheduler.py` を修正して定期的に料理を送ってくれるようにします。

```scheduler.py

from flask import Flask, request, abort
import os

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, TextMessage, TextSendMessage,
)
from linebot.models import (
   CarouselColumn, CarouselTemplate, FollowEvent,
   LocationMessage, MessageEvent, TemplateSendMessage,
   TextMessage, TextSendMessage, UnfollowEvent, URITemplateAction
)
import psycopg2
import requests
from bs4 import BeautifulSoup
import re

app = Flask(__name__)

LINE_CHANNEL_ACCESS_TOKEN = "<botのChannel access token>"
line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)

#接続情報取得
def get_connection():
    dsn = "host=＜Host＞ port=5432 dbname=＜Database＞ user=＜User＞ password=＜Password＞"
    return psycopg2.connect(dsn)

def main():

    #料理リスト
    list = []

    #クックパッドで検索
    url = "https://cookpad.com/search/" + "豚肉" + "?order="

    #検索結果取得
    r = requests.get(url)
    soup = BeautifulSoup(r.text, 'html.parser')

    #レシピタイトル
    title = soup.find_all(class_="recipe-title")

    #レシピ画像
    image = soup.find_all(class_="recipe-image")

    #レシピ内容
    memo = soup.find_all(class_="recipe_description")

    #5つのレシピを料理リストに追加
    i=0
    for each in title[:5]:
        result_dict = {
                "thumbnail_image_url": image[i].find('img').get('src'),
                "title": title[i].get_text(),
                "text": memo[i].get_text().strip('\n'),
                "actions": {
                    "label": "料理を見る",
                    "uri": "https://cookpad.com" + each.get('href')
                }
        }
        list.append(result_dict)
        i+=1

    #カルーセルに渡す形に変換
    columns = [
        CarouselColumn(
            thumbnail_image_url=column["thumbnail_image_url"],
            title=column["title"],
            text=column["text"],
            actions=[
                URITemplateAction(
                    label=column["actions"]["label"],
                    uri=column["actions"]["uri"],
                )
            ]
        )
        for column in list
    ]

    #メッセージ作成
    messages = TemplateSendMessage(alt_text="料理について提案しました。",template=CarouselTemplate(columns=columns))

    #DBに登録されている友達分送信する
    with get_connection() as conn:
        with conn.cursor() as cur:
            cur.execute("SELECT * FROM Users")
            for each in cur:
                line_bot_api.push_message(each[0], messages=messages)

if __name__ == "__main__":
    main()
```

## 完成イメージ２
ここまでで以下のようにHerokuのschedulerで指定した時間に"豚肉"料理が送信されてくればOKです！
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">定期的に料理提案してくれるbot作ってみた<a href="https://t.co/7u7j4MOZrl">https://t.co/7u7j4MOZrl</a>…<a href="https://twitter.com/hashtag/%E7%8F%BE%E5%BD%B9%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#現役エンジニアと繋がりたい</a> <a href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&amp;ref_src=twsrc%5Etfw">#駆け出しエンジニアと繋がりたい</a> <a href="https://twitter.com/hashtag/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0?src=hash&amp;ref_src=twsrc%5Etfw">#プログラミング</a> <a href="https://twitter.com/hashtag/Python?src=hash&amp;ref_src=twsrc%5Etfw">#Python</a> <a href="https://twitter.com/hashtag/linebot?src=hash&amp;ref_src=twsrc%5Etfw">#linebot</a> <a href="https://twitter.com/hashtag/heroku?src=hash&amp;ref_src=twsrc%5Etfw">#heroku</a> <a href="https://t.co/CCZsM67Y7K">pic.twitter.com/CCZsM67Y7K</a></p>&mdash; そると (@jsalt_0525) <a href="https://twitter.com/jsalt_0525/status/1357705841537732614?ref_src=twsrc%5Etfw">February 5, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


今回は材料に固定で"豚肉"を入れていますが
材料を何種類かDBに持っといて定期的にランダムで材料を選んで送信してくれるようにしたりとか
過去にこっちから送った材料をDBに保持しておいて勝手に提案してくれたりとか
色々発展させればもっと楽しくなりそうですね！

## まとめ
今回はPython + LineBot + Herokuで料理を提案してくれるbotの作成を行いましたが
料理の提案以外にもいろんなことが出来そうですね。
基盤さえ出来ていれば後は乗せていけばいいだけなので
自分が好きなことにどんどん応用していって知見を広げていきましょう！

## ソースコード
https://github.com/beniho/CuisineBot