これはGo (その3) Advent Calendarの20日目の記事です。
昨日は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.RequestSetBasicAuthでユーザー名・パスワードを指定する。
ユーザー名・パスワードは引数で渡してもいいけど、今回は環境変数から値を取得するようにしてみた。

マルチパートファイルアップロード

multipart.WriterCreateFormFileで作成したio.Writerに対して画像データを書いていけばよさそう。
今回は、コマンドライン引数にローカルファイルだけでなく画像URLも指定できる事にした。
URLならhttp.Getの結果のres.Bodyが、ローカルファイルならos.Open(path)io.ReadCloserなので、
これをio.Copyio.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) + "%")
      }
    }
    

実行

このツイートの画像を引数に指定して実行してみよう。
> 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パッケージを作ってみた」です。

Copyright© 2011-2017 Shunsuke Otani All Right Reserved .