【Python】PDFを元の書式を保ってDeepLで自動翻訳する。【要Windows・Word】

###9/9追記
クリップボード経由でのDeepLへの入力を廃止し、Javascriptを用いた方式に変更。
それに伴いSeleniumのヘッドレスモードでの使用に対応。

###9/14追記
表周りでレイアウトが崩れる問題に対応。
文章と同じparagraph中に画像が埋め込まれている場合（画像化された数式など）、文字を置き換えると画像が消えることが判明、解決策が見つかるまで翻訳対象から除外。

<details><summary>アップデート版</summary>
<div>

```python
import win32com.client
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
import time
import re
from math import ceil
from threading import Thread

DRIVER_PATH = 'chromedriver.exe'
user_agent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36'
options = Options()
options.add_argument(f'--user-agent={user_agent}')
options.add_argument('--disable-gpu')
options.add_argument('--disable-extensions')
options.add_argument('--proxy-server="direct://"')
options.add_argument('--proxy-bypass-list=*')
options.add_argument('--start-maximized')
options.add_argument('--headless') #コメントアウトでヘッドレスモード解除（Chromeが表示される）


def Deeptrans(t, driver):
    global translated_texts
    stextarea = driver.find_element_by_css_selector(
        '.lmt__textarea.lmt__source_textarea.lmt__textarea_base_style')
    ttextarea = driver.find_element_by_css_selector(
        '.lmt__textarea.lmt__target_textarea.lmt__textarea_base_style')
    for i in range(t * unit, min((t + 1) * unit, length)):
        if sourse_texts[i]: sourse_text = sourse_texts[i]
        else: continue
        if not sourse_text.strip():
            continue
        driver.execute_script(
            f'$(".lmt__source_textarea").val({repr(sourse_text)});')
        stextarea.send_keys(Keys.RIGHT)
        translated_text = ""
        while not translated_text:
            time.sleep(1)
            translated_text = ttextarea.get_property("value")
        stextarea.send_keys(Keys.CONTROL, "a")
        stextarea.send_keys(Keys.BACKSPACE)
        translated_texts.append({"index": i + 1, "text": translated_text})


def runDriver(t):
    global options
    driver = webdriver.Chrome(executable_path=DRIVER_PATH, options=options)
    url = 'https://www.deepl.com/ja/translator'
    driver.get(url)
    Deeptrans(t, driver)
    driver.quit()


def multiThreadTranslate(file_path, font):
    global length, unit, sourse_texts, translated_texts
    app = win32com.client.Dispatch("Word.Application")
    #app.Visible = True
    doc = app.Documents.Open(file_path)
    try:
        doc.Paragraphs(1).Range.Font.Name = font
    except:
        print('指定されたフォントは存在しません')
        return
    length = doc.Paragraphs.Count
    n = 9
    unit = ceil(length / n)
    sourse_texts = [
        doc.Paragraphs(i + 1).Range.Text if
        (str(doc.Paragraphs(i + 1).Range.Style) != "TableGrid" and
         str(doc.Paragraphs(min(length, i + 2)).Range.Style) != "TableGrid" and doc.Paragraphs(i + 1).Range.InlineShapes.Count)
        else None for i in range(length)
    ]
    translated_texts = []
    threads = []
    for t in range(n):
        thread = Thread(target=runDriver, args=(t, ))
        thread.setDaemon(True)
        thread.start()
        threads.append(thread)

    for thread in threads:
        thread.join()
    for translated_text in sorted(translated_texts, key=lambda i: i["index"]):
        doc.Paragraphs(translated_text["index"]
                       ).Range.Text = translated_text["text"].replace(
                           '\n', '\r')
        doc.Paragraphs(translated_text["index"]).Range.Font.Name = font
    doc.SaveAs2(FileName=re.sub("(.+)(\.pdf)", r"\1_jp.pdf", file_path),
                FileFormat=17)
    doc.Close(SaveChanges=0)
    app.Quit()
    print('Process is completed.')


if __name__ == '__main__':
    file_path = input('PDFの絶対パスを入力してください（ドラッグアンドドロップでも可）:     ')
    print('フォントを選択してください')
    fonts = {'1': '游明朝', '2': 'メイリオ', '3': 'BIZ UDP明朝 Medium', '4': 'その他'}
    font = fonts[input('   '.join(
        [", ".join(list(fonts.items())[i])
         for i in range(len(fonts))]) + ":     ")]
    if font == 'その他': font = input('フォント名を入力してください:     ')
    multiThreadTranslate(file_path, font=font)
```
</div></details>

###9/15追記
・1スレッド版で上記問題をとりあえず解消しました。
・勝手にフォントサイズが調節されてガタガタになる問題、変なインデントが入る問題を解消しました。

