19番目の日記

情報系よわよわ高専生がやったことや思ったことをつらつらと.

簡単な授業変更情報botを作ってみた

きっかけ

 今年5月末頃、クラスのLINEグループをクラスslackに移行しないかとう案がでました。いろいろ反対意見も出てたのですが、なんやかんやあり、とりあえず一ヶ月程のお試し期間を設けることになりました。そのお試し期間中にslack移行案発案者から僕にこんな一言が。

 「SIGMA君、授業変更bot作ってよ」

今考えりゃなんで僕に言ったのか謎です。それまでに僕はpythonでwebスクレイピングの方法を学んでいたので同じ要領で作れるだろうということでとりあえずやってみることに。

まずはサイトから授業変更情報の抜き出し

 webスクレイピング自体はやったことあったのでここはそこまで苦労しなかったです。とはいえ数回やった程度なので細かいやり方は忘れていたので過去にみていたサイト*1を見ながらとりあえず情報の抜き出しをしてみました。

import requests
from bs4 import BeautifulSoup
response = requests.get(url)
bs = BeautifulSoup(response.content, "html.parser")
# tdタグのみ抜き出し
td = bs.select('td')

パッケージはRequestsとBeautifulSoup4を使用しました。
流石にサイトのURLを貼ったら学校バレするので変数urlに代入してます。
requestsモジュールのget関数に学校のサイトの、クラスの授業変更情報のページのURLを渡しhtmlを取得しresponseに代入、BeautifulSoupにresponseのbyte形式とhtml.perserを渡し取得したhtmlを操作可能にしています。(ここら辺は僕の独自解釈なのでご容赦を)
 html.perserはhtmlのタグ情報を解析してプログラムで扱えるようにするプログラムらしいです。
抜き出したい授業変更情報はtdタグで記述されていますのでひとまずtdタグを全部抜き出し変数tdに代入しました。これで変数tdにはtdタグで記述されている文を上から順に格納されました。
 しかしtdタグには授業変更情報以外もありますので、ここから授業変更情報のみを抜き出します。授業変更情報は以下の表のような形で書かれています。

日時
科 目 名
◯月□日 △時限目 ××××→・・・

また、表の要素はこの番号の順に格納されています。

①日時
②科 目 名
③◯月□日 △時限目 ④××××→・・・

授業変更情報の直前には必ず「科目名」という単語がありますのでfor文で線形探索し、授業変更情報の始まりの要素数を調べます。

sub = 0
for i in range(len(td)):
    if "科 目 名" in td[i]:
        sub = i + 1

これで授業変更情報の始まりの要素数が変数subに格納されました。なのでtd[sub]より後の要素は授業変更の情報だとわかりました。
 ここまでで学校のサイトから授業変更の情報の抜き出しが完了しました。

授業変更情報の中から必要な情報のみを取得

 ここまでで抜き出した情報は、予定されている授業変更情報がすべて含まれています。このプログラムは夜に実行されて次の日の授業変更情報をslackに投稿するようにしたいので実行された日の次の日の授業変更情報のみを抜き出したいのです。例えば、6月22日に実行されたならば、抜き出したい授業変更情報は6月23日のものです。なのでこの場合、tdの中に「6月23日」という文字列があるかどうかを探索し、あればその日の授業変更情報を取得します。

日付の取得

 これを実装するためにまずは実行された日の次の日の日付を取得します。datetimeモジュールを使用しました。

import datetime
# 今日の日付を取得
dt_now = datetime.datetime.now()
# 現在時刻が午前なら今日、午後なら明日の授業変更
now = dt_now.hour
if now > 12:
    crt = 1
    dat = "明日"
else:
    crt = 0
    dat = "今日"

today_date = (datetime.datetime.now() + datetime.timedelta(days=crt)).day
today_month = (datetime.datetime.now() + datetime.timedelta(days=crt)).month

# 日付を曜日付きで文字列に変換
weekday = ["月", "火", "水", "木", "金", "土", "日"]
today_week = datetime.date.today().weekday() + crt
if today_week > 6:
    today_week = 0

today = str(today_month) + "月" + str(today_date) + "日" + "(" + weekday[today_week] + ")"

 最初は完全に翌日分のみを取得するようにしようと思ったのですが僕にある考えが浮かびました。
 「仮に朝実行されたときに次の日の授業変更取得するのはおかしくね?」
まあ朝実行されることは無いわけですが()
なので午前に実行された時はその日の授業変更情報を取得し、午後に実行された時は次の日の授業変更情報を取得するようにしました。
ただ、日付を取得する方法は知っていたのですが次の日の日付の取得の仕方がわからなかったので調べました*2
datetime.datetime.now()で現在の日付を取得し、datetime.timedelta(days=crt)を加算することで、現在の日付からcrt日だけ進めた日付が取得できます。現在の時刻を取得し、12時より前ならcrtを0、12時以降ならcrtを1にして、取得する授業変更が当日分か翌日分かを決めます。datの値はslackに投稿する際に使うのですが今はスルーです。あとは取得した日付を.day、.monthで日付と月に分解し、それぞれtoday_dateとtoday_monthに代入しました。
また、曜日をweekdayにリストで代入しdatetime.date.today().weekday()にcrtを加算したものを要素数で指定することで曜日を取得します。datetime.date.today().weekday()の返り値は月が0、火は1・・・日は6と言った感じです。あとはこれらを文字列として結合し、todayに代入します。これで当日もしくは翌日の日付が文字列として変数todayに格納されました。(翌日の日付でもtodayとは)

