cangoxina

生産性向上見習いのブログ的な何かです

宮崎大学の学生を支える? お知らせ通知サービス 「宮大支援課お知らせBot」

https://cdn-ak.f.st-hatena.com/images/fotolife/k/korosuke613/20191202/20191202024829.png

宮崎大学から学生へのお知らせをする方法の1つにWebサイトでの告知があります。

しかし、そのWebサイトでお知らせが告知されても、学生に通知が行くことはありません。 自分で度々見に行かないと、新しいお知らせがあるかがわからず、不便です。

なので、宮崎大学学生支援課のお知らせページに新しいお知らせが追加されると、それをSNSでお知らせしてくれるサービス2年前に作りました。

宮崎大学 Advent Calendar 2019 2日目の記事です。


目次


作ったもの

LINE版とTwitter版の2種類が存在します。

外観

LINE版「宮大支援課お知らせBot

f:id:korosuke613:20191201230958p:plain

Twitter版「宮大支援課お知らせBot」

f:id:korosuke613:20191201231649p:plain

機能

宮大支援課お知らせBotには以下の機能が存在します。

  1. 学生支援課のページで新しいお知らせが投稿されると、10分以内にユーザに告知する。
  2. 投稿日、タイトル、リンク、ページのスクリーンショット画像、添付されてるPDFの1ページ目の画像(あれば)の要素をまとめて1つのお知らせとする。
  3. LINE版はカルーセル表示を使って過去のお知らせを参照できる。

10分以内にユーザに告知

宮大支援課お知らせBotは10分おきに新しいお知らせが無いかをチェックします。もし新しいお知らせがあった場合、SNSに投稿し、ユーザにお知らせします。

「投稿日、タイトル、リンク、スクショ、PDFの1枚目」の情報で1つのお知らせ

宮大支援課お知らせBotは新しいお知らせを投稿するときに、投稿日、タイトル、リンク、ページのスクリーンショット画像、添付されてるPDFの1ページ目の画像をまとめて投稿します。

f:id:korosuke613:20191201234446p:plain

LINE版は過去のお知らせを参照可能

LINE版では、過去のお知らせをカルーセル表示で参照できます。もちろんお知らせページに遷移できます。

f:id:korosuke613:20191201231537p:plain

実装

宮大支援課お知らせBotの実装について説明します。

github.com

このサービスは以下のような構成で動いています。下手な図になってしまった

f:id:korosuke613:20191202003154p:plain

ホスティングにはHerokuを使っています。データベースはマネージドデータベースであるHeroku Postgresを使っています。 いずれも無料枠内で使えています。

お知らせの取得、配信、問合せサーバはすべてPythonで動作しています。それぞれについて説明します。

お知らせの取得

Heroku SchedulerというHerokuのアドオンを使って、10分おきに学生支援課のお知らせページをスクレイピングしています。

スクレイピング

スクレイピングにはBeautiful Soupを使っています。

お知らせページはcategory-moduleというクラスのついたulタグのネストの中で、以下のような構造になっています。

<ul> <!-- 月ごとのリスト -->
  <li> <!-- 11月 -->
    <ul> <!-- 記事ごとのリスト -->
      <li> <!-- 12月17日(火)インターンシップ... -->
        <a> <!-- 取得するURL情報 -->
          <!-- 取得するタイトル情報 -->
        </a>
        <span> <!-- 取得する投稿日情報 --> </span>
      </li>
    </ul>
  </li>
  <li> <!-- 10月 -->
......
</ul>

f:id:korosuke613:20191202010547p:plain

したがって、以下のようなコードで「投稿日、タイトル、リンク」を取得できます。(実際に使っているコードを改変したものです。実際は、新規のお知らせかのチェック等の処理もしています。)

# URLの指定
html = urllib.request.urlopen("http://gakumu.of.miyazaki-u.ac.jp/gakumu/allnews")
soup = BeautifulSoup(html, "html.parser")

# お知らせのあるulタグを指定
ul = soup.findAll('ul', class_='category-module')

days = []  # 投稿日を格納するリスト
menus = [] # タイトルを格納するリスト
urls = [] # URLを格納するリスト

for li_month in ul[0].findAll('li'):    # 月ごと
    for li in li_month.findAll('li'):   # 記事ごと
        for span in li.findAll('span'):
            if span.string is not None:
                days.append(span.string)    # 投稿日
        for a in li.findAll('a'):
            if a.string is not None:
                menus.append(a.string.strip())  # タイトル
                urls.append('http://gakumu.of.miyazaki-u.ac.jp' + a.get('href'))    # URL

スクリーンショットの取得

記事ページのスクリーンショットを自動で取得し、記事の部分のみトリミングした画像を保存しています。

