kikki's tech note

技術ブログです。UnityやSpine、MS、Javaなど技術色々について解説しています。

Golangで外国語の校閲を行う

本章では、GO言語で外国語の校閲を行う方法について共有します。

背景

MicrosoftのWordを使えば、文言に間違いがあれば、赤線で間違いを指摘してもらえます。その仕組みを機械的に検査できないかといったニーズがあり、検討を開始しました。
WEBサービスとして提供することを検討していたため、WordをAPIとして利用することはライセンスの観点からも考えてもちろんNGです。そこでCloudのマネージド・サービスを探しました。

サービス

APIとして校閲ができるサービスは、AWSGCPになく、以下の2つが該当しました。

Bing Spell Check API

azure.microsoft.com

webspellchecker.netのWEB API

www.webspellchecker.net

今回は、サービスの価格と名前、今後の発展性から、Bing Spell Checkを採用しました。

課題と解決

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
}

筆休め

Golangはゴルーチンを利用することで、パフォーマンスよく処理できますが、APIに制約があるとひと手間をかけて上げる必要があります。

以上、「Golangで外国語の校閲を行う」でした。


※無断転載禁止 Copyright (C) kikkisnrdec All Rights Reserved.