結論
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
まずはベースとなるテストを書く
メソッドが定義されてないとコンパイルできないので、処理のないメソッドを定義する
func ToEnglish(str string) string {
return ""
}
func ToJapanese(str string) string {
return ""
}
そして、テストを書く
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を取得するメソッドを作る
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)
}
取得したトークンと翻訳したい文字列を送りつける
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/xml
の Unmarshal
メソッドで変換しようと思ったけど、1層目にデータがあるせいでうまく Unmarshal
できなかったので、正規表現と文字列操作を使って力技で求めるデータだけを取り出す
format
メソッドを定義する。(んだけども、これに関しては MSさんが修正するか選択できるようにしてほしい…欲を言えば JSON で返してほしい…)
func format(raw string) string {
re := regexp.MustCompile(">.*<")
result := string(re.Find([]byte(raw)))
return result[1:len(result)-1]
}
ここまでで定義したメソッドを繋ぐ(ToXXXXの中でそれぞれ呼び出す)
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 から呼び出せるように繋ぐ
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")
でフラグがついているかを判断し、振り分けている
オプションで 英語→日本語と日本語→英語 を切り替えれるようにする
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に呟ける のを作って記事にします!
リポジトリ
(Golang勉強中なのでPRとかIssueとかご意見を募集してますー!)