はじめに
僕のインターン先の会社が先日、六本木から水道橋に移転しました。
水道橋といえば?そう、東京ドームの最寄駅です。東京ドームは日本におけるエンターテインメントの聖地といっても過言ではないでしょう。僕もポール・マッカートニーやボン・ジョヴィのライブを観に行きましたおっさん臭っとか言わない。
間も無くプロ野球も開幕し、東京ドームそして水道橋駅の賑わいは増していくでしょう。
そこで困るのは水道橋近辺で働く人々です。
特にイベントのない日であれば、総武線は座って帰ることができるのですがイベントのある日はメチャ混みです。しかもライブや野球観戦後のテンションの高い人たちでメチャ混みです。お疲れモードで帰る人々にはややキツイものがあります。
というわけでイベント開催日には混雑を避けて帰るための目安とできるようにと、Slackに日毎の東京ドーム周辺のイベント情報を自動で通知するbotを作成しました。
方針
方針としては
1. 東京ドームのイベント情報を含むHTMLをスクレーピングして取得する
2. 各イベントの情報をテキストとして取得する
3. Slackに送信する
です。
実装
実装はGoogle Apps Scriptで行います。
新規スクリプトを作成するためにはGoogle Apps Scriptの管理ページの左上の「新規スクリプト」から行いましょう。
(普通はGASはスプレッドシートに紐付けることがほとんどのためスプレッドシートから作成しますが、今回はスプレッドシートを経由しないので。この辺は@t_imagawaさんのGoogle Apps Script 入門に詳しく説明があります。
)
1. 東京ドームのイベント情報を含むHTMLをスクレーピングして取得する
東京ドームのイベント情報はこちらのHPに掲載されています。
このページにアクセスするとその日に東京ドームシティで開催予定のイベントの一覧が掲載されています。
Chromeのデベロッパーツールでソースを確認してみると、イベント情報は<div class="c-mod-search-result">...</div>
の中に含まれているようです。
まずはこの部分を取り出してみましょう。
function scraping() {
var url = 'https://www.tokyo-dome.co.jp/event/';
var response = UrlFetchApp.fetch(url);
var source = response.getContentText('UTF-8');
var searchTag1 = '<div class="c-mod-search-result">';
var index1 = source.indexOf(searchTag1);
var target = source.substring(index1 + searchTag1.length);
var searchTag2 = '<!-- /c-mod-search-result -->';
var index2 = target.indexOf(searchTag2);
target = target.substring(0,index2);
Logger.log(target);
}
UrlFetchApp.fetch(url)
では指定したURLにアクセスしHTTPレスポンスを受け取ります。そのレスポンスに対してgetContentText(charset)
メソッドを行いページのHTMLを取得します。
その後、ターゲットとなる部分の前後のタグの位置を検索して切り出し、ターゲットのテキストを取り出します。
しかし・・・
欲しい部分がごっそり抜けている。。。
ここからスクレーピング初心者の僕は小一時間悩みました。
こういう時はGoogle様に聴けば大体解決するものです。
案の定ありました。
GASでPhantomJS Cloudを利用してWebページをスクレイピング
GASユーザーの多くがお世話になっているであろう隣ITさんです。
上記ページに詳しく書いてあるので詳細は割愛しますが、先ほどのスクリプトがなぜうまくいかなかったかというと、「UrlFetch.fetch(url)
はJavaScript実行前のHTMLを取得しているのでJavaScriptが埋め込まれていいるサイトのHTMLはうまく取得できない」からなんですね。
それを解消するために「PhamtomJSを利用してJavaScript実行後のHTMLを取得しよう」というわけです。
隣ITさんの記事に従ってスクリプトを書き直したものがこちら。
function scraping() {
const URL = 'https://www.tokyo-dome.co.jp/event/';
var key = '****************************';
var option =
{url:URL,
renderType:"HTML",
outputAsJson:true};
var payload = JSON.stringify(option);
payload = encodeURIComponent(payload);
var url = "https://phantomjscloud.com/api/browser/v2/"+ key +"/?request=" + payload;
var response = UrlFetchApp.fetch(url);
var json = JSON.parse(response.getContentText());
var sourceAll = json["content"]["data"];
var startTag = '<div class="c-mod-search-result">';
var endTag = '<!-- /c-mod-search-result -->';
var target = cut(sourceAll, startTag, endTag);
return target;
}
function cut(source, startTag, endTag){
var index1 = source.indexOf(startTag);
var target = source.substring(index1 + startTag.length);
var index2 = target.indexOf(endTag);
target = target.substring(0,index2);
Logger.log(target);
return target;
}
HTMLから始まりと終わりを指定して抜き出す操作はこれからたくさん出てくるのでcut()
という関数にまとめました。
このログをみてみると・・・
それっぽいのが出てきましたね。野球シーズン始まる感満載です。
2. 各イベントの情報をテキストとして取得する
イベント情報部分のHTMLを取得できたので、ここからは個々のイベントの情報をテキストとして取り出していきます。
function getEventsToday(source) {
//その日の日付をHTMLの該当箇所から取得
var date = cut(source, '<span data-json-event-text-target="">', '・すべて</span>');
//イベント情報を格納していく変数
var events = [date];
while(source.indexOf('<div class="grid-cmn__col col-lg-6 col-sm-12">') != -1){
var location = getLocation(source);
var title = getTitle(source);
//個々のイベントは連想配列に格納(キー名および"short: false"というのはslackに投稿するための準備)
var event = {value: '@' + location, title: title, short: false};
events.push(event);
var index = source.indexOf('<p class="c-txt-caption-01">');
source = source.substring(index + 30);
}
Logger.log(events);
return events;
}
function getLocation(source){
var locationSpan = cut(source,'<span class="c-txt-tag__item','span>');
var location = cut(locationSpan,'">','</');
return location;
}
function getTitle(source){
var titleSpan = cut(source,'<p class="c-ttl-linkblock"','p>');
//イベントタイトルに貼られたリンクの種類により、タイトルの切り出し方を変える
if(titleSpan.indexOf('class="c-link-01') == -1){
var title = cut(titleSpan,'>','</');//リンクなしの場合
} else if(titleSpan.indexOf('rel="noopener noreferrer">') == -1){
var title = cut(titleSpan,'class="c-link-01">','</');//サイト内リンクの場合
} else {
var title = cut(titleSpan,'rel="noopener noreferrer">','</');//外部リンクの場合
}
return title;
}
個々のイベント情報は以下のようなHTMLにより記述されています。
getEventsToday()
では、個々のイベントはgrid-cmn__col col-lg-6 col-sm-12
というclassを持ったdivに格納されているので、先頭からチェックしていき、このクラス名が見つからなくなるまで繰り返しイベント情報を取得しています。
getTitle()
でなぜif文により切り出し方を変えているかというと、イベント名には基本的に詳細ページへのリンクが貼られてるのですが、中には外部ページへのリンクだったらそもそもテキストのみでリンクでない場合があるようです、それに対応するためです。
これを実行すると以下のようなログが得られました。
HTMLのタグがちゃんと外れてテキストだけでイベント情報を格納できました。
3. Slackに送信する
ここからはSlack APIを利用してSlackにイベント情報を送信するための操作となります。
Slackにメッセージを送信するbotの作成に関しては @namutaka さんのSlack Botの種類と大まかな作り方に詳しいです。
以下では、Slackに送信するメッセージの中で今回採用したattachmentsという形式のメッセージを送信するのに渡すJSONを作っていきます。
function makeMessage(events){
var fields = [];
for(var i=1; i<events.length; i++){
fields.push(events[i]);
}
var message = {
"attachments": [
{
"fallback": "Required plain-text summary of the attachment.",
"color": "#36a64f",
"pretext": events[0] + "の東京ドームイベント情報です",
"title": "詳細はこちら",
"title_link": "https://www.tokyo-dome.co.jp/event/",
"fields":
fields
}
]
}
Logger.log(JSON.stringify(message));
return message;
}
attachments形式の中の"fields"というパラメータに先ほど作成した個々のイベントの連想配列をそのまま渡しています。(連想配列を作るときに"short:false"などを含めていたのはここで簡潔にするためでした。)
あとはこのJSONを渡して、botのwebhook用のURLにPOSTをすればメーセージを投稿できます。
function postToSlack(message) {
var url = 'https://hooks.slack.com/services/*********/***********/***************';
var options = {
"method" : "POST",
"headers": {"Content-type": "application/json"},
"payload" : JSON.stringify(message)
};
UrlFetchApp.fetch(url, options);
}
あとは一日一回実行するようにトリガーを設定すれば完成です。
さいごに
途中から説明が雑になった感が否めないです。すみません。
今回はスクレーピングの練習的な意味合いが強かったのでひとまず成功ということにしてますが、実用的な観点からすると、ご覧の通り全然人気なさそうなあまり人混みの原因とならなそうなイベントも含まれているので、「今日は混みそうですよ!!」 という日に限り通知するように改善していきたいです。