気象庁アメダス観測データのAPI「JJWD」をきちんと作り直した

**長らく停止しておりましたが2020年9月、再始動しました。**
**https://jjwd.info/**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185296/01e1b9e4-152b-c203-efd0-7b699cc19005.png)


## JJWD とは / 作り直した動機

JSONized Japanse Weather Dataの略で、気象庁が公開しているCSV形式のアメダス観測値データを使いやすいJSON形式に変換して提供するAPIサービスです。

当初は[2017年のアドベントカレンダー](https://qiita.com/ryo-a/items/4f651dbcf82537ac36f1)に間に合わせるために構築したAPIでした。

停止したにもかかわらず、記事にコンスタントにLGTMがついておりました。
需要はあるものを放置したくはないですし、データを必要としている人も多いと思ったのできちんと使える形で整備した次第です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185296/45540693-3718-7bc8-ff41-bbc3c1cb7f1e.png)

## 使い方

### 使用に際して

気象業務法に抵触しない範囲で使用してください。
観測された値を基に何らかの処理を行って独自の予測を行う場合、資格ならびに気象庁の許可が必要です（観測されたままの値や、発表された気象予報を第三者に伝達することは問題ありません）。

また、気象庁以外が「警報」「注意報」などを発表することは許可されていません。

### 観測所検索

GET `https://jjwd.info/api/v2/stations/search?`

URL に以下のパラメータを付与して検索できます。

- `stn_name_ja`: 観測所名 日本語（部分一致）
- `stn_name_en`: 観測所名 英語（部分一致）
- `pref_ja`: 都府県振興局 日本語（部分一致）
- `pref_en`: 都府県振興局 英語（部分一致）
- `address`: 住所 日本語（部分一致）

たとえば
`https://jjwd.info/api/v2/stations/search?pref_ja=東京&address=世田谷`
では、都府県振興局に `東京` を含み、なおかつ住所に `世田谷` を含む観測所が取得できます。

北海道は振興局単位なのでご注意ください。

```json:/api/v2/stations/search?pref_ja=東京&address=世田谷
{
    "about": "JJWD - JSONized Japanese Weather Data:  https://jjwd.info/",
    "datasource": "https://www.data.jma.go.jp/obd/stats/data/mdrr/",
    "author": "Original data are from Japan Meteorological Agency. API data are modified by jjwd.info .",
    "version": "v2.0",
    "stations": [
        {
            "stn_num": 44126,
            "pref_ja": "東京",
            "pref_en": "Tokyo",
            "stn_type": "RobotRain",
            "stn_name_ja": "世田谷",
            "stn_name_ja_kana": "セタガヤ",
            "stn_name_en": "SETAGAYA",
            "stn_multipoint": false,
            "target_main_ja": null,
            "target_sub_ja": null,
            "target_main_en": null,
            "target_sub_en": null,
            "stn_temp": false,
            "stn_daylight": true,
            "address": "世田谷区岡本",
            "address_sub": null,
            "lat": 35.6267,
            "lng": 139.62,
            "lat_sub": null,
            "lng_sub": null,
            "elevation": 35,
            "alt_anemometer": null,
            "alt_thermometer": null,
            "elevation_sub": null,
            "alt_anemometer_sub": null,
            "alt_thermometer_sub": null,
            "start_date_rain": "1974-11-01",
            "start_date_multi": null,
            "snow_stn_num": null,
            "updatedAt": "2020-09-11T19:00:32.933Z",
            "preall": {
                "year": 2020,
                "month": 9,
                "day": 12,
                "hour": 19,
                "minute": 50,
                "precip_1h_new_record_month": null,
                "precip_1h_new_record_in_decade": null,
                "precip_3h_new_record_month": null,
                "precip_3h_new_record_in_decade": null,
                "precip_6h_new_record_month": null,
                "precip_6h_new_record_in_decade": null,
                "precip_12h_new_record_month": null,
                "precip_12h_new_record_in_decade": null,
                "precip_24h_new_record_month": null,
                "precip_24h_new_record_in_decade": null,
                "precip_48h_new_record_month": null,
                "precip_48h_new_record_in_decade": null,
                "precip_72h_new_record_month": null,
                "precip_72h_new_record_in_decade": null,
                "precip_daily_new_record_month": null,
                "precip_daily_new_record_in_decade": null,
                "precip_1h": 0.5,
                "precip_1h_q": 8,
                "precip_1h_daily_max": 13,
                "precip_1h_daily_max_q": 5,
                "precip_3h": 0.5,
                "precip_3h_q": 8,
                "precip_3h_daily_max": 24,
                "precip_3h_daily_max_q": 4,
                "precip_6h": 1,
                "precip_6h_q": 8,
                "precip_6h_daily_max": 24,
                "precip_6h_daily_max_q": 4,
                "precip_12h": 5,
                "precip_12h_q": 8,
                "precip_12h_daily_max": 26.5,
                "precip_12h_daily_max_q": 4,
                "precip_24h": 29,
                "precip_24h_q": 8,
                "precip_24h_daily_max": 29,
                "precip_24h_daily_max_q": 4,
                "precip_48h": 29,
                "precip_48h_q": 8,
                "precip_48h_daily_max": 29,
                "precip_48h_daily_max_q": 4,
                "precip_72h": 29.5,
                "precip_72h_q": 8,
                "precip_72h_daily_max": 29.5,
                "precip_72h_daily_max_q": 4,
                "precip_daily": 28.5,
                "precip_daily_q": 4,
                "updatedAt": "2020-09-12T11:13:41.878Z"
            },
            "max_wind": null,
            "max_gust": null,
            "max_temp": null,
            "min_temp": null
        }
    ]
}
```

