大量の住所を Google Maps 上にマッピングする必要が生じ、住所を緯度と経度に変換することになったので、いわゆるジオコーディングをプログラムすることにしました。 ジオコーディングに関しては、Google Maps API が無料で使用できて、かつ、扱いやすいので、これを使うことにします。
Google Maps API では、緯度や経度が 10 進数で返却されるのですが、今回は諸事情で 60 進数 (度分秒) で表現する必要があったので、そのあたりの処理も入れています。
仕様
- 入力は住所やランドマークを記載した CSV ファイル
- 出力は、住所やランドマーク、緯度、経度を記載した CSV ファイル
具体的な例としては、以下のような入力に対して、
東京ドーム 東京駅
以下のファイルが出力されます。
東京ドーム,35°42'20.3"N,139°45'6.8"E 東京駅,35°40'52.7"N,139°45'58.5"E
緯度、経度をこういう形式で記述するのは初めてでしたが、Google Maps はこの形式でも場所を特定できるんですね。知らなんだ。
技術要素
Golang 入門者なので、今回は
- CSV からの読み取り、書き込み
- JSON のデコード
- Channel
- errgroup
あたりにチャレンジしました。
JSON から Go Struct の生成
Go は静的言語であるが故に、基本的には JSON と Go の構造体 (Struct) をマッピングする必要があります(interface を使用して回避することはできますが、逆に実装がダルいことになります…) 今回は、Google Maps API のレスポンスのうち、必要なデータは限られていたので、以下のような struct を作っています。
type Geo struct { Results []struct { FormattedAddress string `json:"formatted_address"` Geometry struct { Location struct { Lat float64 `json:"lat"` Lng float64 `json:"lng"` } `json:"location"` } `json:"geometry"` } `json:"results"` Status string `json:"status"` }
この Struct とのマッピングを記述するのはわりかしダルいので、ぼくは JSON-to-Go というサービスを使いました。
左側のテキストボックスに JSON をぶちこむと、右側に Go の struct が出力されます。 こういうところを手で書くほど潤沢な時間が人類に残されているわけではないので、今後も積極的に使っていきたい。
実装コード
Go、まだ慣れない。
package main import ( "context" "encoding/csv" "encoding/json" "flag" "fmt" "io" "math" "net/http" "net/url" "os" "golang.org/x/sync/errgroup" ) func main() { var infile = flag.String("i", "", "住所が入ったCSVファイル") var outfile = flag.String("o", "", "出力するCSVファイル") flag.Parse() if *infile == "" { fmt.Fprintf(os.Stderr, "specify csv file with -i\n") os.Exit(1) } if *outfile == "" { fmt.Fprintf(os.Stderr, "specify output file with -o\n") os.Exit(1) } if err := newApp(*infile, *outfile).run(); err != nil { fmt.Fprintf(os.Stderr, "%v", err) } } type Geo struct { Results []struct { FormattedAddress string `json:"formatted_address"` Geometry struct { Location struct { Lat float64 `json:"lat"` Lng float64 `json:"lng"` } `json:"location"` } `json:"geometry"` } `json:"results"` Status string `json:"status"` } type App struct { AddressFile string GeoDecodeFile string Client *http.Client } // NewApp creates a new application with input and output file pointer. func newApp(infile, outfile string) *App { return &App{ AddressFile: infile, GeoDecodeFile: outfile, Client: &http.Client{}, } } func (app *App) run() error { infp, err := os.Open(app.AddressFile) if err != nil { return fmt.Errorf("open %s: ", app.AddressFile) } defer infp.Close() outfp, err := os.Create(app.GeoDecodeFile) if err != nil { return fmt.Errorf("open %s: ", app.GeoDecodeFile) } defer outfp.Close() eg, ctx := errgroup.WithContext(context.Background()) q := make(chan string, 1000) eg.Go(func() error { return app.enqueue(ctx, infp, q) }) eg.Go(func() error { return app.putGeocode(ctx, outfp, q) }) if err := eg.Wait(); err != nil { return err } return nil } func (app *App) enqueue(ctx context.Context, fp *os.File, q chan<- string) error { reader := csv.NewReader(fp) reader.LazyQuotes = true for { records, err := reader.Read() if err == io.EOF { break } if err != nil { return fmt.Errorf("error while reading %s", fp.Name()) } select { case <-ctx.Done(): return ctx.Err() case q <- records[0]: } } close(q) return nil } func (app *App) putGeocode(ctx context.Context, fp *os.File, q <-chan string) error { for address := range q { lat, lng, err := app.geocode(ctx, address) if err != nil { return fmt.Errorf("decode %s", err) } fmt.Fprintf(fp, "%s,%sN,%sE\n", address, convert(lat), convert(lng)) } fp.Sync() return nil } // 10 進数による座標を 60 進数(度分秒)に変換する // ref. http://www.benricho.org/map_latlng_10-60conv/ func convert(n float64) string { degree := math.Trunc(n) leftover := (n - degree) * 60 minute := (int)(math.Trunc(leftover)) leftover -= (float64)(minute) second := leftover * 60 return fmt.Sprintf("%d°%d'%3.1f\"", int(degree), minute, second) } func (app *App) geocode(ctx context.Context, address string) (lat, lng float64, err error) { values := url.Values{} values.Add("address", address) req, err := http.NewRequest("GET", "https://maps.googleapis.com/maps/api/geocode/json", nil) if err != nil { return -1, -1, err } req = req.WithContext(ctx) req.URL.RawQuery = values.Encode() resp, err := app.Client.Do(req) if err != nil { return -1, -1, err } defer resp.Body.Close() var geo Geo decoder := json.NewDecoder(resp.Body) err = decoder.Decode(&geo) if err != nil { return -1, -1, err } fmt.Printf("[%d] %s\n%s\n\n", resp.StatusCode, address, geo) l := geo.Results[0].Geometry.Location return l.Lat, l.Lng, nil }