Edited at

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

More than 1 year has passed since last update.


結論

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とかご意見を募集してますー!)