### 観測所番号を指定して取得

GET `https://jjwd.info/api/v2/station/{stn_num}`

観測所番号がわかっている場合はこちらを使用することができます。

```json:/api/v2/station/46046
{
    "about": "JJWD - JSONized Japanese Weather Data:  https://jjwd.info/",
    "datasource": "https://www.data.jma.go.jp/obd/stats/data/mdrr/",
    "author": "Original data are from Japan Meteorological Agency. API data are modified by jjwd.info .",
    "version": "v2.0",
    "station": {
        "stn_num": 46046,
        "pref_ja": "神奈川",
        "pref_en": "Kanagawa",
        "stn_type": "RobotRain",
        "stn_name_ja": "相模原中央",
        "stn_name_ja_kana": "サガミハラチュウオウ",
        "stn_name_en": "SAGAMIHARACHUO",
        "stn_multipoint": false,
        "target_main_ja": null,
        "target_sub_ja": null,
        "target_main_en": null,
        "target_sub_en": null,
        "stn_temp": false,
        "stn_daylight": true,
        "address": "相模原市中央区中央",
        "address_sub": null,
        "lat": 35.5717,
        "lng": 139.37,
        "lat_sub": null,
        "lng_sub": null,
        "elevation": 149,
        "alt_anemometer": null,
        "alt_thermometer": null,
        "elevation_sub": null,
        "alt_anemometer_sub": null,
        "alt_thermometer_sub": null,
        "start_date_rain": "1975-05-16",
        "start_date_multi": "1975-05-16",
        "snow_stn_num": null,
        "createdAt": "2020-08-29T15:44:50.463Z",
        "updatedAt": "2020-09-11T19:00:32.937Z",
        "preall": {
            "year": 2020,
            "month": 9,
            "day": 12,
            "hour": 19,
            "minute": 50,
            "precip_1h_new_record_month": null,
            "precip_1h_new_record_in_decade": null,
            "precip_3h_new_record_month": null,
            "precip_3h_new_record_in_decade": null,
            "precip_6h_new_record_month": null,
            "precip_6h_new_record_in_decade": null,
            "precip_12h_new_record_month": null,
            "precip_12h_new_record_in_decade": null,
            "precip_24h_new_record_month": null,
            "precip_24h_new_record_in_decade": null,
            "precip_48h_new_record_month": null,
            "precip_48h_new_record_in_decade": null,
            "precip_72h_new_record_month": null,
            "precip_72h_new_record_in_decade": null,
            "precip_daily_new_record_month": null,
            "precip_daily_new_record_in_decade": null,
            "precip_1h": 0,
            "precip_1h_q": 8,
            "precip_1h_daily_max": 10,
            "precip_1h_daily_max_q": 5,
            "precip_3h": 0.5,
            "precip_3h_q": 8,
            "precip_3h_daily_max": 10.5,
            "precip_3h_daily_max_q": 4,
            "precip_6h": 2,
            "precip_6h_q": 8,
            "precip_6h_daily_max": 13.5,
            "precip_6h_daily_max_q": 4,
            "precip_12h": 5,
            "precip_12h_q": 8,
            "precip_12h_daily_max": 16,
            "precip_12h_daily_max_q": 4,
            "precip_24h": 18.5,
            "precip_24h_q": 8,
            "precip_24h_daily_max": 18.5,
            "precip_24h_daily_max_q": 4,
            "precip_48h": 18.5,
            "precip_48h_q": 8,
            "precip_48h_daily_max": 18.5,
            "precip_48h_daily_max_q": 4,
            "precip_72h": 19,
            "precip_72h_q": 8,
            "precip_72h_daily_max": 19,
            "precip_72h_daily_max_q": 4,
            "precip_daily": 17.5,
            "precip_daily_q": 4,
            "updatedAt": "2020-09-12T11:13:41.884Z"
        },
        "max_wind": null,
        "max_gust": null,
        "max_temp": null,
        "min_temp": null
    }
}
```

