臨時休校でもLINEbotで学校との距離を縮める
コロナ禍におけるたび重なる臨時休校で子どもへの連絡が取りづらく、子どもも学校との距離感を感じてしまっていると聞き、その解消となればと思いLINE bot(ボット=自動で返信してくれるロボット)を作ってみました
作ったもの
学校のLINEアカウントを登録してトーク画面で話しかけると、コロナウィルスに関する公的な情報や、学校からのお知らせ、JR・バスの運行情報が取得できる、生徒にとってコロナ禍の学校生活における便利なツールとなっています
構成図
作成手順
環境構築
Python3.7のインストール
flaskやHeroku CLIなどパッケージのインストール
LINE公式アカウントの開設
Herokuのアカウント作成
ネット上で参考記事が多数あるので省きます。
LINE Developersからチャネルを作成してChannel Secretとアクセストークンを取得
上記のような他の記事を参考にしてください
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については以下が参考になります
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として設定
上記のような他の記事を参考にしてください
つまづいたところ
バスの運行状況を取得しようとしたところ認証エラーが出た
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
上手くいかず
コメント