気象庁が、2012年12月、ホームページで防災情報XMLというXML形式でのデータ提供を開始しました。今までは気象庁と専用線で接続されていた一部の報道機関、民間気象会社、研究機関等しか利用できなかったものが誰でもホームページから利用できるようになったので、これは大きな進歩と言えるでしょう。一方、このデータを利用するためには、PubSubHubbubと呼ばれるプロトコルに対応したsubscriberを用意する必要があり、単にRSSリーダーやウェブクローラーでURLを監視していればデータを入手できるというものでもありません。ここでは、まずsubscriberの作成について、今さら感はありますが、簡単に解説します。
気象庁防災情報XML(以下「JMX」)は、各種電文(天気予報や地震情報など)が発表されると、まずPubSubHubbubのハブと呼ばれるサーバーを介してPOSTリクエストにより配信通知が送られてきます。そこで、まずsubscriberには、これに含まれるURLをたどって更新されたXMLを入手するという機能が必要です。加えて、ハブは定期的に(数日間に一回)更新を継続して受け取る意思があるかGETリクエストにより確認してきますので、これに回答する購読意思確認の機能が必要です。これらの2つのまったく異なる機能を単一のURLで提供しなければいけないのが少々やっかいなところです。詳しくは気象庁ホームページ「電文公開の仕組み」に書かれていますが、結局のところ、大きなフレームワークとしては、
- POSTリクエストなら配信通知
- GETリクエストなら購読意思確認
- それ以外はエラー
といったものを用意すれば良いことになります。
購読意思確認
本来PubSubHubbubのフィード受信登録はsubscriber側で行うのですが、今回のJMX公開は一応「試行的」という位置づけのため、気象庁側で行います。このため、あらかじめ気象庁に届け出た情報をベースに、ハブから「引き続き購読するってことでいいの?」と確認が来ます。
この購読意思確認はGETリクエストで行われ、パラメータとして、
hub.mode subscribeまたはunsubscribe
hub.topic フィードのURL
hub.verify_token 気象庁に届け出た任意の文字列
hub.challenge ランダムな文字列
が用いられます。基本的には、(1) hub.modeが”subscribe”であり、(2) hub.topicが http://xml.kishou.go.jp で始まる有効なURLであり、さらに(3) hub.verify_token が(自分が)気象庁に届け出た任意の文字列であること、を確認できれば良いでしょう。これらのパラメータに問題がなければ、ステータスコードは200、本文にはhub.challengeで与えられた文字列をそのまま応答として返すと、購読意思を確認したことになります。このとき、hub.challengeはそのまま返す必要があり、前後に無駄な空白や改行文字を加えてはならないことに注意が必要です。パラメータに問題があるようであれば、ステータスコード 404 を返します。
atomフィードによる配信通知の受信
基本的には、POSTリクエストで送られてきたHTTPボディ部分をフィードとして読み取ります。ここで送られてきたデータが真正なものかどうか確認したいのですが、現段階ではできないものと考えて良いでしょう。非公式には、HTTPヘッダ中のX-Hub-Signatureヘッダがhub.verify_tokenをキーとした(フィードの)ハッシュであるという情報もありますが、これが将来的に保証されているわけではありません。また、User-AgentヘッダがAppEngine-GoogleでありIPアドレスがGoogleのものであることが確認できれば「それっぽい」ことは言えるでしょうが、Googleの持つIPアドレス空間は膨大であり、また、User-Agentも将来にわたって固定されるわけではありませんから、あくまで「それっぽさ」(尤度)を確認できるにとどまります。
さて、Atomフィードには電文自体は含まれていないので、具体的な電文の中身はAtomフィード中に含まれるURLをたどる必要があります。Atomフィードは複数の電文の更新情報を含んでいますし、それぞれの電文のダウンロードに数分以上かかる場合もありますので、いったんフィードをキューに放り込んで後から解析しながらダウンロードするなどの手法も良いでしょう。
気象庁の防災情報XMLを受け取るためには、以上の2つの機能を実装していく必要があります。ざっとネットで検索したところでは、素直な実装として、
- 気象庁の防災情報PuSH試行試行配信を受信するサブスクライバCGIの例(Perl by walkureさん)
- 気象庁防災情報XMLフォーマットを受信するためのPubSubHubbubサブスクライバ(node.js by まぎすとるさん)
あたりが見つかります。ちなみに当サイトではPHPを用いてデータベースにAtomフィードとともにXML電文を保存しています。大まかなフレームワークは以下のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
<?php define("VERIFY_TOKEN", /* hub.tokenに相当するもの */); switch ($_SERVER["REQUEST_METHOD"]) { case "GET": verify_subscription(); break; case "POST": receive_feed(); break; default: syslog(LOG_WARNING, "Unsupported method $rmethod"); error_exit(418, "I'm a teapot", "An attempt was made to brew coffee with a teapot."); } exit; function verify_subscription() { if (! verify_subscription_main()) { syslog(LOG_WARNING, "Subscription verification failed."); error_exit(404, "Not Found", "The requested URL " . $_SERVER["SCRIPT_NAME"] . " was not found on this server." ); } // store_subscription(); header("HTTP/1.1 200 OK"); print $_GET["hub_challenge"]; syslog(LOG_INFO, "Subscription verification successful."); exit; } function verify_subscription_main() { $hub_mode = $_GET["hub_mode"]; $hub_topic = $_GET["hub_topic"]; $hub_challenge = $_GET["hub_challenge"]; $hub_verify_token = $_GET["hub_verify_token"]; $remote_host = remote_host(); /* add a remote_host check here */ if ($hub_mode == "" || $hub_topic == "" || $hub_challenge == "") { syslog(LOG_WARNING, "Either hub_mode, hub_topic, or hub_challenge is empty."); return false; } // see if the topic URL is a valid URL and starts from http://xml.kishou.go.jp if (parse_url($hub_topic) === false) { syslog(LOG_WARNING, "hub_topic is not a valid URL."); return false; } $pos = strpos($hub_topic, "http://xml.kishou.go.jp/"); if ($pos === false || $pos != 0) { syslog(LOG_WARNING, "hub_topic does not start with http://xml.kishou.go.jp/"); return false; } if ($hub_verify_token != VERIFY_TOKEN) { syslog(LOG_WARNING, "hub_verify_token $hub_verify_token != " . VERIFY_TOKEN); return false; } return true; } function receive_feed() { $xmlstr = file_get_contents("php://input"); $parsed = parse_feed($xmlstr); if ($parsed === false) error_exit(404, "Not Found", "The feed seems erroneous."); $hdr = $parsed[0]; $entries = $parsed[1]; // store_feed($hdr, $entries, $xmlstr); error_exit(200, "OK"); exit(0); } function parse_feed($content) { if (strlen($content) == 0) { syslog(LOG_WARNING, "The feed is empty."); return false; } libxml_use_internal_errors(true); $xml = simplexml_load_string($content); if ($xml === false) { syslog(LOG_WARNING, "XML parse error:"); foreach (libxml_get_errors() as $e) { logwrite(" + " . $e->message); } libxml_clear_errors(); return false; } $hdr = array(); $hdr["title"] = (string) $xml->title; $hdr["subtitle"] = (string) $xml->subtitle; $hdr["updated"] = (string) $xml->updated; $hdr["id"] = (string) $xml->id; $hdr["rights"] = (string) $xml->rights; foreach ($xml->link as $l) { $rel = (string) $l->Attributes()->rel; $href = (string) $l->Attributes()->href; $hdr[$rel] = $href; } foreach (array("id", "title", "updated", "related") as $tag) { if ($hdr[$tag] == "") { syslog(LOG_WARNING, "Missing $tag in feed."); return false; } } $entries = array(); foreach ($xml->entry as $e) { $ret = array(); $ret["title"] = (string) $e->title; $ret["id"] = (string) $e->id; $ret["updated"] = (string) $e->updated; $ret["author"] = (string) $e->author->name; $ret["link_href"] = (string) $e->link->Attributes()->href; $ret["link_type"] = (string) $e->link->Attributes()->type; $ret["content"] = (string) $e->content; $ret["link_type"] = (string) $e->link->Attributes()->type; $ret["content"] = (string) $e->content; foreach (array("id", "title", "updated", "link_href") as $tag) { if ($ret[$tag] == "") { syslog(LOG_NOTICE, "Missing $tag in feed entry."); } } array_push($entries, $ret); } return array($hdr, $entries); } ?> |
もちろん実際にはもう少し例外処理などを加えて頑強にする必要があります。また、フィード及びXML電文はデータベースに保存していますがその処理については上記スクリプトからは省いています。保存したデータについては、こちらの記事をご覧ください。
pubsubhubbubの構築の参考にさせていただいています。
気象庁の登録の前にでpubsubhubbubのサイトでsubscriberのテスト登録がのっていますがnoriさんは、問題なく登録できたでしょうか?
登録をかけるとステータス408と409が返ってくるのですがそのようなことはありませんでしたか?
ちなみに下記のサイトになります。
https://pubsubhubbub.appspot.com/subscribe
@どんさん、
コメントありがとうございます。PubSubHubbubのサイトでは特に問題ありませんでした。HTTP Status 408はタイムアウトなので、ひょっとするとサーバーの負荷が高まっているとか、そういったことかも知れません。409はとても珍しいエラーなので、そちらの事情はわかりませんが…