各パラメータの意味など、詳細はドキュメントをご参照ください
**https://jjwd.info/doc-ja.html**

## データソース

### 基地局データ

気象庁の[地域気象観測所一覧 [ZIP圧縮形式]](https://www.jma.go.jp/jma/kishou/know/amedas/kaisetsu.html)から取得しています。
ZIP形式の中にCSVが格納されているのですが、開発中に提供されているファイルの名前が変わってしまい、取得できなくなるエラーが発生しました（気象庁さん、配布するファイルの名称は固定してほしい……）。

### 観測値データ

[「最新の気象データ」CSVダウンロード](https://www.data.jma.go.jp/obd/stats/data/mdrr/docs/csv_dl_readme.html)から取得しています。[データのつらさ](https://qiita.com/ryo-a/items/4f651dbcf82537ac36f1#%E6%B0%97%E8%B1%A1%E5%BA%81%E3%81%AE%E3%82%AA%E3%83%BC%E3%83%97%E3%83%B3%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92json%E3%81%AB%E3%81%97%E3%81%9F%E3%81%84)は相変わらずです。その辺は前回と同じようになんとかしました。


## 実装

Node.js + Express.js + Sequelize の無難な構成です。

```:ざっくりとしたディレクトリ構成
/
| - /schedule
| - | - fetch_amedas_stations.js
| - | - fetch_csv_files.js
| - /models
| - /public
| - | - /css
| - | - /js
| - app.js
| - package.json
```

### データベース

[Heroku Postgres](https://elements.heroku.com/addons/heroku-postgresql)を使用しています。件数的にはHobby devに収まるので無料の枠内で運用しています。

アクセス数が増えたら Connection Limit などを緩和するために Standard 0 などにアップグレードする必要がありそうです（Hobby Basicは行数の制限緩和のみなので）。

### データの更新

[Heroku Scheduler](https://devcenter.heroku.com/articles/scheduler) でNodeのスクリプトを走らせてUpsertしています。
Heroku Scheduler を使うと、Herokuにデプロイした repository 上にあるスクリプトを定時に走らせる事が可能です。DBのモデルやnpmパッケージも本体と共用できるので便利です。

観測値データの更新は 10 分おきに取得しています。
基地局データの更新は 1 日おきに取得しています。ただし、生の CSV データが配布されている観測値と異なり、基地局データは元データが zip で圧縮されているので `/tmp` にデータをダウンロードした上で展開する処理を挟んでいます（Heroku では `/tmp` や `/log` 以外にファイルを書き込めないので要注意です）。

### 静的ページ（ドキュメント）

シンプルに `app.use(express.static(__dirname + '/public'));` で配信しています。

デザインには [Tailwind.css](https://tailwindcss.com/) を使用しています。
便利なCSSフレームワークですが、何もしないとファイルサイズが巨大なので、デプロイ前に[purge](https://tailwindcss.com/docs/controlling-file-size)やminifyしましょう。

また、軽くCSSアニメーションを入れたかったので[animista](https://animista.net)を使用しています。

## デプロイ

Heroku を使用しています。
独自ドメインを使用したかったので、いまのところ月額7ドルの Hobby Dyno を使用しています。

### 独自ドメイン

[公式ドキュメント](https://devcenter.heroku.com/articles/custom-domains)を参考に、バリュードメインで以下のように CNAME で設定しています。

```:ドメイン設定
cname @ ****************.herokudns.com.
```