コロナで臨時休校中の子どもに学びを届けるプロジェクト2

臨時休校でもLINEbotで学校との距離を縮める

 コロナ禍におけるたび重なる臨時休校で子どもへの連絡が取りづらく、子どもも学校との距離感を感じてしまっていると聞き、その解消となればと思いLINE bot(ボット=自動で返信してくれるロボット)を作ってみました

作ったもの

 学校のLINEアカウントを登録してトーク画面で話しかけると、コロナウィルスに関する公的な情報や、学校からのお知らせ、JR・バスの運行情報が取得できる、生徒にとってコロナ禍の学校生活における便利なツールとなっています

構成図

作成手順

環境構築

Python3.7のインストール
flaskやHeroku CLIなどパッケージのインストール
LINE公式アカウントの開設
Herokuのアカウント作成

ネット上で参考記事が多数あるので省きます。

LINE Developersからチャネルを作成してChannel Secretとアクセストークンを取得

LINEチャットボット(オウム返しボット)
以下の点に注意してから作業を始めてください! パソコンで作業をする ブラウザの自動翻訳機能をオフにする LINE Developersに登録・設定 1.にアクセスします。 2.ページ右上のコンソールにログインをクリックします。 3.前回と同

上記のような他の記事を参考にしてください

REST APIでbotアプリを実装

ローカルのディレクトリをgitリポジトリとして設定

% git init

Herokuでアプリケーション作成

※ローカルのリポジトリ(ディレクトリ)名とアプリケーション名が違う場合は設定し直さなくてはいけなくなるので注意

$ heroku create アプリケーション名

Creating ● アプリケーション名... done
https://アプリケーション名.herokuapp.com/ | https://git.heroku.com/アプリケーション名.git

取得したChannel SecretとアクセストークンをHerokuに設定

後ろの--appを省くと今いるプロジェクトの環境変数を設定してくれる

% heroku config:set YOUR_CHANNEL_SECRET="Channel Secretの文字列" --app アプリケーション名
% heroku config:set YOUR_CHANNEL_ACCESS_TOKEN="アクセストークンの文字列" --app アプリケーション名

設定の確認

% heroku config --app アプリケーション名

./main.py

FlaskでRESTful APIとその後の処理を作っていく

FlaskによるRESTful APIについては以下が参考になります

Flask-RESTful API With Heroku
Create a basic Flask-RESTful API And Deploy it to Heroku
from flask import Flask, request, abort

from linebot import (
    LineBotApi, WebhookHandler
)
from linebot.exceptions import (
    InvalidSignatureError
)
from linebot.models import (
    MessageEvent, PostbackEvent, FollowEvent,
    TextMessage, TextSendMessage, ImageSendMessage, TemplateSendMessage,
    ButtonsTemplate, ConfirmTemplate,
    PostbackAction, MessageAction, URIAction
)

from scrape import scrape
from fixed_phrase import FixedPhrase
import os


app = Flask(__name__)

latest_school_info =\
    "2 臨時休業中の課題の提出及び検定試験の受験申込みについて\n・4月17日に配布された課題の提出は、次の学校登校日に提出してください。\n" \
    "・検定試験の受験申込みについても締め切り日を延長します。次の登校に受験申込みの手続をしてください。\n※次の検定試験の最終締め切り日は5月29日(金)です。" \
    "全商簿記検定試験,全商珠算・電卓検定試験,全商ビジネス文書検定試験"
latest_school_corona =\
    "<【重要】生徒及び保護者のみなさまへ (全日制・定時制)>(5月1日)\n1 臨時休業の延長について\n新型コロナウィルス感染症対策のため、学校は5月6日(水)まで臨時休業としていましたが、***教育委員会の指示により、下記の期間まで延長となりましたので、" \
    "お知らせいたします。\n延長期間  5月7日(木)~10日(日)まで\nこの期間は、不要不急の外出を避け、引き続き体調管理や健康観察に努めてください。また、学校から出された課題にも引き続き取り組んでください。\n" \
    "なお、5月11日(月)以降については、決まり次第、学校ホームページ等でお知らせいたします。"


#環境変数取得
YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]
line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)

# ユーザーがメッセージが送信した時LINE Messaging APIが署名を検証し、問題なければhandler.handleに定義されている関数を呼び出す
@app.route("/callback", methods=['POST'])
def callback():
    # get X-Line-Signature header value
    signature = request.headers['X-Line-Signature']

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)

    return 'OK'

