気象庁の防災情報をリアルタイムで受け取る

気象庁は試験的に、気象庁防災情報 XML フォーマットとしてリアルタイムに(プッシュ配信で) XML で防災情報を提供しています。
この公開 XML 電文は第三者への提供やサイトへの掲載といった二次利用が可能なため、いろいろな活用が出来そうです。
とりあえずなんとか受信するところまで実装出来ましたので、その手順を残しておこうと思います。

使用するプロトコルと実装

PubSubHubbub(PuSH) の仕組みを利用して HTTP でプッシュ配信されます。
気象庁からのデータが Hub に登録され、Hub から全ての購読者 (subscriber) へ配信される流れです。
この subscriber に当たる部分をこちらで用意する必要があります。

登録自体は気象庁の方が行いますので、その時に飛んでくる登録意思確認に正しく応答する部分と、実際にプッシュ配信されるデータを受け取る部分が実装できていれば OK です。

なお調べていくと PubSubHubbub Hub を使うような記述が見られますが、現在は Alert Hub を使っています。
登録意思確認のテストに PubSubHubbub Hub を使用すると、応答に改行が入っていても成功してしまいます。
しかしながら Alert Hub では失敗しますのでハマります。
気象庁から <登録失敗のお知らせ> という悲しいメールを受け取ることになりますのでご注意下さい。

そんなわけで登録意思確認のテストは必ず Alert Hub を使うようにして下さい。
プッシュ配信データは PubSubHubbub Hub でも問題ありませんでした。
配信する中身は こちら からお好きなものを選んで下さい。

Django で開発準備

今回は Django という Python のフレームワークを使用して開発しました。
前々から気になっていたものの使用するのは今回が初めてで、知らないプロトコルを知らないフレームワークを使って開発する事となり、なかなかに学習コストが高く苦労しました……。

慣れていないせいか結構調べるのにも苦労したので、同じ境遇の方の参考になればとメモ的なものも書いてみます。
だいぶ試行錯誤していたので抜けている部分も多いと思いますが。

普段はサーバ上でいきなりがしがし書いてしまう事が多いのですが、今回は MacBook 上で開発環境を作りサーバにデプロイする方法にしました。
これも普段あまりやらないので手間取る事もありましたが、結果的には分かりやすくなったので正解でした。
Python の virtualenv を使用しましたが便利ですね。

開発について、基本的には Django のチュートリアルを進めれば必要な事が全て詰まっています。
はじめての Django アプリ作成、その1

今回のように開発環境と本番環境が別の場合、本番環境にも Python と virtualenv が必要です。
また使用するデータベースに合わせたパッケージも必要になります。
本番環境でもデータベースを用意するまで sqlite3 で動作させたかったため、以下のように virtualenv と sqlite3 を導入しました。

pkg install py27-virtualenv
pkg install py27-sqlite3

virtualenv 環境内に Django を導入します。
本番環境では PostgreSQL を使うので psycopg2 もインストールしています。

virtualenv virtualenv
source virtualenv/bin/activate.csh
pip install django
pip install uwsgi
pip install psycopg2

開発環境では sqlite3 で、本番環境では PostgreSQL でと分けたかったので、Djangoの設定ファイルの読み込みを実行環境ごとに分けてみるを参考にして以下のように設定しました。

settings.py があるプロジェクトのディレクトリに develop_settings.py を作成し、以下のような内容で保存します。
(jma は django-admin startproject で指定したプロジェクト名に置き換えて下さい)

from jma.settings import *

DEBUG = True
ALLOWED_HOSTS = []

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}

settings.py には PostgreSQL の本番環境の設定を書いておけば、普通に起動した場合には本番環境として動作します。
開発環境として動作させるときには settings ファイルを明示的に指定します。

python manage.py runserver --settings=jma.develop_settings

Django で subscriber の開発

実際に開発していきます。
実装するものは GET で飛んでくる登録意思確認への応答と、POST で飛んでくるプッシュ配信データの受け取りです。

まずは登録意思確認。
動作としてはシンプルで、以下のような GET リクエストが来ますので hub.challenge に入ってくる文字列をそのまま返してあげれば OK です。
hub.verify_token はこちらから気象庁へ登録時に指定したものが入りますので、合っていれば成功という処理にすれば良さそうです。

GET /subscribe/?hub.verify_token=XXXXXXXX&hub.challenge=challengestrings&hub.topic=http%3A%2F%2Fxml.kishou.go.jp%2Fpath%2Fto%2Ffile.xml&hub.mode=subscribe&hub.lease_seconds=432000

パスは subscriber のエンドポイントが http://example.jp/subscribe/ の場合、パラメータは hub.mode と hub.lease_seconds 以外はダミーです。

前述の通り、hub.challenge を返す際に改行コードが入ると失敗しますので注意が必要です。
Django のテンプレートを vi で編集すると改行コードが入ってしまうので、以下のようにして改行コードが入らないようにテンプレートを作成しました。

