退屈なことは GAS にやらせよう ~Web スクレイピング編~
こんにちは.あらい大先生です.
前回の記事で GAS による作業の自動化の例を紹介したので,今回から実装の仕方を紹介します.
最初に紹介する自動化は「SEIYU の割引セールの開催日を Google カレンダーへ登録する作業」です.
開発の背景
SEIYU の割引セールに関する前提知識を以下に示します.
- SEIYU はクレディセゾンと提携していたため,セゾンカード感謝デーという名の割引セールが開催されていました(現在は提携を終了しています [1]).
- 割引セールは土曜日に開催されることが多かったですが,確実ではありませんでした.
- 割引セールの開催日は公式サイトに公開されていましたが,いつサイトが更新されるのか分かりませんでした.
そのため、割引セールを狙って買い物するには,「こまめに公式サイトへアクセスして割引セールの開催日を確認し,カレンダーへ登録する」という面倒な作業が必要でした.
そこで,「GAS,Web スクレイピング,Google カレンダー,LINE Notify を組み合わせて上記の作業を自動化できないか?」と,私は考えました.
また,この自動化を実現するには,3つの処理が必要だと考えました.
- Web ページから割引セールの開催日のみを抽出する.
- 先ほどの開催日を Google カレンダーへ登録する.
- LINE Notify で開催日を通知する.
実装
まずは先ほどの 1. ~ 3. を個別に実装していきます.
「GAS ○○」と検索すれば沢山の解説記事が見つかるので,積極的に参考にしていきましょう.
個別の実装が完了したら,良い感じに組み合わせていきます.
Web ページから割引セールの開催日のみを抽出する.
SEIYU の割引セールの情報が掲載された Web ページから割引セールの開催日のみを抽出して Date 型へ変換するプログラムを,以下に示します.
ただし、現在の SEIYU の公式サイトには割引セールの情報が掲載されていないため,Wayback Machine [2] によってアーカイブされた Web ページ [3] で代用しています.
function webScraping() {
// 年越しを考慮するために,スクリプトを実行した年と月を用意する.
const baseDate = new Date();
const thisYear = baseDate.getFullYear();
const thisMonth = baseDate.getMonth();
const html = UrlFetchApp.fetch('https://web.archive.org/web/20210506131022/https://www.seiyu.co.jp/shop/%E8%A5%BF%E5%8F%8B%E5%8D%97%E8%8D%89%E6%B4%A5%E5%BA%97/').getContentText('UTF-8');
// console.log(html);
const pTag = Parser.data(html).from('<p class="shop_recommend_banner_date">').to('</p>').build();
// console.log(pTag); // 5/8(土)、5/15(土)、5/29(土)
let dateStrs = pTag.split('、');
// console.log(dateStrs); // [ '5/8(土)', '5/15(土)', '5/29(土)' ]
for (let dateStr of dateStrs) {
let offYear = thisYear;
let splitedDateStr = dateStr.match(/[0-9]{1,2}\/[0-9]{1,2}/g)[0].split('/');
// console.log(splitedDateStr); // (例) [ '5', '8' ]
let offMonth = parseInt(splitedDateStr[0]);
let offDate = parseInt(splitedDateStr[1]);
// 12月にスクリプトが実行されており,かつ1月の開催日を取得した場合,1 < 12 で True となり,来年の開催日とみなす.
if (offMonth < thisMonth + 1) {
offYear = thisYear + 1;
}
let targetOffDay = new Date(offYear, offMonth - 1, offDate);
// console.log(targetOffDay); // (例) Mon May 08 2023 00:00:00 GMT+0900 (Japan Standard Time)
}
}
3~6 行目に関しては後で説明します.
まずは 8行目の const html = UrlFetchApp.fetch(url).getContentText('UTF-8');
で文字列に変換された HTTP レスポンスを取得します.
この処理は「右クリック → ページのソースを表示」に相当します.
次に,先ほど取得した HTML のソースコードから,割引セールの開催日のみを抽出する方針を考えていきます.
この作業を行う際は,「ページのソースを表示」や「検証」を用いて実際の Web ページと HTML のソースコードを対応付けしながら考えると良いでしょう.
ただし,「検証」を用いる場合,先ほど取得した HTML のソースコードと表記が異なることがあるので注意しましょう.
画像1が示すように,抽出したい情報は割引セールの開催日なので,HTMLのソースコードに対して [0-9]{1,2}\/[0-9]{1,2}
のような正規表現と一致する文字列を取得すれば良さそうです.
しかし,このソースコードの中の他の場所に日付が記載されている場合,その日付も取得してしまいます.
実際,画像1と画像2の両方で,コメントアウトされた日付が存在していることを確認できます.
そこで,抽出したい情報の周辺の情報(タグ,class属性,id属性)を用いて,検索する範囲を絞り込むことにします.
画像2が示すように,「ページのソースを表示」を用いて先ほどの灰色で網掛けされた部分を確認すると,割引セールの開催日は <p class="shop_recommend_banner_date"></p>
で囲まれていることが分かります.
そのため,11 行目の const pTag = Parser.data(html).from('<p class="shop_recommend_banner_date">').to('</p>').build();
を用いて割引セールの開催日を示す文字列 '5/8(土)、5/15(土)、5/29(土)'
を取得します.
なお,Parser は ライブラリであるため,インポートする必要があります.
その手順については [4] を参考にすると良いでしょう(他にも沢山あります).
最後に,先ほど取得した文字列 '5/8(土)、5/15(土)、5/29(土)'
から年月日を抽出します.
まずは,日付が「、」で区切られていることに着目して 14行目の let dateStrs = pTag.split('、');
で分割します.
これにより,配列 [ '5/8(土)', '5/15(土)', '5/29(土)' ]
を取得できます.
その後,17~25 行目のように,各要素に対して let splitedDateStr = dateStr.match(/[0-9]{1,2}\/[0-9]{1,2}/g)[0].split('/');
を用いて月と日を取得します.
ただし,今月と来月の割引セールの開催日が掲載される場合がありました.
また,画像が示すように,割引セールの開催日には年が記載されていません.
そのため,3~6 行目と 27~30 行目で,年越しを考慮する処理を行っています.
以上の処理により,割引セールの年月日を取得でき,Date 型へ変換します.
33 行目の console.log(targetOffDay);
で変数を出力させると,確かに Date 型へ変換できていることを確認できます.
開催日を Google カレンダーへ登録する.
割引セールの開催日を Google カレンダーへ登録するプログラムを,以下に示します.
function createOffDate(calendar, targetOffDay) {
// 重複登録の防止(いったん消す)
for (duplication of calendar.getEventsForDay(targetOffDay, { search: "5%OFF" })) {
duplication.deleteEvent();
}
calendar.createAllDayEvent(
title = '5%OFF',
date = targetOffDay,
options = {
description: '西友南草津店 - 店舗詳細|SEIYU\nhttps://www.seiyu.co.jp/shop/%E8%A5%BF%E5%8F%8B%E5%8D%97%E8%8D%89%E6%B4%A5%E5%BA%97/'
}
);
}
calendar には CalendarApp.getCalendarById(id)
の返り値,offDate には Date 型に変換された割引セールの開催日が代入されるという想定です.
なお,id は Google カレンダーの ID です.
Google カレンダーの ID を確認する手順は [5] を参考にすると良いでしょう(他にも沢山あります).
実際に割引セールの開催日を登録する処理は 8~13 行目です.
しかし,これだけだと動作確認のための実行や,トリガーによる定期実行によって,開催日の重複登録が行われてしまいます.
そのため,3~6 行目で古い情報を削除してから改めて登録しています.
LINE Notify で開催日を通知する.
Web スクレイピングの結果を LINE Notify で通知する処理を,以下に示します.
function lineNotify(token, text) {
const seiyuUrl = 'https://www.seiyu.co.jp/shop/%E8%A5%BF%E5%8F%8B%E5%8D%97%E8%8D%89%E6%B4%A5%E5%BA%97/';
const lineUrl = 'https://notify-api.line.me/api/notify';
const options = {
'method' : 'POST',
'headers' : {'Authorization' : 'Bearer ' + token},
'payload' : {'message' : 'SEIYU 5%OFF\n' + text + '\n' + seiyuUrl},
};
UrlFetchApp.fetch(lineUrl, options);
}
token には LINE Notify から発行されたパーソナルアクセストークン,text には割引セールの開催日を示した文字列が代入されるという想定です.
パーソナルアクセストークンを発行する手順については,[6] を参考にすると良いでしょう(他にも沢山あります).
text には,Web スクレイピングを実装する際の副産物の pTag を代入することにします.
結合
それぞれの処理を実装したので,3つの関数を良い感じ結合します.
自分用に開発したものなので,モジュールの独立性や変数名の分かりやすさ等を考慮せずに結合している点についてはご了承ください.
function main () {
const calendar = CalendarApp.getCalendarById(PropertiesService.getScriptProperties().getProperty('calendarId'));
const token = PropertiesService.getScriptProperties().getProperty('token');
// 年越しを考慮するために,スクリプトを実行した年と月を用意する.
const baseDate = new Date();
const thisYear = baseDate.getFullYear();
const thisMonth = baseDate.getMonth();
const html = UrlFetchApp.fetch('https://web.archive.org/web/20210506131022/https://www.seiyu.co.jp/shop/%E8%A5%BF%E5%8F%8B%E5%8D%97%E8%8D%89%E6%B4%A5%E5%BA%97/').getContentText('UTF-8');
// console.log(html);
const pTag = Parser.data(html).from('<p class="shop_recommend_banner_date">').to('</p>').build();
// console.log(pTag); // 5/8(土)、5/15(土)、5/29(土)
let dateStrs = pTag.split('、');
// console.log(dateStrs); // [ '5/8(土)', '5/15(土)', '5/29(土)' ]
for (let dateStr of dateStrs) {
let offYear = thisYear;
let splitedDateStr = dateStr.match(/[0-9]{1,2}\/[0-9]{1,2}/g)[0].split('/');
// console.log(splitedDateStr); // (例) [ '5', '8' ]
let offMonth = parseInt(splitedDateStr[0]);
let offDate = parseInt(splitedDateStr[1]);
// 12月にスクリプトが実行されており,かつ1月の開催日を取得した場合,1 < 12 で True となり,来年の開催日とみなす.
if (offMonth < thisMonth + 1) {
offYear = thisYear + 1;
}
let targetOffDay = new Date(offYear, offMonth - 1, offDate);
// console.log(targetOffDay); // (例) Mon May 08 2023 00:00:00 GMT+0900 (Japan Standard Time)
// カレンダーに登録する.
createOffDay(calendar, targetOffDay);
}
// LINE Notify で通知する.
lineNotify(token, pTag);
}
カレンダー ID とパーソナルアクセストークンをソースコードに書くわけにはいかないので,スクリプトプロパティに保存しておきます.
また,保存された値を参照する際は PropertiesService.getScriptProperties().getProperty()
を用います.
スクリプトプロパティの使い方については, [7] が参考になるでしょう(他にも沢山あります).
動作確認
試しにスクリプトを実行させてみます.
実行結果を以下に示します.
画像のように LINE Notify から通知が届くこと,および Google カレンダーに割引セールの開催日が登録されることを確認できました.
また,スクリプトを実行した日は 2022年8月7日 で,割引セールの開催日は 5/8 (土) なので,年越しを考慮していることも確認できました.
スクリプトが正しく動作することを確認できたので,最後にトリガーを設定すれば完成です.
終わりに
今回は「SEIYU の割引セールの開催日を Google カレンダーへ登録する作業の自動化」を紹介しました.
このスクリプトは,2021年2月から2022年3月までスケジュール自動登録 Bot として活躍してくれました.
また,完成品をイメージしやすかったため,面接での話の種としても活躍してくれました.
皆さんも是非ともスケジュール自動登録 Bot を開発して,日常生活と就職活動へ役立てましょう.
参考
- 株式会社クレディセゾン:株式会社西友との提携サービス終了について,クレジットカードは永久不滅ポイントのセゾンカード(オンライン),入手先〈https://www.saisoncard.co.jp/static2/lp/14xu202201/app.html〉(参照 2022-08-06).
- Internet Archive:Wayback Machine(オンライン),入手先〈https://archive.org/web/〉(参照 2022-08-06).
- 西友:西友南草津店 – 店舗詳細(オンライン),入手先〈https://web.archive.org/web/20210506131022/https://www.seiyu.co.jp/shop/%E8%A5%BF%E5%8F%8B%E5%8D%97%E8%8D%89%E6%B4%A5%E5%BA%97/〉(参照 2022-08-06).
- てつお:【GAS】Parserライブラリの使い方|インストール手順と使用方法,平社員のプログラミングブログ(オンライン),入手先〈https://tetsuooo.net/gas/1944/〉(参照 2022-08-06).
- 初心者でもわかるGoogle Apps Script活用のススメ:【コピペで使える】GASでIDを使ってカレンダーを取得してみる(オンライン),入手先〈https://for-dummies.net/gas-noobs/how-to-get-calendar-by-id-via-gas/〉(参照 2022-08-06).
- スキプラ@元エンジニア:GAS超入門⑦ – LINEに通知してみる,note(オンライン),入手先〈https://note.com/skipla/n/nefdfa2abd350〉(参照 2022-08-06).
- 経営管理deプログラミング:GAS新IDEでスクリプトプロパティ管理GUI機能復活〜スクリプトの並び順変更・タイムゾーン設定も追加〜(オンライン),入手先〈https://admin-it.xyz/gas/gas-new-ide-update/〉(参照 2022-08-06).