Go
golang
cli
CognitiveServices

Golangで自分用のcli翻訳ツール作ってみた

結論

Golang標準パッケージ整っててAPI叩くのやりやすい + TDDでやると見通し良くなる

導入

金曜日の夜に突如思い立って Golang の勉強に cli ツールを3つ作ることにした。
この記事は1つ目。

Golang で cliツールを作るにあたって urfave/cli というパッケージを使うのが楽らしいので、そこは乗っかることに。

go get github.com/urfave/cli

でローカル環境でとりあえず使えるようにした。

やること

コマンドラインでパッと変数名になる英単語を調べれる

  • テストベースで書く(TDDもどき)

目標

  • trans [日本語の単語] で英語を返す
  • trans -e [英単語] で日本語を返す

やらないこと

  • 熟語・短文を調べれるようにする(APIが賢いおかげで日本語→英語は余裕でできた!)
  • 速度を追い求める
  • オフラインでも使えるようにする

実装開始

ディレクトリ構造

$GOPATH/github.com/ahaha0807/translation
└── src
    └── script
        ├── Gopkg.lock
        ├── Gopkg.toml
        ├── main
        │   └── main.go
        ├── translation
        │   └── translation.go
        └── vendor

まずはサンプルコード書いてみた

package main

import (
    "github.com/urfave/cli"
    "fmt"
    "os"
)

func main () {
    app := cli.NewApp()
    app.Name = "translation-ahaha0807"
    app.Usage = "translation on command"
    app.Version = "0.0.1"

    app.Action = func (context *cli.Context) error {
        fmt.Println("Hello world on cli")
        return nil
    }

    app.Run(os.Args)
}

実行してみる

go run ./translation.go
# ==> Hello world on cli

あと、cliパッケージを使ってることで、helpオプションを自動で作ってくれるんだって。便利ー。

go run ./translation.go help
NAME: translation-ahaha0807 - translation on command

USAGE:
   translation [global options] command [command options] [arguments...]

VERSION:
   0.0.1

COMMANDS:
     help, h  Shows a list of commands or help for one command

GLOBAL OPTIONS:
   --help, -h     show help
   --version, -v  print the version

まずはベースとなるテストを書く

メソッドが定義されてないとコンパイルできないので、処理のないメソッドを定義する

translation/translation.go

func ToEnglish(str string) string {
    return ""
}

func ToJapanese(str string) string {
    return ""
}

そして、テストを書く

translation/translation_test.go
func TestToEnglish(t *testing.T) {
    actual := ToEnglish("バナナ")
    expected := "Banana"

    AssertTranslation(actual, expected, t)

    actual = ToEnglish("耳")
    expected = "Ear"

    AssertTranslation(actual, expected, t)

    actual = ToEnglish("変数")
    expected = "Variable"

    AssertTranslation(actual, expected, t)
}

func TestToJapanese(t *testing.T) {
    actual := ToJapanese("Banana")
    expected := "バナナ"

    AssertTranslation(actual, expected, t)

    actual = ToJapanese("ear")
    expected = "耳"

    AssertTranslation(actual, expected, t)

    actual = ToJapanese("VARIABLE")
    expected = "変数"

    AssertTranslation(actual, expected, t)

    actual = ToJapanese("CoFfEe")
    expected = "コーヒー"

    AssertTranslation(actual, expected, t)
}

func AssertTranslation(actual string, expected string, t *testing.T) {
    if actual == "" {
        t.Errorf("翻訳を実行できませんでした。")
    }

    if actual != expected {
        t.Errorf("翻訳結果が異なります %v, %v", actual, expected)
    }
}

$ go test translation_test.go
実装はまだ書いてないので、当然落ちる。

メソッドの実装

翻訳部分の実装

翻訳部分は Microsoft の Translator API を利用する。