授業変更情報の中からtodayの日付を探索

事前準備

次に全ての授業変更から、todayに格納されている日付の授業変更情報を取得します。
まずは探索するための前準備をします。

# tdタグの全ての数を数える
count_all = len(td)

# change_todayの要素数jと授業変更があるかのフラグ
j = 0
flag = False
# 授業変更の個数を代入
count_change = (count_all - sub)

# 全ての授業変更を格納するリストと必要な分の授業変更のみを格納するリストを作成
change_all = [[""] for i in range(count_change)]
change_today = [["", ""] for i in range(int(count_change / 2))]

output = "<!channel>" + "\n" + today + "\n"

 まずはtdの全部の数からsubを減算することで全部の授業変更の数を求め、count_changeに代入します。
 次に全ての授業変更を格納するリストchange_allと、取得したい授業変更を入れるリストchange_todayを用意します。授業変更は日付の要素と変更内容の二つの要素があるのでそれを一つにまとめるためにchange_todayは二次元配列にしてあります。
 また、cange_todayの要素数指定のためのjと、授業変更情報があるかどうかのフラグflagを用意しておきました。
["◯月□日 △時限目","××××→・・・"]
こんな感じに格納します。
 outputは最終的にslackに投稿する文字列として@channelのメンションと日付を事前に格納しておきます。

いよいよ探索
# 授業変更がある場合のみ探索
if sub != 0:

    # 全ての授業変更を文字列として代入
    for i in range(count_change):
        change_all[i] = td[sub + i].get_text()

    # 今日(明日)の授業変更のみを抜き出し
    for i in range(0, count_change, 2):
        if today in change_all[i]:
            flag = True
            change_today[j][0] = change_all[i].replace(today, "")
            change_today[j][1] = change_all[i+1]
            output = output + change_today[j][0] + "\n  " + change_today[j][1] + "\n"
            j += 1

subが0(=「科目名」という単語が見つからない)ということは授業変更が無いということなのでその時は探索しません。

まずはtdのsub以降の要素以外使わないのでchange_allにtdのsub以降の要素を抜き出して入れておきます。change_allは日付→変更内容→日付→変更内容のように交互に格納されているので、ループカウンタiを一つ飛ばしにし、日付の要素だけを見ていきます。todayが含まれている要素が見つかったら、flagをTrueにして、その日付の要素とその日の変更内容をchange_todayに格納し、jを一つ進めることで授業変更内容が複数あった場合でも順に格納できるようにしています。また、slackに一度に全て投稿するために、授業変更情報を逐次outputと結合しています。これで、必要な情報のみを取得することができました。

slackへ投稿

 こちらも方法がわからなかったので調べることに。
qiita.com
こちらの記事を見ながらやりました。
まずはIncoming Webhookの設定ページ*3
にて
投稿先のチャンネルを指定

Webhook URLをコピー

設定を保存


これで得られたURLをプログラムで使用します。
slackwebモジュールを用いました。

import slackweb
slack = slackweb.Slack(url=slack_url)

# 授業変更がなかった場合
if flag is False:
    slack.notify(text=dat + today + "の授業変更はありません")

# 授業変更があった場合、slackにメンションつきで投稿
else:
    slack.notify(text=output)

今回はWebhook URLをslack_urlに代入してあります。

flagがFalse(=授業変更がない)なら
「今日(明日)◯月△日(☆)の授業変更はありません」
の形で投稿、Trueなら前の段階で求めたoutputを投稿します。
 これで簡単なものですが授業変更情報を取得するプログラムは完成しました。

プログラムを定期的に実行する

 ここまでで実行すると授業変更情報を取得しslackに投稿するプログラムは完成しました。しかしこれを定期的に実行されるようにしなければbotとはいえません。AzureやAWSなど友達に言われ考えたのですが、使い方がわからず断念。そこで使用したのがこちら

みんな大好きラズベリーパイですね。
ラズパイのcrontabコマンドを使いました。
こちらも例によってネットで調べながらやることに*4

crontab -e 

こちらのコマンドを実行し、設定用のエディタを開き、

0 20 * * 0-4 /home/pi/berryconda3/bin/python /home/pi/jyugyo/main.py ||[$? -eq1]

と記述。ポイントとしてはコマンドと実行ファイルは絶対パスで記述しなければいけません。

分 時 日 月 曜日 [コマンド] 

のように設定し、指定しない場合は*を記述します。
曜日は日曜が0から1ずつ増やした値です。
また。0-4とハイフンでつなげることで範囲指定ができます。

この設定では日曜〜木曜の20時に実行します。

||[$? -eq1]

これはよくわからないのですがうまくいかず悩んでいた時にどっかのサイトで見て、書いたらなんかできたのでつけてます。(教えて偉い人)
これで授業変更botが完成しました。

今後の課題

 ひとまず授業変更情報botはできたのですが、簡易的なものですのでまだまだ改良できるところはあると思います。
今考えているのは、水曜日に月曜授業など、行事予定として組み込まれているものは授業変更として扱われないので、Googleカレンダーから行事予定を取得してそういったものに対応していきたいなと。



非常にわかりづらい文章で申し訳ありませんが、ここまで読んでくれた方ありがとうございます。