<details><summary>とりあえず解決版（シングルスレッド）</summary>
<div>

```python
import win32com.client
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
import time
import re
from tqdm import tqdm

DRIVER_PATH = 'chromedriver.exe'
options = Options()
options.add_argument('--disable-gpu')
options.add_argument('--disable-extensions')
options.add_argument('--proxy-server="direct://"')
options.add_argument('--proxy-bypass-list=*')
options.add_argument('--start-maximized')


def Deeptrans(file_path, font):
    app = win32com.client.Dispatch("Word.Application")
    app.Visible = True
    doc = app.Documents.Open(file_path)
    driver = webdriver.Chrome(executable_path=DRIVER_PATH,
                              chrome_options=options)
    url = 'https://www.deepl.com/ja/translator#en/ja'
    driver.get(url)
    stextarea = driver.find_element_by_css_selector(
        '.lmt__textarea.lmt__source_textarea.lmt__textarea_base_style')
    ttextarea = driver.find_element_by_css_selector(
        '.lmt__textarea.lmt__target_textarea.lmt__textarea_base_style')

    length = doc.Paragraphs.Count
    for i in tqdm(range(length)):
        if str(doc.Paragraphs(i + 1).Range.Style) == "TableGrid":
            continue
        sourse_text = doc.Paragraphs(i + 1).Range.Text
        fs = doc.Paragraphs(i + 1).Range.Font.Size
        alignment = doc.Paragraphs(i + 1).Alignment
        lindent = doc.Paragraphs(i + 1).LeftIndent
        rindent = doc.Paragraphs(i + 1).RightIndent
        if doc.Paragraphs(i + 1).Range.InlineShapes.Count:
            if sourse_text.strip() == "/": continue
            doc.Paragraphs(i + 1).Range.Font.Name = font
            t = ""
            te = []
            cnt = 0
            for j in range(doc.Paragraphs(i + 1).Range.Words.Count):
                if "/" not in doc.Paragraphs(i + 1).Range.Words(j + 1).Text:
                    t += doc.Paragraphs(i + 1).Range.Words(j + 1).Text
                    doc.Paragraphs(i + 1).Range.Words(j + 1).Text = "'' "
                else:
                    te.append(t)
                    t = ""
                    cnt += 1
            if t: te.append(t)

            for j, sourse_text in enumerate(te):
                if len(sourse_text.strip()) > 5:
                    driver.execute_script(
                        f'$(".lmt__source_textarea").val({repr(sourse_text)});'
                    )
                    stextarea.send_keys(Keys.RIGHT)
                    translated_text = ""
                    while not translated_text:
                        time.sleep(1)
                        translated_text = driver.find_element_by_css_selector(
                            '.lmt__textarea.lmt__target_textarea.lmt__textarea_base_style'
                        ).get_property("value")
                    stextarea.send_keys(Keys.CONTROL, "a")
                    stextarea.send_keys(Keys.BACKSPACE)
                    te[j] = translated_text

            g = (j for j in te)
            c = 0
            for j in doc.Paragraphs(i + 1).Range.Words:
                if j.Text == "'' ":
                    j.Text = ""
                elif "/" in j.Text:
                    try:
                        j.InsertBefore(g.__next__())
                        c += 1
                        if c == cnt:
                            j.InsertAfter(g.__next__())
                    except:
                        pass
            doc.Paragraphs(i + 1).Alignment = alignment
            doc.Paragraphs(i + 1).Range.Font.Size = fs
            doc.Paragraphs(i + 1).LeftIndent = lindent
            doc.Paragraphs(i + 1).RightIndent = rindent
            continue

        if re.search(r"[\x00-\x1F\x7F]",
                     sourse_text.strip()) or len(sourse_text.strip()) < 5:
            continue
        driver.execute_script(
            f'$(".lmt__source_textarea").val({repr(sourse_text)});')
        stextarea.send_keys(Keys.RIGHT)
        translated_text = ""
        while not translated_text:
            time.sleep(1)
            translated_text = driver.find_element_by_css_selector(
                '.lmt__textarea.lmt__target_textarea.lmt__textarea_base_style'
            ).get_property("value")
        stextarea.send_keys(Keys.CONTROL, "a")
        stextarea.send_keys(Keys.BACKSPACE)
        doc.Paragraphs(i + 1).Range.Text = translated_text
        doc.Paragraphs(i + 1).Range.Font.Name = font
        doc.Paragraphs(i + 1).Alignment = alignment
        doc.Paragraphs(i + 1).Range.Font.Size = fs
        doc.Paragraphs(i + 1).LeftIndent = lindent
        doc.Paragraphs(i + 1).RightIndent = rindent
    driver.quit()
    doc.SaveAs2(FileName=re.sub("(.+)(\.pdf)", r"\1_jp.pdf", file_path),
                FileFormat=17)
    doc.Close(SaveChanges=0)
    app.Quit()
    print('Process is completed.')


if __name__ == '__main__':
    file_path = input('PDFの絶対パスを入力してください:     ')
    print('フォントを選択してください')
    fonts = {'1': '游明朝', '2': 'メイリオ', '3': 'BIZ UDP明朝 Medium', '4': 'その他'}
    font = fonts[input('   '.join(
        [", ".join(list(fonts.items())[i])
         for i in range(len(fonts))]) + ":     ")]
    if font == 'その他': font = input('フォント名を入力してください:     ')
    Deeptrans(file_path, font)
```
</div></details>

