これはGo (その3) Advent Calendarの20日目の記事です。
昨日はikawahaさんの「goa でデザイン・ファーストをシュッとする」でした。
この猫なに猫?APIという猫画像をアップロードすれば猫の種類と確率値を上位5位まで教えてくれるWebサービスがある。
このAPIを叩くコードをGoで書いてみた。
昨日はikawahaさんの「goa でデザイン・ファーストをシュッとする」でした。
この猫なに猫?APIという猫画像をアップロードすれば猫の種類と確率値を上位5位まで教えてくれるWebサービスがある。
このAPIを叩くコードをGoで書いてみた。
curlでの呼び出し例
呼び出し例として以下のようにcurlでの例が示されている。curl -u xxx:yyy -F "image=@zzz" http://whatcat.ap.mextractr.net/api_queryふむ。GoのHTTP ClientでBasic認証、マルチパートファイルアップロードを行えば実現できそう。
Basic認証
http.Request
のSetBasicAuth
でユーザー名・パスワードを指定する。ユーザー名・パスワードは引数で渡してもいいけど、今回は環境変数から値を取得するようにしてみた。
マルチパートファイルアップロード
multipart.Writer
のCreateFormFile
で作成したio.Writer
に対して画像データを書いていけばよさそう。今回は、コマンドライン引数にローカルファイルだけでなく画像URLも指定できる事にした。
URLなら
http.Get
の結果のres.Body
が、ローカルファイルならos.Open(path)
がio.ReadCloser
なので、これを
io.Copy
でio.Writer
にコピーする。レスポンスJSONをパースして構造体として返す
type Cat struct
で構造体にCat
という名前を付け、JSONで返るレスポンスボディをパースしてこの構造体で返すようにする。よくあるパターンで、こんな構造体を用意すればいいかと思った…
type Cat struct { Breed string `json:"breed"` Probability float64 `json:"probability"` }が、思わぬ落とし穴が。
どうもJSONの形式が、名前と値を持っておらず猫の種類と確率値を配列の第一要素・第二要素に入れた配列の配列になっているようだ。
[ ["Aegean_cat", 0.735941171646], ["Asian_cats", 0.0728636011481], ["LaPerm", 0.054945282638], ["american_wirehair", 0.0304534453899], ["Bahraini_Dilmun_Cat", 0.0132808424532] ]うーん、困った。
自力でパースしないといけないのかな…と思っていたが、golang は ゆるふわに JSON を扱えまぁす!を参考に、
*interface{}
をjson.Unmarshal
に渡し、結果をキャスト(「型アサーション」という用語が正しい?)して、改めて値を Cat 構造体に設定する事で解決できた。
完成したコード
- whatcat/whatcat.go
package whatcat import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "mime/multipart" "net/http" "os" "path/filepath" "regexp" ) const Url = "http://whatcat.ap.mextractr.net/api_query" func WhatCat(path string) ([]Cat, error) { var b bytes.Buffer w := multipart.NewWriter(&b) if err := createImage(w, path); err != nil { return nil, err } w.Close() res, err := post(b, w) if err != nil { return nil, err } defer res.Body.Close() body, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } if err := validate(res.StatusCode, body); err != nil { return nil, err } return parse(body) } func createImage(w *multipart.Writer, path string) error { _, name := filepath.Split(path) fw, err := w.CreateFormFile("image", name) if err != nil { return err } r := regexp.MustCompile(`^https?://.*$`) var rc io.ReadCloser if r.MatchString(path) { rc, err = createImageFromUrl(path) } else { rc, err = createImageFromFile(path) } if err != nil { return err } defer rc.Close() if _, err = io.Copy(fw, rc); err != nil { return err } return nil } func createImageFromUrl(url string) (io.ReadCloser, error) { res, err := http.Get(url) if err != nil { return nil, err } return res.Body, nil } func createImageFromFile(path string) (io.ReadCloser, error) { return os.Open(path) } func post(b bytes.Buffer, w *multipart.Writer) (*http.Response, error) { req, err := http.NewRequest("POST", Url, &b) if err != nil { return nil, err } username := os.Getenv("WHATCAT_USERNAME") password := os.Getenv("WHATCAT_PASSWORD") req.SetBasicAuth(username, password) req.Header.Set("Content-Type", w.FormDataContentType()) client := new(http.Client) return client.Do(req) } func validate(status int, body []byte) error { if status >= http.StatusInternalServerError { return fmt.Errorf("Server error. %s", body) } if status >= http.StatusBadRequest { return fmt.Errorf("Client error. %s", body) } return nil } func parse(body []byte) ([]Cat, error) { var result interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, err } var cats = []Cat{} results := result.([]interface{}) for _, r := range results { cat := r.([]interface{}) cats = append(cats, Cat{Breed: cat[0].(string), Probability: cat[1].(float64)}) } return cats, nil } type Cat struct { Breed string Probability float64 }
- main.go
package main import ( "./whatcat" "fmt" "os" "strconv" ) func main() { if len(os.Args) <= 1 { fmt.Fprintln(os.Stderr, "Neither file path nor url is specified.") return } cats, err := whatcat.WhatCat(os.Args[1]) if err != nil { fmt.Fprintln(os.Stderr, err) return } for _, cat := range cats { fmt.Println(cat.Breed + " : " + strconv.FormatFloat(cat.Probability, 'f', 10, 64) + "%") } }
実行
このツイートの画像を引数に指定して実行してみよう。はふ…。 pic.twitter.com/btz23HOTZN
— ザネリ (@so_zaneli) 2016年12月10日
> go run main.go https://pbs.twimg.com/media/CzTcW3qUAAAe9PK.jpg Himalayan_cat : 0.9966155887% Selkirk_Rex : 0.0033684424% munchkin_cat : 0.0000112807% British_Longhair : 0.0000019983% LaPerm : 0.0000017260%
> go run main.go https://pbs.twimg.com/media/CzTcW3nUUAQRFtF.jpg Arabian_Mau : 0.9856963158% Aegean_cat : 0.0118447915% american_wirehair : 0.0013294291% Cymric : 0.0005801994% Manx_cat : 0.0002078217%
> go run main.go https://pbs.twimg.com/media/CzTcW3mVEAAE-yU.jpg american_bobtail : 0.2447371632% Ragamuffin_cat : 0.1668234318% munchkin_cat : 0.1492303461% Japanese_Bobtail : 0.1320474595% Cymric : 0.1162061393%
> go run main.go https://pbs.twimg.com/media/CzTcW6FVEAAR5Li.jpg Scottish_Fold : 0.9919397235% munchkin_cat : 0.0065293154% Asian_cats : 0.0008168578% american_curl : 0.0005830775% alpine_lynx_cat : 0.0001184104%うむ、それっぽい値が返ってきている。
(ちなみにこの子達は吉祥寺の猫カフェきゃりこのにゃんこ。HPで答え合わせをしてみてね。)
おまけ
せっかくGoで書いたので、Gopher君の画像(designed by Renee French, Creative Commons Attributions 3.0 licensed.)を指定してみよう。> go run main.go https://blog.golang.org/gopher/gopher.png Oriental_cat : 0.9977165461% Siamese : 0.0013568689% Sphynx_cat : 0.0004057394% Cornish_Rex : 0.0003750115% HavanaBrown_cat : 0.0000559103%おお、ちゃんと(?)猫の種類が返ってきた。
どうやら耳が三角形に尖った猫と推定されているような?
明日はshibukawaさんの「Golang用のi18nパッケージを作ってみた」です。