Golangで外国語の校閲を行う
本章では、GO言語で外国語の校閲を行う方法について共有します。
背景
MicrosoftのWordを使えば、文言に間違いがあれば、赤線で間違いを指摘してもらえます。その仕組みを機械的に検査できないかといったニーズがあり、検討を開始しました。
WEBサービスとして提供することを検討していたため、WordをAPIとして利用することはライセンスの観点からも考えてもちろんNGです。そこでCloudのマネージド・サービスを探しました。
Bing Spell Check API
課題と解決
Spell Check APIには、校閲する方法として、「proof」と「spell」があります。
docs.microsoft.com
英語だけを校閲する際には、「proof」という検査方法で、約4千文字まで校閲できます。しかし多言語となると、「proof」では校閲できず、「spell」で検査するのですが、最大文字数が130文字もしくは65文字までと、文章を検査するには貧弱な仕組みでした。今回は、文単位に切り分けて検査するよう調整しました。
コード
以下コードの一部です。Bing Spell Check APIからのレスポンスを定義し、一度に文章を校閲できるよう繰り返しリクエストするよう調整しました。
// 提案した単語群 type Suggest struct { Suggestion string `json:"suggestion"` Score float64 `json:"score"` } // 不正確な単語群 type FlaggedToken struct { Offset int `json:"offset"` Token string `json:"token"` TokenType string `json:"type"` Suggestions []Suggest `json:"suggestions"` } // MSのBingAPIモデル type BingResponse struct { index int ApiType string `json:"_type"` FlaggedTokens []FlaggedToken `json:"flaggedTokens"` } // ここで校閲を行う func HogeHoge(sources []string, translationTypeCode string, canUseProof bool) (err error) { var wg sync.WaitGroup index := 0 mode := "" done := make(chan struct{}) errChan := make(chan error, 1) resChan := make(chan BingResponse) // 校閲の方法を選択 if canUseProof { mode = "proof" } else { mode = "spell" } go func() { ticker := time.NewTicker(50 * time.Millisecond) defer ticker.Stop() LOOP: for { select { // goルーチンでリクエストすると、{ "statusCode": 429, "message": "Rate limit is exceeded. Try again in 1 seconds." } エラーが発生 // Bing Spell Checkに秒間あたりのリクエスト回数に制限があるため、苦肉の策としてスリープを追加 >< case <-ticker.C: if index < len(sources) { wg.Add(1) go func(index int) { defer wg.Done() e := requestSpellCheckAPI(sources[index], index, translationTypeCode, mode, resChan) if e != nil { errChan <- e } }(index) index++ } else { break LOOP } } } go func() { wg.Wait() close(resChan) }() }() go func() { for res := range resChan { // スペルチェックの結果を元に、ここで何かしらの処理を行う } done <- struct{}{} }() select { case <-done: return nil case e := <-errChan: return e } } // 文章を添削する func requestSpellCheckAPI(source string, index int, language string, mode string, c chan<- BingResponse) (err error) { result := BingResponse{index: index} if len([]rune(source)) == 0 { return nil } values := url.Values{} values.Set("text", source) req, err := http.NewRequest("POST", "https://api.cognitive.microsoft.com/bing/v7.0/spellcheck?mkt="+language+"&mode="+mode, strings.NewReader(values.Encode())) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Ocp-Apim-Subscription-Key", "Bing Spell Check APIのキーをここに") // 通常のリクエストの場合に、TLS handshake timeout が発生するため、タイムアウト時間を延長 netTransport := &http.Transport{ DialContext: (&net.Dialer{ Timeout: 120 * time.Second, KeepAlive: 60 * time.Second, }).DialContext, TLSHandshakeTimeout: 120 * time.Second, MaxIdleConns: 2, } client := &http.Client{ Transport: netTransport, } res, err := client.Do(req) defer func() { _ = res.Body.Close() }() if err != nil { return err } else if res.StatusCode != http.StatusOK { return errors.New(fmt.Sprintf("%s%d", "Spell check API's status is ", res.StatusCode)) } body, err := ioutil.ReadAll(res.Body) if err != nil { return err } if err := json.Unmarshal(body, &result.Resp); err != nil { return err } c <- result return nil }