【Google Apps Script】英文のGoogleドキュメントをDeepL APIで翻訳してドキュメントに書き込む

## 実現したこと

**英文のGoogleドキュメント** を **日本語に翻訳** して、**Googleドキュメントに書き込む**

![app](https://user-images.githubusercontent.com/47489629/166102834-db1aef02-4024-4c69-ab28-c6b05c1533c5.gif)

## 今回のきっかけ

以下3つがきっかけでした。

- [ウェブ版のDeepL](https://www.deepl.com/ja/translator?referrer=https%3A%2F%2Fwww.google.com%2F)だと、無料プランでは一度に5,000字までしか翻訳できない（2022/04/30時点）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/3d9b95fe-7b54-ec2d-09c3-edbef3a884b5.png)

- ウェブ版で翻訳できても、その後にGoogleドキュメントに手動でコピペする手間が発生する
- Google Apps ScriptとDeepL API触ってみたい！

今回やりたいことは要するに「**英語PDFを日本語で読みたい**」なので、**それを叶えるツールは既にあります**。（こちらとか→「[DeepLでPDFファイルのレイアウトを崩さず丸ごと翻訳できるサードパーティChrome拡張機能「DeepL Opener」を使ってみた](https://gigazine.net/news/20211231-deepl-opener/)」）

にもかかわらず、私が今回使わなかった理由は「一番の動機は3番目で、1つ目と2つ目は後付けだから」です。笑。

それでも気になる！方の参考になれば幸いです。

## 準備したもの

- 翻訳元の英文PDF（GoogleドキュメントでももちろんOK）
- 翻訳したテキストを書き込むGoogleドキュメント
- Google Apps Script（以降GAS）
- DeepLの無料プランへの登録

:::note warn
今回翻訳に使用するDeepL API（v2）は、**一度のリクエストで翻訳できる文字数に限りがあります**。公式ドキュメントによると128 KiBとのことなので、**英語だと12万字ほど**までは可能なのではと思います。この記事に載せたスクリプトではそれ以上の文字数があるドキュメントを翻訳対象として指定するとエラーになってしまうので、**スクリプトを修正する、または文章を複数回に分けて実行する**といった工夫が必要です。
:::

## 実現方法

### 1.PDFをGoogleドキュメントに変換

:::note warn
Googleドキュメントが既にある場合は、このフローは飛ばしてください。:pray:
:::

とても簡単で、２ステップで完了です。

1. Googleドライブに該当のPDFをアップロード
1. そのPDFを副クリック -> 「アプリで開く」 -> 「Googleドキュメント」を選択

これだけです。

ただ参考にした「[PDF データをテキスト化するなら Googleドキュメントが使える！](https://www.sungrove.co.jp/pdf-google-document/)」によると、完璧ではないようで、

- 誤字脱字がある可能性はあるらしい -> ちゃんと確認してないですが
- 縦書きドキュメントは横書きで変換されるらしい -> これも確認してはないですが

といったことがあるらしいので、これらを許容できるドキュメントかどうかの確認は必要そうです。

### 2.Googleドキュメントを日本語に翻訳して、別のGoogleドキュメントに書き込む

まずは、Google Apps Scriptを作成します。

![スクリーンショット 2022-04-30 18.32.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/eca0111d-4d1c-a97c-da68-597e4d78cd78.png)

そして以下のスクリプトを貼り付けます。
（エラーハンドリングとかしてない雑スクリプトです。ご自由に改修してお使いください。）

```js:translate.gas
function main() {
  // ドキュメントの読み込み
  const text = readDocument();
  // 英->日翻訳
  const convertedText = translate(text);
  // ドキュメントへの書き込み
  editDocument(convertedText);
}

function translate(text) {
  console.log('translate処理開始');
  const apiUrl = 'https://api-free.deepl.com/v2/translate';
  const apiKey = '<DeepL APIのAPIキー>';  // ①
  const lang = 'JA';
  const content = encodeURI('auth_key=' + apiKey + '&text=' + text + '&target_lang=' + lang);
  const header = {
    "accept":"gzip, */*",
    "timeout":"20000",
    "Content-Type":"application/x-www-form-urlencoded"
  };
  const parameters = {
    "method": "post",
    "headers": header,
    'payload': content
  }
  const response =  UrlFetchApp.fetch(apiUrl, parameters).getContentText();
  let json = JSON.parse(response);
  const convertedText = json['translations'][0]['text'];
  console.log(convertedText);
  console.log('translate処理終了');
  return convertedText;
}

function readDocument() {
  console.log('readDocument処理開始');
  const DOC_URL = "<翻訳元のGoogleドキュメントのURL>";  // ②
  const doc = DocumentApp.openByUrl(DOC_URL);
  const text = doc.getBody().getText();
  console.log('readDocument処理終了');
  return text;
}

function editDocument(text) {
  console.log('editDocument処理開始');
  const doc = DocumentApp.openByUrl('<翻訳後のテキストを書き込むGoogleドキュメントのURL>'); // ③
  const body = doc.getBody();
  body.appendParagraph('\n' + text);
  console.log('editDocument処理終了');
}
```

念のためですが、上記のスクリプトの実行する関数名を`main`を選択しておきます。

![スクリーンショット 2022-04-30 18.35.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/3fb49c5f-96ab-d30a-944b-e78146525827.png)

まだ翻訳するGoogleドキュメント、翻訳後に書き込むドキュメントを指定していませんので、
**スクリプトの②、③の行をドキュメントのURLに書き換え**ます。（「https://docs.google.com/document/d/{ドキュメントID}/edit 」のようなURL）

また今回は翻訳する方法としてDeepLのAPI（v2）を使用しますので、そのAPIを使用するための**APIキーを取得**する必要があります。

それについては説明は割愛するのですが、
「[DeepL翻訳の無料版APIキーの登録発行手順！世界一のAI翻訳サービスをAPI利用](https://auto-worker.com/blog/?p=5030)」の記事がわかりやすかったです。

**取得できたら、スクリプトの①の行の値を取得したAPIキーで書き換え**ます。

### 3.Let's 翻訳

では早速翻訳します！といっても、「[2. Googleドキュメントを日本語に翻訳して、別のGoogleドキュメントに書き込む](#2.Googleドキュメントを日本語に翻訳して、別のGoogleドキュメントに書き込む)」まで完了していれば、あとは「実行」ボタンを押すだけです。

![スクリーンショット 2022-04-30 18.49.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/65992999-c0bc-55d1-5533-ff130dc27afb.png)

## 遭遇したエラーとそこからの学び

私の経験・スキルとしては、

- JavaScriptあまり触ったことない
- GAS触るの初めて
- DeepL API使うの初めて

という三拍子揃っていたため、いくつかハマりました。
その記録をメモしておきます。

### GASの`UrlFetchApp.fetch`のURL長が2KB以内である必要がある -> GETではなくPOSTでリクエスト

![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/f231ba13-1c89-ade2-355e-4c461ca8b700.png)

というわけで「[Quotas for Google Services](https://developers.google.com/apps-script/guides/services/quotas)」を確認しますと、「2 KB / call」と記載があります。（2022年04/30時点）

![スクリーンショット 2022-04-30 19.17.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/28852c6e-1711-4ba1-1859-d7c8163b9986.png)


今回私は5,000字を超えた長文を翻訳したかったので、この制限には余裕で引っかかりました。
というわけでGETではなく、POSTでリクエストするようスクリプトを作成しました。

### DeepL APIの一度のリクエストボディは128KiBまで -> 今回はスクリプトを単純に複数回実行（2回で済んだので...）

![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/f8caa868-c0e6-3173-34db-b86f549c2329.png)

[準備したもの](#準備したもの)にも記載したのですが、
DeepL API（v2）ではリクエストボディに含められるサイズは、128KiBだそうです。（2022/04/30時点）
なので英語なら12万字ほどは一度のリクエストで翻訳できるかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/401966/0a16487a-de3e-2667-0090-670fe45c3ed6.png)

16万字だと上記のエラーが発生して、10万字は問題なかったので、大体あっているんじゃないかと思います。

## おわりに

GASもDeepL APIも触るのは初めてでしたので、良い勉強になりました。
遅い気づきにはありますが、GASはもっといろんなことができそうなので、時間ある時に深堀してみたいと思います。:blush:

## 参考

- [PDF データをテキスト化するなら Googleドキュメントが使える！](https://www.sungrove.co.jp/pdf-google-document/)
- [GASのUrlfetchAppのHTTPリクエストの制限事項(URL長やリクエスト数の上限) | AutoWorker〜Google Apps Script(GAS)とSikuliで始める業務改善入門](https://auto-worker.com/blog/?p=5162)
- [【GoogleAppsScript】GASでDeepL APIを叩く](https://qiita.com/pnizo/items/338b1a1398127a8942e5)
- [Quotas for Google Services](https://developers.google.com/apps-script/guides/services/quotas)
- [DeepL 技術資料 Accessing the API](https://www.deepl.com/ja/docs-api/accessing-the-api/limits/)