EEW/揺れ検知トリガーの作成をしようと思ったが
これでGPTで行うと実際に揺れを検知するような仕組みを提案される
そうではなく
前回の津波警報の時にはロシアのカムチャッカ半島沖の地震で
津波警報が出ていたので、そういった情報を検知できるようにしたい
また提案の中で
*JMA防災情報XMLの実パーサ(地震/津波の要素抽出)*
とあったが、これがどのようなものかを知りたい
回答は
「端末の揺れを検知する」のではなく、気象庁の公式配信(JMA防災情報XML)から“津波関連の電文”を監視してトリガーを発火させたい
JMA防災情報XMLの“実パーサ”とは
気象庁は“PULL型”のAtomフィードを公開しています。代表的には:
* 地震・火山用(高頻度):eqvol.xml(毎分更新)
ここから「津波警報・注意報・予報」「津波情報」「震源・震度」などの電文へのリンクを取得します。 data.e-gov.go.jp
* 随時情報(高頻度):extra.xml(毎分更新)
広義の警報・注意報系の新着が流れます。用途に応じて併用。 data.e-gov.go.jp
配信の概要・取得方法は公式「防災情報XML(PULL型)」にまとまっています(Pushは終了済み)。 xml.kishou.go.jp+1
例:カムチャッカ半島沖で地震発生 → 日本沿岸に影響が見込まれる場合、**「津波警報・注意報・予報」「津波情報」**の電文が発表され、上記フィードに載ります。発表・解除・更新も電文として流れます。
「実パーサ」とは何か?
ここでの“実パーサ”は、JMA XMLの仕様に沿って、関心の要素(見出し、発表種別、対象地域、予想津波高、発表時刻、取消/解除、更新理由など)を確実に抽出する処理を指します。
JMA XMLは複数の名前空間・電文種別で構成され、代表的な抽出ポイントは以下です:
* ヘッダ系(共通)
Head/Title, Head/ReportDateTime, Head/TargetDateTime, Head/Headline/Text, Head/InfoKind, Head/InfoType(発表/訂正/取消 など) xml.kishou.go.jp
* 津波系
Body/Tsunami(jmx_tsunami:名前空間)やInformation[@type=”津波予報領域表現”]配下の
Item/Kind/Name(大津波警報/津波警報/津波注意報/津波予報 等)と
Areas/Area/Name, Code(津波予報区とコード)を列挙。更新・解除時もここに反映。 xml.kishou.go.jp+1
* 地震系(参考)
震源・震度情報やEEW(公開範囲に制約あり)等のjmx_seis:要素。津波判断の参考に使うことがあります(ただし“揺れ検知”はしない)。 data.e-gov.go.jp
JMAは詳細な解説・例示を公開しています(2012/2013年版などが基本仕様をカバー)。パース時の要素階層・コード表はここを参照するのが安全です。 xml.kishou.go.jp
運用のコツ(トリガー条件)
* 監視間隔:毎分。If-Modified-Since等で負荷と無駄取得を抑制。 Qiita
* 一次トリガー:新規・更新電文のItem/Kind/Nameに
「大津波警報 / 津波警報 / 津波注意報 / 津波予報」が含まれる、かつ Areas/Area/Codeが対象地域に一致。 xml.kishou.go.jp
* 抑止・更新:Head/InfoTypeが「取消」「訂正」「追加」などの場合は状態遷移として扱う。
* 表示:JMAマップで最新状況を補助表示(UIリンク)。 気象庁
仕上げ:あなたのシステムへの組み込みポイント
1. 地域フィルタ:静岡沿岸など対象の津波予報区コードだけに絞る。
2. 状態管理:InfoTypeが取消のときは通知解除、訂正/追加は更新扱い。 xml.kishou.go.jp
3. 可視化:ダッシュボードにJMA公式の最新表示もリンクで添える(家族向けに分かりやすい)。 気象庁
4. 安全側設計:不明時は強めの文言で注意喚起(あなたの既存“安全側”方針に合わせる)。
5. ログ/再現性:電文URL・ReportDateTime・InfoKindVersion等を保存(事後検証が楽)。 気象庁
必要なら、対象予報区のコード表や「解除検知」「詳細(予想高、到達予想時刻が文面に含まれる場合の抽出)」まで組み込んだ“完成版パーサ”に拡張します。 この方向なら“揺れ検知ハードウェア”は不要で、カムチャッカ沖など国外震源でも日本向け津波電文を確実にトリガーできます。
生成されたコードを実行
import time
import requests
import feedparser
import xml.etree.ElementTree as ET
EQVOL_FEED = "https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml"
NAMESP = {
"jmx": "http://xml.kishou.go.jp/jmaxml1/",
"jmx_ib": "http://xml.kishou.go.jp/jmaxml1/informationBasis1/",
"jmx_eb": "http://xml.kishou.go.jp/jmaxml1/elementBasis1/",
"jmx_seis": "http://xml.kishou.go.jp/jmaxml1/body/seismology1/",
"jmx_tsunami": "http://xml.kishou.go.jp/jmaxml1/body/tsunami1/",
}
last_modified = None
seen_ids = set()
def fetch_feed():
headers = {}
if last_modified:
headers["If-Modified-Since"] = last_modified
r = requests.get(EQVOL_FEED, headers=headers, timeout=10)
if r.status_code == 304:
return None, None
r.raise_for_status()
return r.text, r.headers.get("Last-Modified")
def parse_entry_xml(url):
r = requests.get(url, timeout=10)
r.raise_for_status()
root = ET.fromstring(r.content)
head = root.find(".//jmx_ib:Head", NAMESP)
title = head.findtext("jmx_ib:Title", default="", namespaces=NAMESP)
report_dt = head.findtext("jmx_ib:ReportDateTime", default="", namespaces=NAMESP)
info_kind = head.findtext("jmx_ib:InfoKind", default="", namespaces=NAMESP)
info_type = head.findtext("jmx_ib:InfoType", default="", namespaces=NAMESP)
headline = head.findtext("jmx_ib:Headline/jmx_eb:Text", default="", namespaces=NAMESP)
# 津波関連の抽出(領域と種別)
areas, kinds = [], []
for item in root.findall(".//jmx_eb:Body//jmx_eb:Information[@type='津波予報領域表現']/jmx_eb:Item", NAMESP):
kind_name = item.findtext("jmx_eb:Kind/jmx_eb:Name", default="", namespaces=NAMESP)
kinds.append(kind_name)
for a in item.findall("jmx_eb:Areas/jmx_eb:Area", NAMESP):
areas.append({
"name": a.findtext("jmx_eb:Name", default="", namespaces=NAMESP),
"code": a.findtext("jmx_eb:Code", default="", namespaces=NAMESP),
})
return {
"title": title,
"report_dt": report_dt,
"info_kind": info_kind,
"info_type": info_type,
"headline": headline,
"kinds": list(set(kinds)),
"areas": areas,
"url": url,
}
def is_tsunami_trigger(rec):
# 種別に“津波警報・注意報・予報/津波情報”などが含まれるものを対象
if "津波" not in (rec["info_kind"] or rec["title"] or rec["headline"]):
return False
# 警報/注意報/予報の明示があれば強トリガー
keywords = ["大津波警報", "津波警報", "津波注意報", "津波予報", "津波情報"]
if any(k in (rec["headline"] or "") for k in keywords):
return True
# 後方互換(kindsに入っていればOK)
return any(k in keywords for k in rec["kinds"])
def loop():
global last_modified
while True:
try:
feed_text, lm = fetch_feed()
if feed_text:
d = feedparser.parse(feed_text)
# 新しい順に処理
for e in d.entries:
if e.id in seen_ids:
continue
seen_ids.add(e.id)
rec = parse_entry_xml(e.link)
if is_tsunami_trigger(rec):
# === ここであなたの通知/保存パイプラインへ ===
print("[TSUNAMI TRIGGER]", rec["report_dt"], rec["headline"], rec["kinds"][:3], "areas=", len(rec["areas"]))
if lm:
last_modified = lm
except Exception as ex:
print("error:", ex)
time.sleep(60)
if __name__ == "__main__":
loop()
しかしこれだと動作しない
実際には処理が長すぎるのが原因だったようだ
以下の内容を notebook を再起動して実行したら
原因はだいたい次のどれかです:
1. いま津波系の電文が出ていない → フィルタに引っかからず1行もprintされない
2. eqvol.xmlは取れているが、電文のInfoKindやHeadlineに「津波」が無くて弾かれている
3. 取得自体が失敗(304やネットワーク)しているが、分かるログを出していない
4. notebookで無限ループを回していて、動いてはいるが“何もヒットしないから何も出ない”
まずは「取れている?パースできてる?何が弾かれてる?」を確認する**ドライラン(1回だけ走る)**を入れると状況が見えます。下のテスト版を一度そのまま実行してください(無限ループなし・デバッグprint多め)。
なので
import requests, feedparser, xml.etree.ElementTree as ET
EQVOL_FEED = "https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml"
NAMESP = {
"jmx": "http://xml.kishou.go.jp/jmaxml1/",
"jmx_ib": "http://xml.kishou.go.jp/jmaxml1/informationBasis1/",
"jmx_eb": "http://xml.kishou.go.jp/jmaxml1/elementBasis1/",
"jmx_seis": "http://xml.kishou.go.jp/jmaxml1/body/seismology1/",
"jmx_tsunami": "http://xml.kishou.go.jp/jmaxml1/body/tsunami1/",
}
def parse_entry_xml(url):
r = requests.get(url, timeout=15)
r.raise_for_status()
root = ET.fromstring(r.content)
head = root.find(".//jmx_ib:Head", NAMESP)
title = head.findtext("jmx_ib:Title", default="", namespaces=NAMESP)
report_dt = head.findtext("jmx_ib:ReportDateTime", default="", namespaces=NAMESP)
info_kind = head.findtext("jmx_ib:InfoKind", default="", namespaces=NAMESP)
info_type = head.findtext("jmx_ib:InfoType", default="", namespaces=NAMESP)
headline = head.findtext("jmx_ib:Headline/jmx_eb:Text", default="", namespaces=NAMESP)
# 津波領域の抽出
areas, kinds = [], []
for item in root.findall(".//jmx_eb:Body//jmx_eb:Information[@type='津波予報領域表現']/jmx_eb:Item", NAMESP):
kind_name = item.findtext("jmx_eb:Kind/jmx_eb:Name", default="", namespaces=NAMESP)
if kind_name:
kinds.append(kind_name)
for a in item.findall("jmx_eb:Areas/jmx_eb:Area", NAMESP):
areas.append({
"name": a.findtext("jmx_eb:Name", default="", namespaces=NAMESP),
"code": a.findtext("jmx_eb:Code", default="", namespaces=NAMESP),
})
return {
"title": title, "report_dt": report_dt,
"info_kind": info_kind, "info_type": info_type,
"headline": headline, "kinds": list(set(kinds)),
"areas": areas, "url": url
}
def is_tsunami_trigger(rec):
# “津波”を広めに判定(タイトル・見出し・InfoKind)
text_pool = " ".join([rec.get("title",""), rec.get("headline",""), rec.get("info_kind","")])
if "津波" not in text_pool:
return False
# 強ワード
kw = ["大津波警報","津波警報","津波注意報","津波予報","津波情報"]
if any(k in text_pool for k in kw):
return True
# あるいはKindsに入っていればOK
return any(k in rec.get("kinds", []) for k in kw)
# ---- ドライラン:feed取得→最新5件だけ詳細をなめて出力 ----
r = requests.get(EQVOL_FEED, timeout=15)
print("feed status:", r.status_code, "bytes:", len(r.content))
d = feedparser.parse(r.text)
print("entries:", len(d.entries))
for e in d.entries[:5]:
print("ENTRY:", e.get("published",""), e.get("title",""))
try:
rec = parse_entry_xml(e.link)
print(" InfoKind:", rec["info_kind"], "| InfoType:", rec["info_type"])
print(" Title :", rec["title"])
print(" Headline:", (rec["headline"] or "")[:120])
print(" kinds :", rec["kinds"], "| areas:", len(rec["areas"]))
print(" -> TRIGGER?", is_tsunami_trigger(rec))
except Exception as ex:
print(" ERROR parsing:", ex)
を実行
feed status: 200 bytes: 27328 entries: 37 ENTRY: 降灰予報(定時) InfoKind: 降灰予報 | InfoType: 発表 Title : 火山名 薩摩硫黄島 降灰予報(定時) Headline: kinds : [] | areas: 0 -> TRIGGER? False ENTRY: 降灰予報(定時) InfoKind: 降灰予報 | InfoType: 発表 Title : 火山名 諏訪之瀬島 降灰予報(定時) Headline: kinds : [] | areas: 0 -> TRIGGER? False ENTRY: 降灰予報(定時) InfoKind: 降灰予報 | InfoType: 発表 Title : 火山名 霧島山(新燃岳) 降灰予報(定時) Headline: kinds : [] | areas: 0 -> TRIGGER? False ENTRY: 降灰予報(定時) InfoKind: 降灰予報 | InfoType: 発表 Title : 火山名 浅間山 降灰予報(定時) Headline: kinds : [] | areas: 0 -> TRIGGER? False ENTRY: 降灰予報(定時) InfoKind: 降灰予報 | InfoType: 発表 Title : 火山名 草津白根山(白根山(湯釜付近)) 降灰予報(定時) Headline: kinds : [] | areas: 0 -> TRIGGER? False
もしこれが
entries=0 → フィード取得が怪しい(ネットワーク・一時ブロック)。requests.get(EQVOL_FEED).text[:2000]をprintして中身を確認
entries>0かつInfoKindやTitleに「津波」が無い → 正常(いま津波電文が無いだけ)
parse_entry_xmlでエラー → XML名前空間/パスが電文種別で違う可能性。print(rec[“url”])で該当XMLを保存して中身を見るのが早い
というように原因を理解できる
補足(実運用へ進めるとき)
静岡沿岸の津波予報区コードでareasをフィルタ(対象外は通知しない)
InfoType == “取消”で状態を解除に遷移
併せてextra.xmlも監視すると取りこぼしが減ります(仕様差し込みは後でOK)
ノートブックではなく常駐スクリプト/systemdで運用し、ログをローテーション
他にやったことは
1. 通信・ネットワークの確認
まず、最初の requests.get(EQVOL_FEED) で通信が成功しているかを確かめる
import requests
EQVOL_FEED = "https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml"
r = requests.get(EQVOL_FEED, timeout=15)
print("status:", r.status_code)
print(r.text[:1000]) # 最初の1000文字だけ表示
✅ status: 200 なら通信成功。
❌ もし 403 や Timeout が出た場合 → 通信がブロックされている(VPN・プロキシ・ファイアウォールなどの影響)。
結果は
status: 200
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" lang="ja">
<title>é«é »åº¦ï¼å°éç«å±±ï¼</title>
<subtitle>JMAXML publishing feed</subtitle>
<updated>2025-11-06T05:01:14+09:00</updated>
<id>https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml#short_1762372874</id>
<link rel="related" href="https://www.jma.go.jp/"/>
<link rel="self" href="https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml"/>
<link rel="hub" href="http://alert-hub.appspot.com/"/>
<rights type="html"><![CDATA[
<a href="https://www.jma.go.jp/jma/kishou/info/coment.html">å©ç¨è¦ç´</a>,
<a href="https://www.jma.go.jp/jma/en/copyright.html">Terms of Use</a>
]]></rights>
<entry>
<title>éç°äºå ±ï¼å®æï¼</title>
<id>https://www.data.jma.go.jp/developer/xml/data/20251105200049_0_VFVO53_010000.xml</id>
<updated>2025-11-05T20:00:00Z</updated>
<author>
<name>æ°è±¡åº</name>
</author>
<link type="application/xml" href="https://ww
2. フィード構造が読めているか確認
feedparser がXMLを正しく認識していないと、entries が空になる
import feedparser
feed = feedparser.parse("https://www.data.jma.go.jp/developer/xml/feed/eqvol.xml")
print("entries:", len(feed.entries))
for e in feed.entries[:5]:
print(e.title, e.link)
の結果は
カーネル・出力の健全性チェック
これで何も出ないなら、セルが実行されていない/カーネルが固まっている/出力パネルが壊れている可能性が濃厚
import sys, time, os, pathlib, subprocess
print("STEP A1: print works?", flush=True)
sys.stdout.write("STEP A2: stdout.write works?\n"); sys.stdout.flush()
sys.stderr.write("STEP A3: stderr works?\n"); sys.stderr.flush()
for i in range(3):
print(f"tick {i}", flush=True)
time.sleep(0.5)
# ファイル出力で実行確認(UI出力が壊れていても痕跡が残る)
pathlib.Path("notebook_alive.txt").write_text("alive\n")
print("STEP A4: wrote notebook_alive.txt", flush=True)
# サブプロセスの標準出力も確認
ret = subprocess.run(["echo", "STEP A5: subprocess echo"], capture_output=True, text=True)
print(ret.stdout, end="")
print("STEP A DONE", flush=True)
結果は
STEP A1: print works? STEP A2: stdout.write works? STEP A3: stderr works? tick 0 tick 1 tick 2 STEP A4: wrote notebook_alive.txt STEP A5: subprocess echo STEP A DONE
処理そのものが終わらないのが原因なので原因を探す