def make_button_template():
    buttons_template_message = TemplateSendMessage(
        alt_text='Buttons template',
        template=ButtonsTemplate(
            thumbnail_image_url='https://lh3.googleusercontent.com/p/AF1QipPfd5O2n8w82W-e98PIUUlobNMXVak7pXTKaF-s=s1600-w800',
            title='**商業高校のLINEbotです',
            text='メニューから選んでください、直接話しかけても応答します',
            actions=[
                PostbackAction(
                    label='新型コロナウィルス関連',
                    data='corona_info'
                ),
                PostbackAction(
                    label='課題と検定について',
                    data='school_info'
                ),
                PostbackAction(
                    label='JR・バス運行情報',
                    data='train_info'
                ),
                URIAction(
                    label='*商のウェブサイトを開く',
                    uri='http://www.*********.ed.jp/index.html'
                )
            ]
        )
    )
    return buttons_template_message

def make_confirm_template():
    confirm_template_message = TemplateSendMessage(
        alt_text='Confirm template',
        template=ConfirmTemplate(
            text='どの新型コロナウィルス関連情報をみますか?',
            actions=[
                PostbackAction(
                    label='Hoge',
                    data='corona_Hoge'
                ),
                PostbackAction(
                    label='*商',
                    data='corona_Fuga'
                )
            ]
        )
    )
    return confirm_template_message

# ユーザーが公式アカウントを登録した時にhandle_follow関数を呼び出し
@handler.add(FollowEvent)
def handle_follow(event):
    app.logger.info("Got Follow event:" + event.source.user_id)
    line_bot_api.reply_message(
        event.reply_token, make_button_template())

# ユーザーが「メニュー」やpattern.csvにある特定のメッセージを送信した時にhandle_message関数を呼び出し
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    try:
        if event.message.text == 'メニュー':
            line_bot_api.reply_message(
                event.reply_token,
                make_button_template())
        else:
            fixed_phrase = FixedPhrase('library/pattern.csv')
            line_bot_api.reply_message(
                event.reply_token,
                TextSendMessage(text=fixed_phrase.answer(event.message.text)))
    finally:
        line_bot_api.reply_message(
            event.reply_token,
            TextSendMessage(text='すみませんがその言葉には答えられません。困ったときは「メニュー」と話しかけてみてください。'))

# PostbackEventはユーザーからのメッセージ送信やボタン押下に反応するイベント
@handler.add(PostbackEvent)
def handle_postback(event):
    if event.postback.data == 'corona_info':
        line_bot_api.reply_message(
            event.reply_token, make_confirm_template()
        )
    elif event.postback.data == 'school_info':
        line_bot_api.reply_message(
            event.reply_token, TextSendMessage(text=latest_school_info)
        )
    elif event.postback.data == 'corona_Hoge':
        line_bot_api.reply_message(
            event.reply_token, ImageSendMessage(

# コロナの情報はスクレイピングできなかったのでcloudに保存した画像ファイルを表示                original_content_url='https://res.cloudinary.com/**********/v1588177082/kinkixyuuzitai0424-2_uhitun.jpg',
                preview_image_url='https://res.cloudinary.com/***********/c_scale,w_196/v1588177082/kinkixyuuzitai0424-2_uhitun.jpg'
            )
        )
    elif event.postback.data == 'corona_Fuga':
        line_bot_api.reply_message(
            event.reply_token, TextSendMessage(text=latest_school_corona)
        )
    # 運行情報はscrape関数を呼んでスクレイピング
    elif event.postback.data == 'train_info':
        line_bot_api.reply_message(
            event.reply_token, TextSendMessage(text=scrape('運行情報'))
        )

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

./fixed_phrase.py

pattern.csvに入っている特定の文字がユーザーから送信されると定型文を返せるように処理。

import csv
import re


class FixedPhrase:
    def __init__(self, fpath):
        self.fpath = fpath

    # ユーザー入力文が IN 列の文字列パターンにマッチしたら定型文を返す
    def answer(self, user_input):
        self.reader = csv.reader(open(self.fpath, "r", encoding='utf-8_sig'))
        next(self.reader)

        for row in self.reader:
            while row.count("") > 0:
                row.remove("")

            for w in row[1:]:
                if (re.match(w, user_input)):
                    return row[0].replace("{IN}", w)
        return ""

./library/pattern.csv

OUT,IN1,IN2,IN3,IN4,IN5,IN6,IN7,IN8,IN9,IN10
本校は、大正**年に開校し創立**年になります。**ことを学ぶ商業教育と普通教科の指導、部活動など文武両道による教育活動を展開し、バランスのとれた人間教育を目指しています。,(?=.*学校)
開発担当は**です,**,開発,(?=.*誰)(?=.*作っ)
1自主自律の習慣を養う 2勤労を尊ぶ 3教養を高める,(?=.*教育目標)
【全日制】0*-****-3556        【定時制】0*-****-3557,(?=.*電話)
〒0**-00** **市*3条3丁目1番1号,(?=.*住所)

