「気象庁防災情報XML」から全国の地震情報を(ほぼリアルタイムで)抽出し、Slackに通知できるようにする方法
はじめに
メディロムグループには、全国に300店舗以上のリラクゼーションスタジオがあります。そのため、社内から「全国の地震情報を検知して、お客様・スタッフ・店舗の安否確認と初動対応を素早くできる仕組みがほしい」という声があがりました。
そこで、信頼できる情報源として気象庁が提供するXMLフィードを利用させていただき、Google Apps Script(GAS)で処理をして、全国の地震速報をSlackへ自動通知するシステムを自作しました。
本記事では、同様のニーズを持つ方の参考となるよう、実装のポイントや仕組みを紹介します。
技術選定
最初はNode.jsでの実装を試みた
最初はNode.jsを使い、地震情報のデータを取得してSlackに通知する仕組みを作りました。
デプロイ先はRenderを使おうとしましたが、無料プランだと15分アクセスがないとスリープしてしまい、その問題を対処するための仕組みを加える必要がありました。
そこでGoogle Apps Script(GAS)に切り替え
Renderのスリープ問題を回避するために、Google Apps Scriptを使うことにしました。
GASはGoogleのインフラ上で動くため、スリープの心配がなく、トリガーによる定期実行も簡単に設定可能です。
必要なもの
- Googleアカウント
- SlackのIncoming Webhook URL
参考:Kohさんの「Slack Webhook URLの取得方法」 - Google Apps Script(GAS)を使用([Googleスプレッドシート→拡張機能→Apps Script]から作成)
実装のポイント
-
気象庁が提供している地震速報のXMLデータを定期的に取得(GASのトリガーで設定)
-
XMLをパースして、震度や震源地、マグニチュードなどの情報を抽出
-
通知済みの地震情報はスクリプトプロパティにIDを保存し、重複通知を防止
-
震度に応じて通知するかどうかを判定
-
SlackにはWebhook経由でメッセージを送信
地震速報のXMLデータについて
地震速報には、気象庁が提供する「高頻度フィード(毎分更新)」を利用しています。
このフィードは、最新の地震速報を1分ごとに更新しており、直近10分以内に発表された情報を常に掲載しています。
🔗 気象庁:高頻度地震情報フィード
https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml
これにより、ほぼリアルタイムに近い形で地震速報をSlackに通知することが可能となっています。
スクリプトプロパティとは
Google Apps Script(GAS)のスクリプト全体で使えるキーと値のストレージ機能です。外部に公開されることはなく、スクリプト内でのみ参照・更新できる安全な保存場所として活用されます。
詳しくはこちら💁♂️↓
プロパティサービス
コード紹介
🔧 概要設定
const SLACK_WEBHOOK_URL = (セキュリティのためコード上には記載しない)
const JMA_FEED_URL = 'https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml';
const MIN_INTENSITY = 4.5;
🕛 日時フォーマット
function formatIsoToJapaneseDatetime(isoString) {
const date = new Date(isoString);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes().toString().padStart(2, '0');
return `${year}年${month}月${day}日 ${hour}時${minute}分`;
}
気象庁のフィードでは、地震発生時刻などの日付情報が ISO 8601 形式(例: 2025-07-04T12:58:00+09:00
)で提供されています。
このままだとSlack通知にはやや読みにくいため、こちらの関数を記述しています。
🔢 震度テキスト変換
function convertIntensityText(sindo) {
const map = {
"1": "1", "2": "2", "3": "3", "4": "4",
"5-": "5弱", "5+": "5強",
"6-": "6弱", "6+": "6強",
"7": "7"
};
return map[sindo] || sindo;
}
気象庁のフィードでは、震度が 5-
や 5+
のように符号付きで表記されています。
このままだと視認性に劣るため、Slack通知では 5弱
や 5強
といった一般的に馴染みのある表現にこちらの関数で変換しています。
✅ 通知済みIDの管理
function isAlreadyNotified(id) {
const scriptProperties = PropertiesService.getScriptProperties();
const notifiedIds = JSON.parse(scriptProperties.getProperty("notifiedIds") || "[]");
return notifiedIds.includes(id);
}
function markAsNotified(id) {
const scriptProperties = PropertiesService.getScriptProperties();
const notifiedIds = JSON.parse(scriptProperties.getProperty("notifiedIds") || "[]");
if (!notifiedIds.includes(id)) {
notifiedIds.push(id);
}
const maxEntries = 50;
const recentIds = notifiedIds.slice(-maxEntries);
scriptProperties.setProperty("notifiedIds", JSON.stringify(recentIds));
}
上記の2つの関数で、通知済みの地震IDをスクリプトプロパティに保存・参照し、重複通知を防止しています。
-
isAlreadyNotified(id) は、引数の地震IDが通知済みかどうかを確認します。
-
markAsNotified(id) は、通知した地震IDを記録します。
-
保存件数は最大50件に制限しており、古いIDは自動的に削除されます(メモリ節約・誤通知防止)。
🧹 プロパティ初期化(手動で実行)
function clearAllProperties() {
const scriptProperties = PropertiesService.getScriptProperties();
scriptProperties.deleteAllProperties();
Logger.log("すべてのスクリプトプロパティを削除しました");
}
この関数は、スクリプトプロパティに保存されている全データを削除します。
通知済みの地震情報はプロパティに記録されており、同じ情報が再度通知されないようになっています。そのため、「通知テストを再度行いたい」または「状態をリセットしたい」といった場合に、この関数を手動で実行することで、通知処理を最初から試すことができます。
📏 震度を数値化
function parseIntensity(shindo) {
const scale = {
"1": 1, "2": 2, "3": 3, "4": 4,
"5-": 4.5, "5+": 5,
"6-": 5.5, "6+": 6,
"7": 7
};
return scale[shindo] || 0;
}
この関数は、気象庁のフィードで文字列として提供される震度を数値に変換します。
数値に変換することで、震度が設定した閾値より大きいかどうかを判定する際に、比較や計算ができるようになります。
📩 Slack通知
function sendSlackNotification(info) {
const payload = {
text: `🔔 地震情報通知`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `⚠️ 地震が発生しました\n\n*震度:* ${info.formatIntensity}\n*発生時刻:* ${info.formatTime}\n*震源地:* ${info.location}\n*観測地域:* ${info.observedPrefString}\n*マグニチュード:* ${info.magnitude}\n*津波:* ${info.tsunamiText}`
}
}
]
};
UrlFetchApp.fetch(SLACK_WEBHOOK_URL, {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload)
});
}
-
info オブジェクトを受け取り、地震情報の各項目をSlackのメッセージ形式に組み込みます。
-
payload はSlackのWebhookへ送るメッセージデータです。
-
text と blocks を使って、視認性の高いフォーマットで通知を作成しています。
-
UrlFetchApp.fetch を使い、Webhook URLに対してPOSTリクエストを送信して通知します。
通知されるSlackメッセージの例
⚠️ 地震が発生しました
震度: 5強
発生時刻: 2025-07-04 12:58
震源地: 茨城県南部
観測地域: 茨城県、栃木県、埼玉県
マグニチュード: 5.6
津波: なし
🌐 地震情報の取得・通知
function checkEarthquake() {
const response = UrlFetchApp.fetch(JMA_FEED_URL);
const document = XmlService.parse(response.getContentText());
const entries = document.getRootElement().getChildren("entry", document.getRootElement().getNamespace());
const namespace = document.getRootElement().getNamespace();
for (const entry of entries) {
const title = entry.getChild("title", namespace).getText();
const id = entry.getChild("id", namespace).getText();
if (!title.includes("震源・震度に関する情報")) continue;
if (isAlreadyNotified(id)) continue;
const detailUrl = entry.getChild("link", namespace).getAttribute("href").getValue();
const detailResponse = UrlFetchApp.fetch(detailUrl);
const detailDoc = XmlService.parse(detailResponse.getContentText());
const report = detailDoc.getRootElement();
const bodyNs = XmlService.getNamespace('http://xml.kishou.go.jp/jmaxml1/body/seismology1/');
const ebNs = XmlService.getNamespace('http://xml.kishou.go.jp/jmaxml1/elementBasis1/');
const headNs = XmlService.getNamespace('http://xml.kishou.go.jp/jmaxml1/informationBasis1/');
const body = report.getChild("Body", bodyNs);
if (!body) return;
const earthquake = body.getChild("Earthquake", bodyNs);
if (!earthquake) return;
const magnitude = earthquake.getChild("Magnitude", ebNs).getText();
const location = earthquake.getChild("Hypocenter", bodyNs)
.getChild("Area", bodyNs)
.getChild("Name", bodyNs).getText();
const intensityText = body.getChild('Intensity', bodyNs)
?.getChild('Observation', bodyNs)
?.getChild('MaxInt', bodyNs)?.getText() || "0";
const intensity = parseIntensity(intensityText);
if (intensity < MIN_INTENSITY) return;
const time = report.getChild('Head', headNs)
.getChild('ReportDateTime', headNs)
.getText();
const formatTime = formatIsoToJapaneseDatetime(time);
const formatIntensity = convertIntensityText(intensityText);
const tsunamiText = body.getChild("Comments", bodyNs)
?.getChild("ForecastComment", bodyNs)
?.getChild("Text", bodyNs)?.getText() || "不明";
const observedPrefSet = new Set();
const observation = body.getChild('Intensity', bodyNs)?.getChild('Observation', bodyNs);
const prefs = observation?.getChildren("Pref", bodyNs) || [];
for (const pref of prefs) {
const maxInt = pref.getChild("MaxInt", bodyNs)?.getText();
const prefName = pref.getChild("Name", bodyNs)?.getText();
if (parseIntensity(maxInt) >= MIN_INTENSITY) {
observedPrefSet.add(prefName);
}
}
const observedPrefString = Array.from(observedPrefSet).join("、");
sendSlackNotification({
formatIntensity,
formatTime,
location,
magnitude,
tsunamiText,
observedPrefString
});
markAsNotified(id);
}
}
-
「震源・震度に関する情報」のみ対象とし、既に通知済みの地震はスキップ。
-
各地震の詳細情報(震源地、マグニチュード、最大震度、発生時刻、津波情報)を抽出。
-
震度を数値化し、設定した閾値以上の地震のみ通知。
-
最大震度を超えた地域(都道府県)をリストアップ。
-
取得した情報を整形してSlackに通知。
-
通知済みの地震IDを保存し、重複通知を防止。
この関数を定期実行することで、リアルタイムに近い形で社内Slackに地震速報を届ける仕組みになっています。
定期実行の設定
-
GASエディタの「時計マーク」から「トリガー」を開く
-
checkEarthquake 関数を定期実行に設定(例:1分ごと)
テスト & リセット
-
Slackに通知が届かないときはURLとWebhook設定を再確認
-
通知ID(スクリプトプロパティ)をリセットしたいときは clearAllProperties() を手動実行
おわりに
今回の開発では、自動化の便利さとGASの手軽さを改めて実感しました。
地震速報をSlackで手軽に受け取りたい方の参考になれば幸いです。
採用情報
メディロムグループでは以下のようなサービスを展開しています。
全国300店舗以上のリラクゼーションスタジオ「Re.Ra.Ku」
世界初!充電不要の活動量計「MOTHER bracelet」
ヘルスケアコーチングアプリ「Lav」
ヘルスケア領域に興味があるエンジニア、PMを絶賛募集中です!
少しでも興味を持っていただけた方は、ぜひ以下のリンクからエントリーしてみてください。
一緒に新しいヘルスケアサービスをつくっていきましょう🌟
Discussion