#はじめに
以前、PDFの自動翻訳についての記事を書きましたが、
**やっぱり画像や数式、段組みなど、元の形を保ちたい！**
という心残りがあったため、手段を探したところ、Wordを利用した方法にたどり着いたので紹介したいと思います。
ただし、PDFとWordとの相性によってはあまりきれいに加工できない場合もあります。
#使用例
PDFはこちらからお借りしました→https://mirela.net.technion.ac.il/publications/
文字幅・文字数の関係で位置がずれています。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501281/74860501-a054-098c-cf46-5b63dab89b47.png" width=40%/>$EN→JA$<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/501281/2b41e93f-7d85-131c-ca61-3f053da4545d.png" width=40%/>

#必要なもの
・Windows PC
・[Microsoft Word](https://www.microsoft.com/ja-jp/microsoft-365/word)
・[ChromeDriver](https://sites.google.com/a/chromium.org/chromedriver/)（下記プログラムをそのまま実行する場合は実行ディレクトリ下に保存してください）

#流れ
***プログラム開始
　　↓
Wordで対象PDFをdocxとして開く
　　↓
Paragraphごとに文章を取得
　　↓
SeleniumでDeepL翻訳
　　↓
Word経由で書き換え
　　↓
PDFとして保存
　　↓
プログラム終了***

#実装
1段落ごとにやってると時間がかかるのでマルチスレッドで実行するようにしました。
何らかの理由で段落の位置を管理している番号がズレてしまうようでしたら、時間はかかりますが1段落ごとに翻訳するバージョンも載せておくのでお試しください。

```python
import win32com.client
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
import time
import re
import pyperclip as ppc
from math import ceil
from threading import Thread, Lock

DRIVER_PATH = 'chromedriver.exe'
options = Options()
options.add_argument('--disable-gpu')
options.add_argument('--disable-extensions')
options.add_argument('--proxy-server="direct://"')
options.add_argument('--proxy-bypass-list=*')
options.add_argument('--start-maximized')


def Deeptrans(t, driver):
    global translated_texts
    stextarea = driver.find_element_by_css_selector(
        '.lmt__textarea.lmt__source_textarea.lmt__textarea_base_style')
    ttextarea = driver.find_element_by_css_selector(
        '.lmt__textarea.lmt__target_textarea.lmt__textarea_base_style')
    for i in range(t * unit, min((t + 1) * unit, length)):
        sourse_text = sourse_texts[i]
        if re.search(r"[\x00-\x1F\x7F]",
                     sourse_text.strip()) or len(sourse_text.strip()) < 5:
            continue
        lock.acquire()
        ppc.copy(sourse_text)
        stextarea.send_keys(Keys.CONTROL, "v")
        lock.release()
        translated_text = ""
        while not translated_text:
            time.sleep(1)
            translated_text = ttextarea.get_property("value")
        stextarea.send_keys(Keys.CONTROL, "a")
        stextarea.send_keys(Keys.BACKSPACE)
        translated_texts[str(i + 1)] = translated_text


def runDriver(t):
    driver = webdriver.Chrome(DRIVER_PATH)
    url = 'https://www.deepl.com/ja/translator'
    driver.get(url)
    Deeptrans(t, driver)
    driver.quit()


def multiThreadTranslate(file_path, font):
    global lock, length, unit, sourse_texts, translated_texts
    app = win32com.client.Dispatch("Word.Application")
    app.Visible = True #コメントアウトでWord非表示
    doc = app.Documents.Open(file_path)
    try:
        doc.Paragraphs(1).Range.Font.Name = font
    except:
        print('指定されたフォントは存在しません')
        doc.Close(SaveChanges=0)
        app.Quit()
        return
    length = doc.Paragraphs.Count
    n = 9 #Chromeを9つ開いて同時実行
    unit = ceil(length / n)
    lock = Lock()
    clipboard = ppc.paste()
    sourse_texts = [doc.Paragraphs(i + 1).Range.Text for i in range(length)]
    translated_texts = {}
    threads = []
    for t in range(n):
        thread = Thread(target=runDriver, args=(t, ))
        thread.setDaemon(True)
        thread.start()
        threads.append(thread)

    for thread in threads:
        thread.join()
    for k, v in translated_texts.items():
        doc.Paragraphs(int(k)).Range.Text = v.replace('\n', '\r')
        doc.Paragraphs(int(k)).Range.Font.Name = font
    doc.SaveAs2(FileName=re.sub("(.+)(\.pdf)", r"\1_jp.pdf", file_path),
                FileFormat=17)
    doc.Close(SaveChanges=0)
    app.Quit()
    print('Process is completed.')
    ppc.copy(clipboard)


if __name__ == '__main__':
    file_path = input('PDFの絶対パスを入力してください:     ')
    print('フォントを選択してください')
    fonts = {'1': '游明朝', '2': 'メイリオ', '3': 'BIZ UDP明朝 Medium', '4': 'その他'}
    font = fonts[input('   '.join(
        [", ".join(list(fonts.items())[i])
         for i in range(len(fonts))]) + ":     ")]
    if font == 'その他': font = input('フォント名を入力してください:     ')
    multiThreadTranslate(file_path, font=font)
```

<details><summary>1段落ずつver.（プログレスバーのおまけ付き）</summary>
<div>

```python
import win32com.client
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
import time
import re
import pyperclip as ppc
from tqdm import tqdm

DRIVER_PATH = 'chromedriver.exe'
options = Options()
options.add_argument('--disable-gpu')
options.add_argument('--disable-extensions')
options.add_argument('--proxy-server="direct://"')
options.add_argument('--proxy-bypass-list=*')
options.add_argument('--start-maximized')


def Deeptrans(file_path, font):
    clipboard = ppc.paste()
    app = win32com.client.Dispatch("Word.Application")
    app.Visible = True
    doc = app.Documents.Open(file_path)
    driver = webdriver.Chrome(executable_path=DRIVER_PATH,
                              chrome_options=options)
    url = 'https://www.deepl.com/ja/translator#en/ja'
    driver.get(url)
    stextarea = driver.find_element_by_css_selector(
        '.lmt__textarea.lmt__source_textarea.lmt__textarea_base_style')
    ttextarea = driver.find_element_by_css_selector(
        '.lmt__textarea.lmt__target_textarea.lmt__textarea_base_style')

    for i in tqdm(range(doc.Paragraphs.Count)):
        sourse_text = doc.Paragraphs(i + 1).Range.Text
        if re.search(r"[\x00-\x1F\x7F]",
                     sourse_text.strip()) or len(sourse_text.strip()) < 5:
            continue
        ppc.copy(sourse_text)
        stextarea.send_keys(Keys.CONTROL, "v")
        translated_text = ""
        while not translated_text:
            time.sleep(1)
            translated_text = ttextarea.get_property("value")
        stextarea.send_keys(Keys.CONTROL, "a")
        stextarea.send_keys(Keys.BACKSPACE)
        doc.Paragraphs(i + 1).Range.Text = translated_text
        doc.Paragraphs(i + 1).Range.Font.Name = font
    driver.quit()
    doc.SaveAs2(FileName=re.sub("(.+)(\.pdf)", r"\1_jp.pdf", file_path),
                FileFormat=17)
    doc.Close(SaveChanges=0)
    app.Quit()
    print('Process is completed.')
    ppc.copy(clipboard)


if __name__ == '__main__':
    file_path = input('PDFの絶対パスを入力してください:     ')
    print('フォントを選択してください')
    fonts = {'1': '游明朝', '2': 'メイリオ', '3': 'BIZ UDP明朝 Medium', '4': 'その他'}
    font = fonts[input('   '.join(
        [", ".join(list(fonts.items())[i])
         for i in range(len(fonts))]) + ":     ")]
    if font == 'その他': font = input('フォント名を入力してください:     ')
    Deeptrans(file_path, font)
```
</div>
</details>

#使い方
使い方は保存してコマンドラインから実行するだけです(必要なライブラリは別途インストールしてください）。
しばらくすると元ファイルと同じディレクトリに`元の名前_jp.pdf`というファイルが出力されます。

#課題
数式などと地の文の区別が難しく、形が崩れたり消えてしまうことがある。
付け焼き刃で対応したが、逆に翻訳されない文などが生じてしまっている。
いい方法求む。

#まとめ
Word様様です。
興味がありましたらどうぞお試しください。