こんにちは!MAREMI(マレミ)の遠藤です。
年初に能登半島大震災が発生しました。
いつも地震は予想もしないところからやってきます。
南海トラフ地震や東京大地震など、まだまだ震災の怖さに私達は悩まされそうですね。
こうした台風対策、地震対策、パンデミックといった災害において、どのようにして事業を継続させるかという観点で、BCP(Business Continuity Plan:事業継続計画)対策というものが最近注目されているようです。
【やったこと】
そこで今回、以下Googleサービス、Slack、気象庁防災情報XMLを使って安否確認Botを作ってみました。
・Google App Script
・Google Forms
投稿イメージはこんな感じです。
Google App Scriptでは時間間隔を指定したバッチ処理ができます。
今回は10分ごとにプログラムが走って、新たに地震情報を検知したら、SlackにGoogleフォームのURLが追加された投稿を行うというBotを作成しました。
【気象庁防災情報XMLフォーマットについて】
気象庁が提供している防災情報XMLについては、以下URLにてアクセスできます。
https://www.data.jma.go.jp/developer/xml/feed/eqvol_l.xml
上記、赤枠で囲んでいる部分が地震に関する情報になっていて、そこが10分以内に発生していたら検知して地震があったとお知らせしてくれるという仕組みになります。
具体的には10分以内のentry要素内に「震度速報」というワードがあった場合に、そのlinkを参照します。
link内は以下のようになっており、Headline→titleとBody→Pref→Areaの情報を使って地震に関するテキスト情報を作成します。
【Google App Scriptに記述したプログラムコード】
今回作成したソースコードについては以下です。
初期値設定にある「google_form_url」と
postSlack関数内の「payload」にSlackのTokenとChannel情報を準備入力してもらえればすぐ使えます。
/**
* 気象庁の地震情報(高頻度フィード)を取得しSlackに安否確認を飛ばす
* http://xml.kishou.go.jp/xmlpull.html
*/
function getJisinkun() {
var feedUrl = 'https://www.data.jma.go.jp/developer/xml/feed/eqvol_l.xml';
var feedHtml = UrlFetchApp.fetch(feedUrl).getContentText();
var feedDoc = XmlService.parse(feedHtml);
var feedXml = feedDoc.getRootElement();
var feedentry = getElementsByTagName(feedXml, 'entry'); //xmlのentry要素を配列で取得
var atom = XmlService.getNamespace('http://www.w3.org/2005/Atom');
//初期値設定
var google_form_url = "https://forms.gle/xxxxxxxxxxxxx"
//気象庁のxmlデータ(feed)より、entry要素を繰り返し探索
var quakeInfo = "震度速報"; //地震情報
feedentry.forEach(function(value, i) {
var slackFlg = false; //slack送信フラグ
var combinedAreaString = ""; //地震情報
var text = ""; //Slack送信テキスト
var titleText = value.getChild('title', atom).getText(); //title
var linkText = value.getChild('link', atom).getAttribute('href').getValue(); //link
// 'published' 要素を取得し、存在する場合のみ処理を行う
var publishedElement = value.getChild('updated', atom);
if (publishedElement != null) {
var publishedText = publishedElement.getText();
var publishedDate = new Date(publishedText);
// 現在日時との差を計算 (ミリ秒単位)
var currentDate = new Date();
var timeDiff = currentDate.getTime() - publishedDate.getTime();
// 10分以内の地震情報のみを対象とする
// if (timeDiff <= 24 * 60 * 60 * 1000) {
if (timeDiff <= 10 * 60 * 1000) {
//titleが震度速報の場合(震度3以上が震度速報に該当)
if (titleText.includes(quakeInfo)) {
//気象庁のxmlデータ(data)の情報を取得
var dataUrl = linkText;
var dataHtml = UrlFetchApp.fetch(dataUrl).getContentText();
var dataDoc = XmlService.parse(dataHtml);
var rootElement = dataDoc.getRootElement();
var titleText = value.getChild('title', atom).getText(); //title
// 'Headline'タグの要素を取得
var headlineElements = getElementsByTagName(rootElement, 'Headline');
if (headlineElements.length > 0) {
var headlineElement = headlineElements[0];
var textElements = getElementsByTagName(headlineElement, 'Text');
if (textElements.length > 0) {
headlineText = textElements[0].getText();
Logger.log(headlineText);
}
}
// 'Pref'タグの要素を取得
var prefElements = getElementsByTagName(rootElement, 'Pref');
prefElements.forEach(function(pref) {
// 各'Pref'内の'Area'タグの要素を取得
var areaElements = getElementsByTagName(pref, 'Area');
areaElements.forEach(function(area) {
// 'Area'内の'Name'タグと'MaxInt'タグを取得
var nameElements = getElementsByTagName(area, 'Name');
var maxIntElements = getElementsByTagName(area, 'MaxInt');
if (nameElements.length > 0 && maxIntElements.length > 0) {
var name = nameElements[0].getText();
var maxInt = maxIntElements[0].getText();
var areaString = name + ": 震度" + maxInt + "; ";
// if(name.match(/東京都|神奈川県|埼玉県|千葉県|茨城県|大阪府|福岡県|岩手県/)){
combinedAreaString += areaString; // 文字列を結合
slackFlg = true; //送信フラグをtrue
// }
}
});
});
Logger.log(combinedAreaString);
text = "<!channel>\n【安否確認】\n" + headlineText + "\n\n【地震情報】\n" + combinedAreaString + "\n安否確認のため、各自リアクションを行ってください。\n" + google_form_url
postSlack(slackFlg, text)
}
}
}
});
}
function postSlack(slackFlg, text) {
//送信フラグがtrueの場合
if(slackFlg == true){
var url = "https://slack.com/api/chat.postMessage";
var payload = {
"token" : "xoxb-xxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"channel" : "xxxxxxxxxxxxx",
"text" : text
};
var params = {
"method" : "post",
"payload" : payload
};
// Slackに投稿する
UrlFetchApp.fetch(url, params);
Logger.log("送信完了");
}else{
Logger.log("送信なし");
}
}
/**
* @param {string} element 検索要素
* @param {string} tagName タグ
* @return {string} data 要素
*/
function getElementsByTagName(element, tagName) {
var data = [], descendants = element.getDescendants();
for(var i in descendants) {
var elem = descendants[i].asElement();
if ( elem != null && elem.getName() == tagName) data.push(elem);
}
return data;
}
【Google App Scriptの作成】
以下URLからGoogle App Scriptを表示します。
https://script.google.com/home
エディタ > コード.gsに上記で作成したプログラムコードを貼り付けます。
【Slack Appの作成】
Slack連携において、Slack APIを使います。Google App ScriptのライブラリからSlack Appを作成する必要があります。
ライブラリの右の+ボタンを押下し、以下Slack用のスクリプトIDを入力します。
1on93YOYfSmV92R5q59NpKmsyWIQD8qnoLYk-gkQBI92C58SPyA2x1-bq
上記キャプチャのようにライブラリの下にSlackAppというのが作成されていれば完了です。
【Slackアプリ】
以下からslack apiのページを表示します。
https://api.slack.com/apps
Create an App > From scratch > App Nameと連携先のWorkspaceを指定 > Create App
これでSlackアプリが作成されます。
【Slack Botの設定】
OAuth & Permissions > Scopes > Add an OAuth Scope を選択します
パーミッション選択では以下を追加します。
・chat:write
・chat:write.public
ページ上部にあるInstall to Workspaceをクリックし、アクセス権限を許可すれば、Slack BotのWorkspaceへの導入が行われます。
OAuth Tokens for Your Workspace に Bot User OAuth Token が表示されていれば完了です。
上述したプログラム内のpayload内のtokenに記述します。
【Slack上でBotによる投稿を行いたいチャンネルIDを取得する】
投稿したいチャンネル上で、右クリック > コピー > リンクをコピー します。
上述したプログラム内のpayload内のchannelに記述します。
【Google App Scriptでのデプロイ作業】
GAS上の右上にあるからデプロイから新しいデプロイを作成し、情報を入力してデプロイで完了します。
【Google App Scriptのトリガー設定】
Google App Scriptのトリガーに移動します。
トリガーを追加から、実行対象の関数(ここではgetJisinkun)を選択し、時間などトリガーを発生させたい条件を入力します。
以上で完了です。