ページのレンダリングにはPhantomJSを利用しています。*1

詳しくは以下の記事を参照してください。

korosuke613.hatenablog.com

PDFの1ページ目の画像取得

PDFの1ページ目の画像取得に関しては、以下のような手順でPythonで行っています。

  1. 記事ページ内にPDFのリンクがあれば、それの1番目のリンク先をダウンロードする。
  2. subprocessモジュールを使い、ImageMagickのconvertコマンドを呼び出す。
  3. PDFの1ページ目がPNGになる。
from os.path import isfile, splitext
from subprocess import check_call, CalledProcessError

def convert(pdf):
    """Convert a PDF to JPG"""
    if not isfile(pdf):
        print("ERROR", "Can't find {0}".format(pdf))
        return

    png = splitext(pdf)[0] + ".png"
    pdf = pdf + "[0]"

    try:
        check_call(["convert", "-background", "white", "-flatten", "-density", "144", pdf, png])
        print("Converted", "{0} converted".format(pdf))
    except (OSError, CalledProcessError) as e:
        print("ERROR", "ERROR: {0}".format(e))

配信

スクレイピング時に、新しいお知らせが確認できた場合のみ、各SNSで配信を行います。

LINE

LINEでの告知には、line-bot-sdk-pythonを使って、Messaging APIをたたいています。

line_bot_api = LineBotApi('アクセストークン')
line_bot_api.multicast('送信するユーザのIDのリスト', TextSendMessage(text='送りたいテキスト'))

上のようなコードで簡単にテキストを送信できます。 画像も似たような感じで送信できます。

Twitter

Twitterでの告知には、Twitter API*2を利用しています。

詳細は、この記事がわかりやすいです。

問合せサーバ

LINE版では、トークルームにおいて、特定の文字列をBotに対して送信することで、過去のお知らせを閲覧できます。

ユーザから受信したテキストをHerokuアプリに送るのにはWebhookを利用します。Webhookの設定はLINE Official Account Managerでできます。

f:id:korosuke613:20191202015944p:plain

僕は、Flaskというライブラリを使ってリクエストを捌きました。Flaskとline-bot-sdk-pythonを組み合わせると簡単にできます。

例えば、以下のような関数を定義することで、テキストを受信した際に、「あああああ」と返します。(参照)

@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    line_bot_api.reply_message(
        event.reply_token,
        TextSendMessage(text='あああああ'))

ルーセル表示では、一回につき5件までのカードを登録できます。 お知らせBotでは、4件を記事の情報、残り1件をさらに古い記事のカルーセルを表示するためのリンクとしました。*3

上のコードの場所はこちら

作ってみて

2017年6月当時、Python覚え立ての頃に、「なんかPythonでやってみたいな〜」と思って作ったのがこのサービスでした。

はじめはLINE Botのみだったのですが、当時、Push APIを使えるアカウントは無料枠だと友達が50人までしか追加できないという縛りがありました*4。 せっかく作ったからもっとたくさんの方に利用していただきたいと考えて、Twitter版の運用も始めました*5

ちょくちょくと使っていただけているみたいで、LINE版がアクティブユーザ40人、Twitter版がフォロワー121人となっております。 最初は友人のみがユーザだったのですが、クチコミか、友人が友人に教えたのか、今では知らないユーザの方が多いです。

すごく助かってる!」という声もたまに届き、作りがいがあったな〜と感じております。

2年前に作ってからなにかと機能拡張する余裕がなくて、ほっぽっているのですが、「カテゴリを選んで必要ないニュースを受信しないようする」や、「学生支援課のページ以外も対象にする」などの機能拡張案はあります。ただ、めんどいのでやっていません

当時は今の何倍も未熟であり、ソースコードが非常に汚いです。久しぶりにコードを読んでヤバいと感じました。*6

ソースコードも公開しているので、誰かしらが引き継いでいい感じにしてくれることを切に願っています笑


*1:このサービスを作った後?にHeadless Chromeが登場したため、現在はPhantomJSはサポートされてないようです...皆さんもChromeを使いましょう...参照

*2:昔は簡単に登録できたのに今めっちゃめんどくさいですよね

*3:もっとしっかり説明したかったのですが、力尽きました。

*4:現在は、ひと月に送信できるメッセージが1000件までという風になりました。おかげで、50人以下なのに月の途中でメッセージが送れなくなるという問題が発生しています。有料にしたいのですが月5000円強必要で、収益化できていない現在では有料ユーザにするつもりはありません。

*5:といっても、それぞれのAPIをたたいているだけなのですが

*6:作ってから半年後くらいに、ソースコードリファクタリングしてもっと保守などをマシにした宮大支援課お知らせBot2を作ってたのですが、とちゅうでだれてやめました。


スポンサーリンク