🤖
保育園からのお知らせをSlackに通知する
きっかけ
- 保育園からのお知らせがWebで投稿されるシステムになった。
- 見たかどうかが、園の方でわかるシステム(見てないと 先生にみてますか?と言われる)
- 毎日Webで見にいくのが面倒
- Slackに投稿させるようにする事で毎日の手間と見忘れ防止
システム構成
システム構成は以下のとおりです。
- サーバ上のスクリプトをcronで定期実行させる(1日1回)
- スクリプトはWebサイトにアクセスして、お知らせ情報をスクレイピングする
- 新しいお知らせがあったら、内容を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