Gasを用いた自動翻訳〜改良版

### 今回のお題
今回のお題はGasを用いた自動翻訳です。
実際の利用イメージは以下をご参照ください。

また、今回の記事は、[前回](https://qiita.com/keishi04hrikzira/items/87ed4e5f50aec53319c2)作成した翻訳プログラムの改良版です。

加えて、スクレイピングの知識が必要となる場面もございます（スクレイピングについての記事は[こちら](https://qiita.com/keishi04hrikzira/items/9005d89ecfd751a0ef90)）。

今回はスクレイピングや翻訳についての基本的な知識、すなわちParserライブラリやLanguageオブジェクトについては解説しませんので、必要に応じて過去記事をご覧ください。

### プログラムの実行イメージ
最初に言語コードの情報をスクレイピング
[![Image from Gyazo](https://i.gyazo.com/9e7fd99d8f638d0f6bb240c806de3306.gif)](https://gyazo.com/9e7fd99d8f638d0f6bb240c806de3306)

言語コードの情報を取得後に、翻訳を実行
[![Image from Gyazo](https://i.gyazo.com/1f7a454c1617fbd4a12f922b1229f38f.gif)](https://gyazo.com/1f7a454c1617fbd4a12f922b1229f38f)
### 前回からの改良点
[前回](https://qiita.com/keishi04hrikzira/items/87ed4e5f50aec53319c2)からの改良点は、以下の3点です。

- Google公式サイトにてコードの記載されている全ての言語に対応（ただし、アラビア文字からの翻訳ができないなどいくつかの制約はある）。
- 言語コードをGoogleの公式サイトから取得する作業をプログラム化。また、スプレッドシートにも出力されるように設定。
- 翻訳や言語コードの取得・出力といった作業を、全てスプレッドシート上のボタンのクリックで実行可能なように設定。スクリプトエディタの起動が不要に。

この辺りは、[前回](https://qiita.com/keishi04hrikzira/items/87ed4e5f50aec53319c2)の最後に触れていた部分だったので、そのうちのいくつかに対する回答ということになりますね。
### 実装手順
- スプレッドシートの用意
- スクリプトエディタの用意
- スクレイピング用の関数の作成
- 翻訳作業を行う関数の作成
- 関数をボタン操作で実行できるように編集

順番に見ていきます。
### スプレッドシートの用意
まずはスプレッドシートを用意します。
用意できたら以下の作業を行ってください。

- シートを2つ用意する。一つ目は言語コードのスクレイピング用で、もう一方が翻訳用。
- スクレイピング用のシートにはA列に言語名を、B列に言語コードを出力することとし、A1とB1セルに見出しをつけておく。
[![Image from Gyazo](https://i.gyazo.com/7251ef5a5c3f666213ccc9cb7aa61329.png)](https://gyazo.com/7251ef5a5c3f666213ccc9cb7aa61329)
- 翻訳用のシートに関してはA列に翻訳前の言語、B列に翻訳したい文章、C列に翻訳先の言語、D列に翻訳語の文章を入力する仕様とし、それに応じて一列目に見出しをつける。
[![Image from Gyazo](https://i.gyazo.com/4af3ec946f765b18497136b9ff3e9477.png)](https://gyazo.com/4af3ec946f765b18497136b9ff3e9477)
- 必須ではないが、A, C列の言語選択をドロップダウンで行えるようにし、その選択肢をスクレイピングシートから拾えるようにしておくとタイプミスなどに若干強くなる。

:::note warn
上記のプルダウンの作り方に関しては今回も解説しません。悪しからず。
:::
スプレッドシートの準備ができたら、スクリプトエディタを用意します。

### スクリプトエディタの用意
今回使用するスクリプトエディタですが、**必ずコンテナバインドスクリプトとして作成する**ようにしてください。

後ほどスクリプト関数とボタンを紐づける際に役立ちます。

作成できたら、以下のIDでParserライブラリを追加しておいてください。

```
1Mc8BthYthXx6CoIz90-JiSzSafVnT6U3t0z_W3hLTAX5ek4w0G_EIrNw
```
追加できたら、コードを書いていきます。
### スクレイピング関数の作成
まずは、翻訳に必要な言語コードの情報を取得します。

スクリプトエディタを以下のように編集してください。

```javascript
// 使用するスプレッドシートを定義
var spreadsheet = SpreadsheetApp.openByUrl("xxx")// "xxx"は使用するスプレッドシートのurl

// google公式サイトから言語コードを拾う関数
function getCodeList(){
  // googleの公式サイトのURL
  var url = "https://cloud.google.com/translate/docs/languages?hl=ja"
  // サイトのhtml情報を取得
  var html = UrlFetchApp.fetch(url).getContentText("utf-8")
  // 言語名とコードを配列に投入
  var languageList = Parser.data(html).from('<td>').to('</td>').iterate()
  // 言語とコードが混在している配列を二次元配列に変換
  var codeList = []
  for(let i = 0; i <= (languageList.length / 2); i++){
    var language = languageList[2 * i]
    var code = new String(languageList[2 * i + 1])
    code = code.replace('<code translate="no" dir="ltr">', "")
    code = code.replace('</code>', "")
    code = code.replace('<code translate="no" dir="ltr">', "")
    code = code.replace('</code>', "")
    // "または"で始まる文字列と、<a ~ </a>で囲まれた文字列を除外したい
    code = code.replace(/または.+/, "")
    code = code.replace(/<a.+/, "")
    code = code.replace("（", "")
    if(language != "undefined" && code != "undefined"){
      var codeMap = [language, code]
      codeList.push(codeMap)
    }
  }
  console.log(codeList)
  return codeList
}

// 言語コードのスクレイピング結果をスプレッドシートに出力する関数
function printCodeList(){
  codeList = getCodeList()
  var sheet = spreadsheet.getSheetByName("yyy")// "yyy"はスクレイピング用のシート名
  // 一度入力内容を全て削除
  var maxRow1 = sheet.getRange(sheet.getMaxRows(), 1).getNextDataCell(SpreadsheetApp.Direction.UP).getRow()
  var maxRow2 = sheet.getRange(sheet.getMaxRows(), 2).getNextDataCell(SpreadsheetApp.Direction.UP).getRow()
  if(maxRow1 >= maxRow2){
    var maxRow = maxRow1
  }else{
    var maxRow = maxRow2
  }
  var emptyList = []
  for(let i = 0; i <= (maxRow - 2); i++){
    emptyList.push(["",""])
  }
  sheet.getRange(2, 1, (maxRow - 1), 2).setValues(emptyList)
  var range = sheet.getRange(2, 1, codeList.length, 2)
  range.setValues(codeList)
}
```
スプレッドシートについては複数の関数で使い回すので、グローバル変数的に定義しています。

まずは

```javascript
function getCodeList(){ ... }
```
以下で言語名とコード名を配列languageListに格納しています。
本来はバラバラに取得したかったのですが、どちらもtdタグに囲まれていたので、一旦は同じ配列に入れました。

ちなみに、この配列languageListをそのまま出力すると以下のようになります。
[![Image from Gyazo](https://i.gyazo.com/bc3c3126f5210b91829fd1b79dc953a1.png)](https://gyazo.com/bc3c3126f5210b91829fd1b79dc953a1)
言語名とコード名が交互に来ているのと、コード名には余計なタグが入っていますね。

なので、以下でコード名以外のものを取り除いたあと

```javascript
  for(let i = 0; i <= (languageList.length / 2); i++){
    var language = languageList[2 * i]
    var code = new String(languageList[2 * i + 1])
    code = code.replace('<code translate="no" dir="ltr">', "")
    code = code.replace('</code>', "")
    code = code.replace('<code translate="no" dir="ltr">', "")
    code = code.replace('</code>', "")
    // "または"で始まる文字列と、(<a ~ </a>)で囲まれた文字列を除外したい
    code = code.replace(/または.+/, "")
    code = code.replace(/<a.+/, "")
    code = code.replace("（", "")
```
言語名とコード名からなる二次元配列を作成しています。

```javascript
var codeMap = [language, code]
codeList.push(codeMap)
```

二次元配列にしているのは実行速度を考慮してのことなので、時間が気にならない方はわかりやすい方法でいいと思います。

あとは２つめの関数でスプレッドシートに出力するだけですね。
こちらに関してはgasの基本がわかっていればあまり問題ないとは思いますが、以下の部分だけ補足しておきます。

```javascript
  // 一度入力内容を全て削除
  var maxRow1 = sheet.getRange(sheet.getMaxRows(), 1).getNextDataCell(SpreadsheetApp.Direction.UP).getRow()
  var maxRow2 = sheet.getRange(sheet.getMaxRows(), 2).getNextDataCell(SpreadsheetApp.Direction.UP).getRow()
  if(maxRow1 >= maxRow2){
    var maxRow = maxRow1
  }else{
    var maxRow = maxRow2
  }
  var emptyList = []
  for(let i = 0; i <= (maxRow - 2); i++){
    emptyList.push(["",""])
  }
  sheet.getRange(2, 1, (maxRow - 1), 2).setValues(emptyList)
```
やっていることはセルのデータの初期化です。
gasで対応している言語自体の数が減ってしまうと、再度スクレイピングを行なった際に過去のものが上書きされずに残ってしまう可能性があるので、そこに対処した形ですね。

最終行をgetMaxRows()ではなくgetNextDataCell()取得しているのは応答時間を考慮してのことで、maxRow1, maxRow2と2つの変数を用意して比較しているのはA列B列の一方のみにデータが入力されていた場合を考えてのことです。

これでスクレイピング結果が出力できました。

いよいよこれをもとに翻訳を行っていきます。
### 翻訳作業を行う関数の作成
スクリプトエディタに以下の関数を追加してください。

```javascript
// 言語名と言語リストからなる連想配列を取得
function getCodeMap(){
  var sheet = spreadsheet.getSheetByName("yyy")// スクレイピング結果のシート名を入力
  var lastRow = sheet.getRange(sheet.getMaxRows(), 1).getNextDataCell(SpreadsheetApp.Direction.UP).getRow()
  var codeMap = {}
  for(let i = 2; i <= lastRow; i++){
    var key = sheet.getRange(i, 1).getValue()
    var value = sheet.getRange(i, 2).getValue()
    codeMap[key] = value
  }
  return codeMap
}
// 翻訳実行関数
function translate() {
  // codeMapを取得
  var codeMap = getCodeMap()
  var sheet = spreadsheet.getSheetByName("zzz")//翻訳実行用のシート名を入力
  var maxRow = sheet.getRange(sheet.getMaxRows(), 2).getNextDataCell(SpreadsheetApp.Direction.UP).getRow()
  for(let i = 2; i <= maxRow; i++){
    var sourceLanguage = sheet.getRange(i, 1).getValue()
    var targetLanguage = sheet.getRange(i, 3).getValue()
    var sourceText = sheet.getRange(i, 2).getValue()
    sheet.getRange(i, 4).setValue(LanguageApp.translate(sourceText, codeMap[sourceLanguage], codeMap[targetLanguage]))
  }
}
```
一つ目の関数では、スクレイピング結果のシートから連想配列を作成しています。
キーが言語名で、バリューが言語コードですね。
その関数を2つ目で呼び出して、翻訳を実行しています。

では、最後にこれをボタン操作で実行できるようにしていきましょう。
### 関数をボタン操作で実行できるように編集
まず、スプレッドシートから「挿入」 > 「図形描画」と進みます。
[![Image from Gyazo](https://i.gyazo.com/073d4e585126ea88834249a7430e0389.png)](https://gyazo.com/073d4e585126ea88834249a7430e0389)
するとパワポのような画面が表示されるので、ボタンになりそうな図形を作成して保存します。
[![Image from Gyazo](https://i.gyazo.com/48c01bbe7b92b7e67f182d2c6d6a449f.jpg)](https://gyazo.com/48c01bbe7b92b7e67f182d2c6d6a449f)
保存すると自動でスプレッドシートに戻ります。
先程の図形が表示されているので、右クリックしてスクリプトの割り当てを選択します。
[![Image from Gyazo](https://i.gyazo.com/0c75cf34661101c566b6678cdbec1bdf.png)](https://gyazo.com/0c75cf34661101c566b6678cdbec1bdf)
翻訳実行関数（例ではtranslate)を入力します。
[![Image from Gyazo](https://i.gyazo.com/a6c9b35d73d984873a4c7207a21237d7.png)](https://gyazo.com/a6c9b35d73d984873a4c7207a21237d7)
これで、ボタンをクリックするだけで関数が実行されるようになりました。

### 終わりに
[前回](https://qiita.com/keishi04hrikzira/items/87ed4e5f50aec53319c2)の宣言通りに、ボタンとの連動や多言語対応については達成しました。

スマホなどの他の媒体への対応などもできれば面白いと思いますが、今の自分では少し時間がかかるかな・・・