echo -n '{{ message }}' > path/to/templates/subscribe/subscribe.html

実際にこのテンプレートに渡しているのはこんな感じです。

return render(request, 'subscribe/subscribe.html',
        {'message': request.GET['hub.challenge']})

Alert Hub のテストが通れば改行コードが付いていない正しいものです。

続いてプッシュ配信データの受け取り。
これは更に動作としてはシンプルで、受け取るだけならデータベースに押し込むだけです。
ただ Django は POST を受け取る際に CSRF のチェックが入りますが、今回は一方的に投げつけられるだけなので頭の方に以下を書いて無効にします。

from django.views.decorators.csrf import csrf_exempt
@csrf_exempt

POST データの中身は request.body に格納されています。
そのまま保存して後で解析しても良いですが、今回は投げつけられた XML データから必要そうなデータを取り出して格納します。
投げつけられるデータにはある程度概要が含まれていますが、詳細は link タグの URL にアクセスする必要があります。
この URL は24時間程度でアクセス出来なくなるので、同時にアクセスして一緒に取ってきてしまいます。

# モデル名は適宜読み替えて下さい
from subscribe.models import Data, Publish
from xml.dom.minidom import parseString
from urllib2 import urlopen, URLError

import にどう書くのかちょっと悩んだ記憶がありますので念のため抜粋します。

xml = request.body
# 念のため生データを保存
rawdb = Data(xml=xml)
rawdb.save()

dom = parseString(xml)
elements = dom.getElementsByTagName('entry')

for element in elements:
    uuid    = element.getElementsByTagName('id').item(0).childNodes[0].data
    publish = element.getElementsByTagName('updated').item(0).childNodes[0].data
    title   = element.getElementsByTagName('title').item(0).childNodes[0].data
    content = element.getElementsByTagName('content').item(0).childNodes[0].data
    link    = element.getElementsByTagName('link').item(0).getAttribute('href')
    try:
        response = urlopen(link)
        data = response.read()
    except URLError, e:
        data = 'ERROR: ' + e.reason

    db = Publish(uuid=uuid, publish=publish, title=title, content=content,
            link=link, data=data)
    db.save()

return render(request, 'subscribe/push.html', {'message': 'OK'})

OK の文字列を返していますが、200の応答なら何でも問題ないようです。
XML のパースは慣れていなかったので悩みました。

ここまで説明してきた2つの動作は、request.method を見て GET か POST かで判断しています。
エラーの場合は以下のように404を返すようにします。

from django.http import Http404
raise Http404("Error")

本番サーバを Nginx + uWSGI で設定

いよいよ本番サーバの設定です。
基本的には Django, uWSGI, Nginx on Freebsd を参考にしていただければ OK ですが、supervisord.conf がそのままではうまくいきませんでした。
最終的に以下のように。

directory = /path/to/django-project/
command = /usr/local/bin/uwsgi -s /var/tmp/%(program_name)s.sock --chmod-socket=666 --home=/path/to/django-project/virtualenv --chdir=/path/to/django-project/jma --module=jma.wsgi --uid=80 --gid=80 --processes=1 --threads=10
stdout_logfile = /var/log/%(program_name)s_stdout.log
stderr_logfile = /var/log/%(program_name)s_stderr.log
startsecs = 10
stopsignal = QUIT
stopasgroup = true
killasgroup = true

root で実行したくなかったので、ソケットファイルを置くディレクトリを /var/tmp 以下にしています。
jma はプロジェクト名です。

本番環境でも admin を使用する場合、このままでは CSS などの静的コンテンツにアクセス出来ません。
Nginx で /static/ へのアクセスを設定し、静的コンテンツをコピーします。

Nginx の設定は以下のように。

location /static {
    alias /path/to/django-project/jma/static;
}

静的コンテンツのコピーは Django の機能としてあります。
settings.py に以下のように静的コンテンツの場所を書きます。

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')

次に以下のように入力するとコピーされます。

python manage.py collectstatic

これで完了です。

登録完了までの流れ

テストも無事終了したらユーザー登録についてを参考にメールで気象庁に登録依頼を出します。
2週間くらいかかるかもという記述がありましたが、水曜日の21時過ぎに出して翌週月曜日の朝7時(!)に作業してもらいました。
しかし悲しいかな届いたのは <登録失敗のお知らせ> でした。
そうです、改行コードの罠にはまりました。
しかし午前中に急いで修正して修正した旨を返信したところ午後一には <登録完了のお知らせ> がきました。
凄い、気象庁の人凄い。

プッシュ配信データも確認できました。
まだ保存するだけの状態なので、これから活用する部分を作成していこうと思います。

参考

以下のサイトを参考にさせて頂きました。
ありがとうございました。