Translator APIの使い方とかは 公式サイト ( https://www.microsoft.com/ja-jp/translator/getstarted.aspx )をどうぞ

Translator APIを使うためにAPIトークンを取得する

認証が必要なので、まず Azureのコンパネで見れるAPI Keyを使って API Tokenを取得するメソッドを作る

translator/translator.go
func getAPIToken() string {
    apiKey := string(os.Getenv("MICROSOFT_TRANSLATE_APIKEY"))

    client := &http.Client{}

    req, _ := http.NewRequest("POST", "https://api.cognitive.microsoft.com/sts/v1.0/issueToken", nil)
    req.Header.Set("Ocp-Apim-Subscription-Key", string(apiKey))
    req.Header.Set("Content-Type", "application/json")

    res, err := client.Do(req)
    if err != nil {
        panic(err)
    }

    apiToken, _ := ioutil.ReadAll(res.Body)
    defer res.Body.Close()

    return string(apiToken)
}

取得したトークンと翻訳したい文字列を送りつける

translator/translator.go
func doTranslate(str string, srcLanguage string, destLanguage string, token string) string {
    host := "https://api.microsofttranslator.com"
    path := "/V2/Http.svc/Translate"

    values := url.Values{}
    values.Set("to", destLanguage)
    values.Add("from", srcLanguage)
    values.Add("text", str)

    option := values.Encode()

    client := &http.Client{}

    s := host + path + "?" + option

    req, _ := http.NewRequest("GET", s, nil)
    req.Header.Set("Authorization", "Bearer "+token)

    res, err := client.Do(req)
    if err != nil {
        return ""
    }
    defer res.Body.Close()

    body, _ := ioutil.ReadAll(res.Body)

    return string(body)
}

上記を実行するとレスポンスが

<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">Apple</string>

な感じで届く。

レスポンスをパースする

これを encode/xmlUnmarshalメソッドで変換しようと思ったけど、1層目にデータがあるせいでうまく Unmarshal できなかったので、正規表現と文字列操作を使って力技で求めるデータだけを取り出す
format メソッドを定義する。(んだけども、これに関しては MSさんが修正するか選択できるようにしてほしい…欲を言えば JSON で返してほしい…)

translator/translator.go
func format(raw string) string {
    re := regexp.MustCompile(">.*<")
    result := string(re.Find([]byte(raw)))

    return result[1:len(result)-1]
}

ここまでで定義したメソッドを繋ぐ(ToXXXXの中でそれぞれ呼び出す)

translator/translator.go
func ToEnglish(str string) string {
    apiToken := getAPIToken()

    rawResult := doTranslate(str, "ja", "en", apiToken)
    result := format(rawResult)

    return result
}

func ToJapanese(str string) string {
    apiToken := getAPIToken()

    rawResult := doTranslate(str, "en", "ja", apiToken)
    result := format(rawResult)

    return result
}

これで基本的にはテストが通るようになる(はず。)

ToXXXX系メソッドを cli から呼び出せるように繋ぐ

main/main.go
app.Action = func(context *cli.Context) error {
    if len(context.Args()) == 0 {
        fmt.Println("USAGE: trans --[option] [words]")
        return nil
    }

    if !context.Bool("english") {
        result := translation.ToEnglish(context.Args().Get(0))
        fmt.Println(result)
        return nil
    } else {
        result := translation.ToJapanese(context.Args().Get(0))
        fmt.Println(result)
        return nil
    }
}

!context.Bool("english") でフラグがついているかを判断し、振り分けている

オプションで 英語→日本語と日本語→英語 を切り替えれるようにする

main/main.go
app.Flags = []cli.Flag{
    cli.BoolFlag{
        Name:  "english, e",
        Usage: "英語 to 日本語",
    },
}

ビルド + PATHを通す

cd src/script/main/
go build -i -o trans main.go

これでビルドされ、 ./trans でコマンドが使える。

./trans バナナ
Banana
./trans -e Banana
バナナ
./trans -h
# ~~~~~

あとはこれを $GOPATH/bin に移動させるだけ!

mv ./trans $GOPATH/bin

trans バナナ
Banana

まとめ

単純なコマンドで、オフラインじゃ使えないまだまだ稚拙なこまんどだけど、自分で作ったやつが動くと嬉しい!
あと、 $GOPATH 使えば読み手の環境を色々考えなくてもよくって、共通化できててすごいこれ嬉しいのかも…!w

次回は コマンドからTwitterに呟ける のを作って記事にします!

リポジトリ

https://github.com/ahaha0807/translation-tool

(Golang勉強中なのでPRとかIssueとかご意見を募集してますー!)