追記
2020-09-13 追記:作り直しました。 新しい記事をご覧ください
https://qiita.com/ryo-a/items/4468ffdbf251ec31dace
以下は古い記事です。
ryo-a と申します。
普段は大学生(B2)をやりつつ、ドワンゴの教育事業本部にてアルバイトとして N予備校プログラミング教材の編集をお手伝いしたり、ElectronというフレームワークのWebサイト/ドキュメント翻訳に参加したりしております。
ちょっと前までは C# で Windows 向けアプリケーションを書いていましたが、最近はもっぱら Node.js で遊んでいます。
こんなもの作りました
http://jjwd.info/
JSONized Japanese Weather Data - Developer Friendly Open Data Optimization Project
正直、CSV を JSON に変換して静的コンテンツとして配信しているだけなのでAPIです!と宣言しづらいのですが、便宜上APIと呼称します。
(やろうかなあ、という機運自体は夏ごろからあったのですが、長らく放置していました。11日のアドカレ枠を取得してから急いで立ち上げたのは内緒)
もし不具合がありましたら Twitter @jjwd_info などにお知らせください。
名称とロゴについて
jsonfied か jsonized か迷ったのですが、 Google検索のヒット数から後者の方が自然と判断しました。(プログラミング用語関連のコーパス欲しいですね)
ロゴは日本式の天気記号をモチーフにして「JJWD」の文字列を表しています。
気象庁のオープンデータをJSONにしたい
ここ数年、行政機関などが積極的にオープンデータの取り組みを進めており、気象庁もずAMeDASデータや予報・警報などの情報を公開してくれていますが、扱いづらいCSV(後述)だったり登録が必要なXMLだったり、「とりあえず使ってみるか」という時にはなんだか面倒な感じです。
幸いなことに再利用・再配信は可能なので、いっそのこと全てのデータを扱いやすい JSON 形式でデータを再配信するシステムを組んでしまおうという考えに至った次第です。
XML形式のものは登録などを要するので、とりあえず今回は CSV で配信されているデータを JSON にしていきます。
CSVがつらい
気象庁の CSV には妙な typo があちこちに含まれているというつらい内容になっています。例えば、最大風速と最大瞬間風速のヘッダ行は同一の内容なのですが……
最大風速 (mxwsp00_rct.csv) | 最大瞬間風速 (gust00_rct.csv) |
---|---|
昨日までの12月の1位の値(m/s) |
昨日までの12月の1位の値(m/s) |
最大瞬間風速だけ全角括弧!!! とてもつらい。
また、最低気温/最高気温 のヘッダーでは今年の最低気温(℃)(昨日まで)
というものがありますが、
前半の閉じ括弧だけ半角です。明らかにtypoだと思います。
配信開始から4年ほど経過しているそうなので誰かが気付いているとは思うのですが、おそらく既にこのデータに合わせて動いているプログラムのことを考えると変更できないということであえて放置されているのでしょうか。
何にせよ、気象データを利用するユーザ全員の手をこんなところで止めてしまうというのは明らかに勿体ないので、この辺の面倒事をJJWDで吸収できたらなと思っているところです。
法律的に注意すること
気象業務法によれば、日本において「天気予報」を行うには許可が必要となっています。
気象庁のWebサイトを見ると、ウェザーニューズ社に代表される民間気象予報会社や一部のテレビ局などがこの許可を得ているようです。
予報業務には許可が要るわけですが、気象庁や許可を得た事業者が観測したり予報したりしたデータをそのまま配信することは法律上は許可が不要なわけです。(もちろん、民間事業者のデータを利用するにはその事業者との契約などが必要だと思われます。)
テレビ局でも上記の許可を得ていないところがありますが、あれは「気象予報」ではなく「気象情報」として「あくまで我々は気象庁やウェザーニューズが発表したものを伝えているだけ」なので問題なしということになります。
また、自作の計測機器で気象観測したデータを配信することも法律的にはNGだそうです。
参考:外気温を測ってホームページで公開すると気象庁から怒られる件 - Qiita
今回作成したサービスはいずれにも該当せず、法的な懸念事項はありません。もちろん、データを利用しても問題ありません。
ただし、今回作成したサービスが配信するJSONを利用してに、何らかの統計処理や計算を行って独自の予報を発表する場合は「気象予報業務」に該当しますのでお気をつけください。
実装
今回の要件は
- CSV データ(文字コードはShift_JIS)を取得
- CSV を JSON に変換する
- JSON を保存し、gzip も圧縮する
- これらの処理をデーモン化して走らせ続け、定時に実行
になります。なお、データは nginx で配信します。
今回はあまり触れていませんが、 nginx もチューニングしていきたいところです。
1. 配信される CSV データを取得
HTTP GET → 取得したデータを UTF-8 にエンコードするまでを cheerio-httpcli に投げています。
元々スクレイピングするつもりだった名残ですが、取得しているのは CSV だけだし cheerio を使う利点はないし書き直したが良さそうだなと思っているところでアドカレの期日が来たパターン。あとで頑張ります。
2. CSV を JSON に変換する
csv-parse なる便利ライブラリがあります。
早速これにデータを通していきましょうと言いたいところですが、気象庁から入手できる CSV ファイルのヘッダ行は日本語なので、いい感じに
replace してあげる必要があります。
置換するテキストの情報をハードコーディングするのも何なので、以下のようなファイルでCSVファイルごとに置換する情報を定義しています。
{
"about":"降水量24h(pre24h00_rct.csv)のための置換情報",
"useRegExp":true,
"観測所番号":"stn",
"都道府県":"pref",
"国際地点番号":"intlStn",
"地点":"point",
"現在時刻\\(年\\)":"year",
"現在時刻\\(月\\)":"month",
"現在時刻\\(日\\)":"day",
"現在時刻\\(時\\)":"hour",
"現在時刻\\(分\\)":"min",
"現在値\\(mm\\)":"rain24h",
"現在値の品質情報":"rain24hQ",
"今日の最大値\\(mm\\)":"rain24hTM",
"今日の最大値の品質情報":"rain24hTMQ",
"今日の最大値起時(時)\\(まで\\)":"rain24hTMhour",
"今日の最大値起時(分)\\(まで\\)":"rain24hTMmin",
"今日の最大値起時\\(まで\\)の品質情報":"rain24hTMtimeQ",
"10年未満での極値更新":"rain24hExtU10",
"極値更新":"rain24hExt",
"昨日までの観測史上1位の値\\(mm\\)":"rain24hR",
"昨日までの観測史上1位の値の品質情報":"rain24hRQ",
"昨日までの観測史上1位の値の年":"rain24hRyear",
"昨日までの観測史上1位の値の月":"rain24hRmonth",
"昨日までの観測史上1位の値の日":"rain24hRday",
"昨日までの([1-9]|1[012])月の1位の値\\(mm\\)":"rain24hMR",
"昨日までの([1-9]|1[012])月の1位の値の品質情報":"rain24hMRQ",
"昨日までの([1-9]|1[012])月の1位の値の年":"rain24hMRyear",
"昨日までの([1-9]|1[012])月の1位の値の月":"rain24hMRmonth",
"昨日までの([1-9]|1[012])月の1位の値の日":"rain24hMRday",
"統計開始年":"startYear"
}
この設定ファイルを require
してあげて、以下のような処理で JSON を吐き出すようにしています。
設定ファイルのように一度読んでしまえばOKなものはわざわざ fs
などを使わずに require
した方が楽かな、と思います。
const rule = require('./settings/replaceRule.json');
let csvData = csvHeaderReplacerWithJSON(fetchedData, rule); //fetchedData は HTTP GET した CSV の中身
let parsedResult = parse(csvData, { columns: true, relax_column_count: true });
function csvHeaderReplacerWithJSON(target, ruleJSON) {
let result = target;
let regExpMode = ruleJSON.useRegExp;
for (var key in ruleJSON) {
var keystring = key;
if (regExpMode == true) {
keystring = new RegExp(key);
}
result = result.replace(keystring, ruleJSON[key]);
}
return result;
}
気象庁CSV には末尾のデータが欠落している場合にカンマを省略してあるものがあり、ヘッダの要素数と一致しない行が時々出てくるので relax_column_count
オプションはONにしてやる必要があります。
3. JSON を保存し、gzip も圧縮する
保存は何の変哲もない fs.writeFileSync
です。
また、 gzip 圧縮には標準の zlib を使用します。
今回生成する JSON ファイルはプロパティなどで同じ文字列が大量に連続して出てくるため、gzip 圧縮してやると容量が大幅に削減できます。
今後、 nginx で gzip_static を利用することも想定しています。
4. これらの処理をデーモン化して走らせ続け、 定時に実行
定番ですが forever を使ってデーモン化します。
なお、デーモン化する js ファイル名は index.js
や app.js
などではなく区別し易いものにしておく方が望ましいです。
というのも、 forever list
でデーモンとして実行中の js ファイルを確認すると以下のようにファイル名で区別をすることになり、 運用するものが増えたときに index.js
が並ぶと区別しづらくなります。
$ forever list
info: Forever processes running
data: uid command script forever pid id logfile uptime
data: [0] QJZc /usr/bin/node index.js 4622 4628 /home/hogehoge/.forever/QJZc.log 0:3:34:49.603
定時実行は node-cron です。
OS の crontab 触りに行くよりもコード内で完結するほうが状態が把握しやすいので良いですね。
動けばいいや感の漂うコードなのであまり読まれたくない(本心)ですが、 GitHub に置いてます。
今後の課題
- ドキュメントの充実
- 項目名自体が気象に関する知識を要求しているものも多々あるので、説明も含めてドキュメントにしたいです
- 私もググるまで 最大風速 と 最大瞬間風速 の違いがわからなかったです
- 急いで書いたのでドキュメントページが読みづらすぎる問題
- 項目名自体が気象に関する知識を要求しているものも多々あるので、説明も含めてドキュメントにしたいです
- XMLも取得したい
- 少し面倒な気はしますが、そちらがメインだと思うので
- もう少しまともなコードにしたい!