csvは1列目が見出し行(出力)OUT、2列目以降が見出し行(入力)INです。正規表現でIN1〜IN10までの言葉がユーザから送信されれば、OUTの内容が返ってくるようになっています

./scrape.py

JR・バスの運行情報や自治体のコロナウィルス最新情報はBeatifulSoupを使ってスクレイピングしてくるようにしています。

from bs4 import BeautifulSoup as bs
# from lxml import html
import urllib.request as ur
import re
import requests


def soup(url):
    req = ur.urlopen(url)
    soup_data = bs(req, "html.parser")
    return soup_data

def scrape(user_input):
    if 'お知らせ' in user_input:
        url = 'http://www.*********.ed.jp'
        req = ur.urlopen(url)
        soup_data = bs(req, "html.parser")
        match = soup_data.find_all('p', limit=5)
        school_info = "*商からのお知らせ\n " + match
        return school_info
    elif "運行情報" in user_input:
        # 運行情報をリストに格納していく
        service_status = []
        # 鉄道運行情報の取得
        train_url = ['http://www.jikokuhyo.co.jp/search/detail/line_is/hoge_fuga',
                     'http://www.jikokuhyo.co.jp/search/detail/line_is/hoge_piyo']
        for u in range(len(train_url)):
            match = soup(train_url[u]).find(class_="corner_block_row_detail_d").string.replace('\n', '')
            line_title = ["【JR】FUGA線",
                          "【JR】PIYO線"]
            train_info = line_title[u] + match
            service_status.append(train_info)

        # バス運行情報の取得
        bus_title = "【バス】**軌道"
        bus_url = "http://www.*********.jp/operation-status/"
        match = soup(bus_url).find(class_="travel_info_box").contents[1].text
        bus_info = bus_title + match
        service_status.append(bus_info)
        return "\n".join(service_status)
    elif "コロナ" in user_input and "HOGE" in user_input:
        url = 'https://web.pref.hoge.lg.jp/kk03/200129.html'
        req = ur.urlopen(url)
        soup_data = bs(req, "html.parser")
        match = soup_data.find_all('li')[27].text
        corona_info = "新型コロナウイルスの対応について\n【お知らせ】\n " + match
        return corona_info
    elif "コロナ" in user_input and "FUGA" in user_input:
        url = 'https://web.pref.fuga.lg.jp/kk03/200129.html'
        req = ur.urlopen(url)
        soup_data = bs(req, "html.parser")
        match = soup_data.find_all('#rs_contents > div > div.submenu-main > p:nth-child(3) > small > a > img')[27].text
        corona_info = "新型コロナウイルスの対応について\n【お知らせ】\n " + match
        return corona_info
    else:
        return "それに関する情報は取得できません。"

Herokuにbotアプリをデプロイ

※デプロイする前の注意点
requirements.txt に必要なライブラリを記述する。
ローカルでpip installしただけでは動作しない。
requirements.txt が root ディレクトリにあれば、Herokuはpython appであると認識するよう

% git add .
% git commit -am "first commit"
% git push heroku master

Herokuのバージョンでエラーが出てバージョンを下げる時は以下のコマンドを実行する

% heroku stack:set heroku-20

ちなみにエラーが出た後でないとこのコマンドは実行できない。

does not appear to be a git repository

このエラーが出たら

git remote add heroku https://git.heroku.com/アプリケーション名.git

もう一度リモートにアプリケーションを追加してやる

正常にプッシュできたかログを確認

$ heroku logs

LINE DevelopersでREST APIのURLをWebhook URLとして設定

LINEチャットボット(オウム返しボット)
以下の点に注意してから作業を始めてください! パソコンで作業をする ブラウザの自動翻訳機能をオフにする LINE Developersに登録・設定 1.にアクセスします。 2.ページ右上のコンソールにログインをクリックします。 3.前回と同

上記のような他の記事を参考にしてください

つまづいたところ

バスの運行状況を取得しようとしたところ認証エラーが出た

import ssl
ssl._create_default_https_context = ssl._create_unverified_context

上のコードを追加することでエラーは解消したが、今度は何も取得してこない
原因は**バスのhtmlがJavascriptによって生成されていること
そこで request.htmlフレームワークをimportで取得しようとしたが

ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1076)
/Applications/Python\ 3.7/Install\ Certificates.command

上手くいかず

コメント

タイトルとURLをコピーしました