追記
herokuが有料化してからこのWebAPIは停止しています。
気象庁のJSONから希望の地域の天気予報を取得するための解説として読んでください。
気象庁の天気予報
2021年2月より、気象庁の天気予報がJSONとして取得できるようになりました。あくまでJSONが取得できるような構造になっているというだけで、正式にAPIとして公開しているわけではないそうです。
天気予報を取得したい地域の地域コードでリクエストを送ると、概要や3日間天気、週間天気などが取得できます。
しかしこの地域コード、自治体コードがベースになっており、あまりなじみがありません。
さらに、気象庁が公開している地域コード一覧JSONは(少なくともGoでは)非常に扱いにくい構造になっており、目的の地域コードを探すのも大変です。
これではせっかくプログラムで天気予報を取得しやすくなったのに、地域指定でつまづいてしまいます。
地域コードを取得するWebAPIを作ってみた
そこで、もっと簡単に目的の地域の天気予報を取得できるように、緯度経度から
- 天気予報リクエスト用コード
- コードに対応する市町村名
- レベル別地域コード
を取得できるようにしてみました。
https://revgeo-forecastcode.herokuapp.com/lat={lat}+lon={lon}
上記のURLの{lat}
を緯度に、{lon}
を経度に置き換えてhttpリクエストを送ると、以下のようなJSONを返します。
https://revgeo-forecastcode.herokuapp.com/lat=35.021077+lon=135.761731
{
"forecastcode": "260000",
"prefname": "京都府",
"cityname": "京都市",
"centers": "010600",
"offices": "260000",
"class10s": "260010",
"class15s": "260011",
"class20s": "2610000"
}
ここで取得できたforecastcode
を利用すれば、気象庁から天気予報を取得できます。基本的にはoffices
コードと同じものになりますが、北海道の十勝地方と鹿児島の奄美地方については、それぞれ釧路地方と鹿児島県のデータに統合されているのでoffices
コードとforecastcode
が異なります。
https://www.jma.go.jp/bosai/forecast/data/overview_forecast/260000.json
https://www.jma.go.jp/bosai/forecast/data/forecast/260000.json
3日間天気のデータにはその都道府県内の各地域の天気予報が含まれています。希望の地域のデータを抽出するためにclass10s
コード等を使用します。
天気予報データの中身については他にも詳しい記事がたくさんあるので省略します。
処理の中身
緯度経度から自治体コードへ変換
緯度経度から自治体コードへの変換は国土地理院の逆ジオコーディングAPIを利用しました。
https://mreversegeocoder.gsi.go.jp/reverse-geocoder/LonLatToAddress?lat={lat}&lon={lon}
このAPIにより市レベル、または特別区や政令市においては区レベルの自治体コードが得られます。
ソース
type RevGeo struct {
Results struct {
MuniCd string `json:"muniCd"`
Lv01Nm string `json:"lv01Nm"`
} `json:"results"`
}
func latlonToLocalCode(lat, lon float64) string {
URL := fmt.Sprintf("https://mreversegeocoder.gsi.go.jp/reverse-geocoder/LonLatToAddress?lat=%v&lon=%v", lat, lon)
body, err := common.GetJson(URL)
common.ErrLog(err)
var rg RevGeo
json.Unmarshal(body, &rg)
return rg.Results.MuniCd
}
自治体コードを市レベルのコードに変換
気象庁の地域コードで利用されているのはもう少し荒い市レベルの自治体コードですので、区レベルのコードを市レベルに変換します。
総務省が市レベルの自治体コード表を公開していますので、これを利用して近似値検索をすることで市レベルにできます。区レベルコードは市レベルコードよりも大きい数字ですので、直近下位を探します。
ちなみに、自治体コードの6桁目はただのチェックディジットですので、CSVに加工するときに切り落としました。
ソース
// 区レベルの自治体コードを市レベルに直す
// 二分探索で近似値検索をしている
func localCodeToCityCode(code string) string {
codes := loadCityCodes()
left, right := 0, len(codes)-1
var mid int
for right-left > 1 {
mid = (left + right) / 2
if code == codes[mid] {
break
} else if code > codes[mid] {
left = mid
} else {
right = mid
}
}
if code < codes[mid] {
mid--
}
return codes[mid]
}
var cityCodes []string
// 総務省が公開している市レベル自治体コード表をCSVにしたものを読み込む
func loadCityCodes() []string {
if len(cityCodes) != 0 {
return cityCodes
}
file, err := os.Open("./code.csv")
common.ErrLog(err)
defer file.Close()
reader := csv.NewReader(file)
for {
line, err := reader.Read()
if err != nil {
break
}
cityCodes = append(cityCodes, line[0])
}
return cityCodes
}
地域コードに変換
気象庁の地域コード一覧を取得します。
https://www.jma.go.jp/bosai/common/const/area.json
1万7千行もあるうえに地域コードが配列にもなっていません。
GoではJSONを読み込むときに構造体を作ってそこにマップするのですが,この形式では構造体の定義も1万7千行になってしまいました。
ものは試しにやってみましたがサイズに耐えられなかったのかやはりエラーが。
どうしたものかと悩んでいるとgo-dproxyという便利そうなライブラリを発見。JSONはmap[string]interface{}型(辞書型)にできることを利用してキーから要素にアクセスするようです。
気象庁の地域コードはcenters, offices, class10s, class15s, class20s
という階層に分かれています。
階層名 | 対応する階層 | 例 |
---|---|---|
centers | 地方 | 近畿地方 |
offices | 都道府県 | 京都府 |
class10s | 位置 | 南部 |
class15s | 代表都市 | 京都・亀岡 |
class20s | 市町村 | 京都市 |
class20s
コードが先の自治体コードの末尾に"00"
を付加したものになっています。
各地域コードには親コード・子コードが記載されているので,class20s
コードが分かればcenters
コードまでたどっていけます。
ソース
func parentcode(code string) string {
category := []string{"class20s", "class15s", "class10s", "offices"}
areainfo := dproxy.New(area.AreaInfoMap())
for _, class := range category {
pcode, err := areainfo.M(class).M(code).M("parent").String()
if err == nil {
return pcode
}
}
return ""
}
func toCityName(citycode string) string {
areainfo := dproxy.New(area.AreaInfoMap())
name, _ := areainfo.M("class20s").M(citycode).M("name").String()
return name
}
func toPrefName(officecode string) string {
areainfo := dproxy.New(area.AreaInfoMap())
name, _ := areainfo.M("offices").M(officecode).M("name").String()
return name
}
レスポンス
URLに含まれる変数を取得するやり方を知らなかったのでググりました。
ここで紹介されているgorilla/muxを使って処理します。
あとは例外地域の処理をしてJSONで返事をするだけです。
ソース
func main() {
r := mux.NewRouter()
r.HandleFunc("/lat={lat}+lon={lon}", procRequest)
log.Fatal(http.ListenAndServe(":"+os.Getenv("PORT"), r))
}
type Results struct {
ForecastCode string `json:"forecastcode"`
PrefName string `json:"prefname"`
Cityname string `json:"cityname"`
Centers string `json:"centers"`
Offices string `json:"offices"`
Class10s string `json:"class10s"`
Class15s string `json:"class15s"`
Class20s string `json:"class20s"`
}
func procRequest(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
lat, err := strconv.ParseFloat(vars["lat"], 64)
common.ErrLog(err)
lon, err := strconv.ParseFloat(vars["lon"], 64)
common.ErrLog(err)
localcode := latlonToLocalCode(lat, lon)
var res Results
if localcode != "" {
citycode := localCodeToCityCode(localcode)
res.Class20s = citycode + "00"
res.Class15s = parentcode(res.Class20s)
res.Class10s = parentcode(res.Class15s)
res.Offices = parentcode(res.Class10s)
res.Centers = parentcode(res.Offices)
switch res.Offices {
case "014030":
res.ForecastCode = "014100"
case "460040":
res.ForecastCode = "460100"
default:
res.ForecastCode = res.Offices
}
res.PrefName = toPrefName(res.Offices)
res.Cityname = toCityName(res.Class20s)
}
json.NewEncoder(w).Encode(res)
}