🤖

保育園からのお知らせをSlackに通知する

2023/02/26に公開

きっかけ

  • 保育園からのお知らせがWebで投稿されるシステムになった。
  • 見たかどうかが、園の方でわかるシステム(見てないと 先生にみてますか?と言われる)
  • 毎日Webで見にいくのが面倒
  • Slackに投稿させるようにする事で毎日の手間と見忘れ防止

システム構成

システム構成は以下のとおりです。

お知らせスクレイピング

  1. サーバ上のスクリプトをcronで定期実行させる(1日1回)
  2. スクリプトはWebサイトにアクセスして、お知らせ情報をスクレイピングする
  3. 新しいお知らせがあったら、内容をSlackに投稿する

必要な知識, ハード, 事前準備

  • Raspberry Piなどのサーバ
  • Linuxの知識 (cron, aptなどでのソフトの導入)
  • 基本的なプログラミング知識(RubyとかPythonとか)
  • Slackアカウントの作成と、Webhookで投稿できるように準備
  • Webサイトのログイン情報
  • HTML, JavaScriptなどに対する基本的な知識

この記事では、主にスクレイピングの部分にフォーカスします。
cronの設定方法やSlackの設定などは他の人の記事などを参考にしてください。
ある程度のことはご自分で調べて解決できる人を対象にしています。
Slackに通知の部分はLINEとかにすると、パートナーにも優しいかもしれません。

スクリプト

Nokogiri, Mechanizeを導入します。

項目 説明
Nokogiri HTMLの解析、必要な項目の取得に使う
Mechanize ログイン、リンクを踏む、formの操作

以下のコマンドで導入できます。

$ sudo gem install nokogiri
$ sudo gem install mechanize

プログラムのポイント

  • Slackへの投稿はWebhookを使って投稿(jsonを組み立ててpostするだけ)
  • 前回どこの記事まで読んだかは、1日以内の記事かで判断(1日に一回起動するので)
    • 厳密にやらなくても自分で使うツールだから少しぐらいルーズでOK
  • スクレイピングは泥臭いけど、一時的に使う物だからOK
  • 添付ファイルがある場合は、Webサイトにログインしてみれば良いのでWebサイトのURLをSlackに投稿
    • 画像をDLとかもできるけど、面倒なのでそこは割り切った

コードはこちら(URLとかパスワードとかは適当なものに置き換えているので、このままでは動きません)
それぞれのシステムによって、HTMLのタグとかは異なってくると思うので、ご参考まで。
JavaScriptとか使ってリッチになっているサイトだと、ヘッドレスのWebブラウザとかの利用が必要になるかもしれません。

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

#
# 保育園のWebサイトに投稿される園からのお知らせをSlackに投稿します。
#

require 'nokogiri'
require 'mechanize'
require 'time'
require 'json'

$website_url = "https://example.com/園のシステムのURL"

# Slackにメッセージを投稿します
# Slackの投稿情報はJSON形式でファイルに記載しておいてあります
def notify_slack(date_str, title, body)
  begin
    File.open("/var/tmp/slack_hook_info.json") do |file|
      info = file.read
      slack_info = JSON.parse(info)

      url = slack_info["web_hook_url"]
      params = {}
      params["text"] = "<!here>\n#{date_str}\n#{title}\n\n#{body}"

      uri = URI.parse(url)
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = uri.scheme === "https"
      response = http.post(uri.path, params.to_json)

    end
  rescue SystemCallError => e
    puts %Q(class=[#{e.class}] message=[#{e.message}])
  rescue IOError => e
    puts %Q(class=[#{e.class}] message=[#{e.message}])
  end
 
end

# 解析するべき時間の記事か判定する
# 1日以内の記事だったら解析する
def is_enable_date?(date_time)
  today = Time.now
  current_time = Time.local(today.year, today.month, today.day, today.hour)
  yesterday_time = current_time - (3600 * 24)

  (date_time > yesterday_time && date_time <= current_time)
end

# 記事の確認、ページを開く(読んだことの通知になる) , 添付ファイルの有無を確認
def analyze_info(agent, info_post_form, date_str, index, title)
  date_time = Time.parse(date_str)

  if !is_enable_date?(date_time)
    # 古い記事の場合は何もしない
    return
  end

  # 次のページを取得する
  info_post_form.field_with(name: '__EVENTTARGET').value = "grdINFO$ctl#{sprintf("%02d", index)}$lnkINFO"
  info_detail = info_post_form.submit

  info_doc = Nokogiri::HTML.parse(info_detail.body)
  body = info_doc.at_css("[id=\"lblNAIYO\"]").text
  if info_doc.at_css('[id="hlFILE_LINK1"]') != nil
      # 添付ファイルがある時には、添付ファイルがある事を通知します。
      body = body + "\n\n <添付ファイルあり>\n[#{$website_url}]"
  end

  # slack に投稿する
  notify_slack(date_str, title, body)
  sleep 1

  # 前のページに戻る
  agent.back
end

# __main__
agent = Mechanize.new
agent.max_history = 5
agent.user_agent_alias = 'Mac Safari'
agent.conditional_requests = false

# Webにアクセスします
login_page = agent.get($website_url)

# ログインフォームを取得して、ユーザID, パスワードを入力してログインボタンを押します
login_form = login_page.form_with(name: 'form1')
sleep 1
login_form.field_with(name: 'txtID').value = 'ユーザID'
login_form.field_with(name: 'txtPASS').value = 'パスワード'
sleep 1
top_page = login_form.click_button
sleep 1

# 園からのお知らせに遷移します
top_page_form = top_page.form_with(name: 'form1')
sleep 1
top_page_form.field_with(name: '__EVENTTARGET').value = 'grdMENU$ctl04$lnk02'
top_page_form.field_with(name: '__EVENTARGUMENT').value = ''

next_page = top_page_form.submit

doc = Nokogiri::HTML.parse(next_page.body)
info_post_form = next_page.form_with(name: 'form1')
#上から6記事読んで本日の物をSlackに投稿します。
(2..6).each do |n|
  date_id = doc.at_css("[id=\"grdINFO_ctl#{sprintf("%02d", n)}_lblDATE\"]")
  if doc.at_css("[id=\"grdINFO_ctl#{sprintf("%02d", n)}_lblDATE\"]") == nil
      # エラー
      p "#{date_id} not found!!"
      notify_slack(Time.now.strftime('%Y-%m-%d'), "HTML parse error","記事の一覧が取得できませんでした。")
      break
  end
  next if doc.at_css("[id=\"grdINFO_ctl#{sprintf("%02d", n)}_lblTITLE\"]") == nil
  date_str = doc.at_css("[id=\"grdINFO_ctl#{sprintf("%02d", n)}_lblDATE\"]").text
  title    = doc.at_css("[id=\"grdINFO_ctl#{sprintf("%02d", n)}_lblTITLE\"]").text

  # デバッグ用 ---
  # pp date_str
  # pp title
  analyze_info(agent, info_post_form, date_str, n, title)
end

Discussion