【自動翻訳】英文入力支援ツール翻訳ちゃん[Python]

#英語が弱いので英語を...
こんにちは．
英語が弱いので英語を勉強しました，というと素晴らしいですよね．
私の場合は「英語が弱いので英語から逃げる方法を考えました．」
というわけで作りました．（唐突）
**「さっさとソースコードをコピペさせろ！」という方や，技術的なお話を求めておられる方は[そろそろ技術的な話](https://qiita.com/_branch_/items/5f5cf4be81980074431b#%E3%81%9D%E3%82%8D%E3%81%9D%E3%82%8D%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E8%A9%B1)へどうぞ！**
**もっとお急ぎの方はwin10(64bit)でさえあれば下の[付録：簡易インストーラー(.bat)](https://qiita.com/_branch_/items/5f5cf4be81980074431b#%E4%BB%98%E9%8C%B2win10-64bit%E7%94%A8%E7%B0%A1%E6%98%93%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%A9)を利用すれば10分ほどで導入できると思います．**

前提として，Python3.Xが導入されている必要があるので[Python 3.7.3のダウンロードとインストール --Letsプログラミング](https://www.javadrive.jp/python/install/index1.html)　を参考に導入してみてください．もちろん3.7.3である必要などなく，最新版でかまいません．

##先に背景をば
私は海外の取引先と英語でやり取りします．つまりメールに限らず英文でやり取りする機会が比較的多いのですが，英語できないマンに英語でのやりとりを迫るとどうなるでしょうか．
そう，Google翻訳に頼りまくることになるんですよね．こういうことをするようになります．

```
1.日本語で内容を考える
2.Google翻訳に投げる
3.英文をゲット
4.ゲットした英文をGoogleで再翻訳
5.再翻訳された日本語を吟味
　→原文と意味に乖離があるようなら必要に応じて1に戻って再度文章を練り直す
```

**これ，あり得なくないですか！？**
めちゃくちゃめんどくさい．いや，英語できるようになれよって話ですけど．
英語に苦手意識があるとこの作業をするしかありません．40~50代で急に英語使わなきゃならなくなった，って人は特にそうじゃないでしょうか？

時間がかかるせいか，部署の残業時間もドエライことになっているし，，，ということで，作りました．

##次はカスを紹介します，
この翻訳ちゃん，入力された日本語をクリップボードにコピーした状態で起動すれば~~文末の"。"~~で区切って一文ごとに分割して翻訳してくれます．
**8/3更新:区切りを改行に変更しました.**
翻訳が完了すればボタンを押して，翻訳された英語をクリップボードにコピーできます．
以下に初期起動時の説明書兼お試し用プリセットのスクリーンショットを示します．
![カス](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642336/bc718836-c285-70dc-ae16-1150c1e0ce7e.png)

**見た目が完全にカスです．**特に一番下のボタン3つがひどい．
技術力が足りてなくてこんなものしかできませんでしたが，
初めてGUIをwxpythonでデザインしたので，
許してくだ，
さい，


#そろそろ技術的な話
ここまでお付き合いいただきありがとうございます．
以下では環境を示しながら導入のためのコードを紹介してゆきます．
##前提環境
**win10 64bit**
**python3.8.4**　pythonのPATHは通っている前提
これだけです．win10 32bit版の方はうまく読み替えて導入を進めてください．

この環境にChromiumというブラウザをインストールして，それを扱うためのパッケージをインストールしていきます．

##python用必要パッケージのインストール
コマンドプロンプトを起動して以下のコマンドを叩いてください．

```
pip install chromedriver_binary==84.0.4147.30.0
pip install wxpython
pip install pyperclip
pip install selenium
pip install beautifulsoup4
```
案外すぐ終わると思います．

##Chromiumのインストール
[Win10 64bit向けChromiumを公式サイト](https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Win_x64%2F768952%2Fchrome-win.zip?generation=1589490546798193&alt=media)からDLし，解凍してください．恐らく`chrome-win`というフォルダが生成されるので，そのまま`C:\Program Files`直下に移動してください．移動できればChromiumの準備は完了です．
Win10 64bit以外の方で，公式リポジトリからDLするという方はChromiumのバージョンが `84.0.4147.0`になるようにしてください．厳密にはメジャーバージョンが84であればいいのですが，動作確認はできてないので...

####Chromiumとは？
オープンソースのwebブラウザで，下のアイコンからわかる通りGoogle Chromeのベースとなるものです．それ以外にも，2020年7月現在OperaやMicrosoft EdgeもChromiumをもとに開発されています．
![chromium_logo](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642336/be2e92be-a1eb-6056-d9bb-dcb1badf1ad3.png)

>-[Wikipwdia](https://ja.wikipedia.org/wiki/Chromium)
>Chromiumプロジェクトの名前の由来は、鉄などのめっきに使われることで知られる金属系元素のクロム（英語でchromium）である。そのクロムめっきを施した金属を英語で chrome と呼ぶことから、「(Google) Chromeを作るのに使うモノ」という意味でこの名称となった。

ただ，普段使いの範囲ではほぼGoogle Chromeのような使用感であり，（少なくとも私にとっては）深く考えることなくChromiumを使用することができます．

####Chromiumを使用するメリット
今回制御対象のブラウザをGoogle Chromeではなく，わざわざChromiumとしたのは以下の3点です．
1.Google Chromeは自動アップデートがある
2.メインで運用している人が少ない
3.それでいてGoogle Chromeと同じ使い勝手で操作できる


まず1に関して，ブラウザに限らず自動アップデートがないアプリは本来セキュリティ上の問題をはじめとして様々な制約があり，不便です．
しかし今回の用途では`ドライバとアプリのメジャーバージョンを一致させる必要がある`必要があるためむしろメリットとなります．
さらに今回はヘッドレスモードで起動するのですが，この場合ユーザは”ただのバックグラウンドアプリ”という認識を持つ傾向があるため，意図してChromiumからwebに接続することはありません．つまりセキュリティ的にほとんど心配がない（もちろんゼロではない）といえるのです．

2に関しては，この翻訳ちゃん，もともとは部署内展開を想定していたので「普段使いのブラウザが制御されると不都合が生じる」可能性がありました．私自身ブラウザに明るいわけではないので，万一に備えてIEやMS Edge, Google Chrome, FireFox, Opera, Sleipnirあたりは避けたかった．

**3に関して，開発者としては正直これが一番デカい．** ，というのも，Google Chromeと同じドライバで動くので，Google Chromeで開発してChromiumで実装→本番環境に近い状況でテスト，というのが簡単に実現できました．はかどる．

こういう訳でChromiumはいろいろと都合がよかったのです．

##翻訳ちゃん
いよいよ本体のソースコードです．
上でインストールしたChromiumに対してSeleniumから制御をかけて翻訳します．

```python
import wx
import wx.lib.scrolledpanel
import pyperclip
import time
import urllib.parse
import chromedriver_binary
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
from time import sleep


class Translator:       #seleniumによる翻訳を定義するクラス
    def __init__(self):
        self.options = Options()
        self.options.binary_location =  "C:\\Program Files\\chrome-win\\chrome.exe"
        self.options.add_argument("--headless")          # ヘッドレスモードで起動
        self.browser = webdriver.Chrome(options=self.options)
        #self.browser.minimize_window()                   # プロキシ有効の場合は最小化で対応
        self.browser.implicitly_wait(2)

    def trans(self, txt , lg1 , lg2):      # lg1からlg2に翻訳する関数
        if txt == "":                      # 入力が空なら空で返す
            return txt
        # 翻訳したい文をURLに埋め込んでからアクセス
        text_for_url = urllib.parse.quote_plus(txt, safe='')
        url = "https://translate.google.co.jp/#{1}/{2}/{0}".format(text_for_url , lg1 , lg2)
        self.browser.get(url)

        # 少し待つ
        wait_time = len(txt) / 1000
        if wait_time < 0.5:
            wait_time = 0.5
        time.sleep(wait_time)

        # 翻訳結果を抽出
        soup = BeautifulSoup(self.browser.page_source, "html.parser")
        ret =  soup.find(class_="tlid-translation translation")

        return ret.text    

    def quit(self):
        self.browser.quit()


def main():
    # クリップボードの内容を取得
    orig = str(pyperclip.paste())

    # 改行を検出して分割．この際改行情報"\r\n"は失われる
    orignal_elements = orig.splitlines()
    print(orignal_elements)

    # 翻訳に関わる文字列を格納する変数
    origs = []
    en_txt = []
    jp_txt = []

    # 空白行を取得するためのグローバル変数
    global blank_row
    blank_row = []
    count = 0
    while count < len(orignal_elements):
        buf = orignal_elements[count]           #"   "のような空白のみの要素を抹殺
        buf = buf.replace(" " , "")             #単にスペースを消すだけでは外国語に対応できないので，差分をとる
        if buf != "":                           #空白でない場合は翻訳対象に追加
            origs.append(orignal_elements[count])
        else:
            blank_row.append(count)             #空白の場合はもともと空白行だったので，要素の場所をintで取得
        count += 1
    
    # 翻訳する2言語を設定[母国語,外国語]    将来的にプルダウンから言語を選択できるように構築する予定．
    global lg
    lg = ["ja" , "en"]

    # 逆翻訳にする二言語を設定 [外国語,日本語]
    global rev_lg
    rev_lg = list(reversed(lg))

    # GUIの準備
    app = wx.App()

    # 読み込み中の表示
    read_frame = wx.Frame(None, wx.ID_ANY, "翻訳中...", size=(250,0))
    read_frame.Centre() #中央に表示
    read_frame.Show()

    global rows
    rows = len(origs)           #lenは0を含むため，行数に注意

    for row in range(rows):     # 原文をもとに先に翻訳しておく
        #原文
        txt = origs[row]
        #print("原文：",txt)

        #英文
        en_txt.append(translator.trans(origs[row], *lg))
        #print("英文：",en_txt)

        #再翻訳
        jp_txt.append(translator.trans(en_txt[row], *rev_lg))

    size = (900,600)
    global frame
    frame = wx.Frame(None, wx.ID_ANY, '翻訳ちゃん', size=size)
    panel = wx.lib.scrolledpanel.ScrolledPanel(frame,-1, size=size, pos=(0,28), style=wx.SIMPLE_BORDER)
    panel.SetupScrolling()
    panel.SetBackgroundColour('#AFAFAF')

    global text
    global en_text
    global jp_text
    global btn
    layout = wx.FlexGridSizer(rows+1,4,0,0)

    text = [""]*rows            #原文テキストウィジェットの準備
    en_text = [""]*rows         #英文テキストウィジェットの準備
    jp_text = [""]*rows         #再訳文テキストウィジェットの準備
    btn = [""]*rows             #翻訳ボタンウィジェットの準備

    cellsize = (270,90)
    for row in range(rows):
        #原文
        txt = origs[row]
        text[row] = wx.TextCtrl(panel, row , txt, style = wx.TE_MULTILINE,size=cellsize)
        #print("原文：",txt)

        #英文
        en_text[row] = wx.TextCtrl(panel, row , en_txt[row], style = wx.TE_MULTILINE,size=cellsize)
        en_text[row].Disable()      #書き込み禁止
        #print("英文：",en_txt)

        #再翻訳
        jp_text[row] = wx.TextCtrl(panel, row , jp_txt[row], style = wx.TE_MULTILINE,size=cellsize)
        jp_text[row].Disable()      #書き込み禁止
        #print("再翻訳文：",jp_txt)

        #翻訳ボタン
        btn[row] = wx.Button(panel, row, "翻訳", size=(60, 40))
        btn[row].Bind(wx.EVT_BUTTON, OnClickBtn)            #ボタンをイベントにバインド


        #ウィジェットの配置
        #layout.Add(text[row], flag=wx.ALIGN_LEFT | wx.GROW)         #原文
        layout.Add(text[row], flag=wx.SHAPED)         #原文
        layout.Add(jp_text[row], flag=wx.SHAPED)      #再翻訳
        layout.Add(en_text[row], flag=wx.SHAPED)      #英文        
        layout.Add(btn[row],flag=wx.SHAPED | 
            wx.ALIGN_CENTER_VERTICAL | wx.TE_MULTILINE)             #翻訳ボタン
    
    copy_btn = wx.Button(panel, wx.ID_ANY, "翻訳完了", size=(80, 40))
    copy_btn.Bind(wx.EVT_BUTTON, OnClickCopyBtn)
    layout.Add(copy_btn,flag=wx.SHAPED | 
        wx.ALIGN_CENTER_VERTICAL | wx.TE_MULTILINE)

    retrans_btn = wx.Button(panel, wx.ID_ANY, "各セルをリセットして再翻訳", size=(200, 40))
    retrans_btn.Bind(wx.EVT_BUTTON, OnClickRetransBtn)
    layout.Add(retrans_btn,flag=wx.SHAPED | 
        wx.ALIGN_CENTER_VERTICAL | wx.TE_MULTILINE)
    
    exit_btn = wx.Button(panel, wx.ID_ANY, "翻訳ちゃんを終了", size=(120, 40))
    exit_btn.Bind(wx.EVT_BUTTON, OnClickExitBtn)
    layout.Add(exit_btn,flag=wx.SHAPED | 
        wx.ALIGN_CENTER_VERTICAL | wx.TE_MULTILINE)

    # ボタンの配置
    layout.AddGrowableCol(0, 3)
    layout.AddGrowableCol(1, 3)
    layout.AddGrowableCol(2, 3)
    layout.AddGrowableCol(3, 1)

    # レイアウトの更新
    panel.SetSizer(layout)

    # ステータスバーを定義
    frame.CreateStatusBar()
    frame.SetStatusText("オンラインサービス利用のため守秘義務を遵守の上ご利用ください")
    
    read_frame.Close()
    frame.Centre() #中央に表示
    frame.Show()
    app.MainLoop()


def OnClickBtn(event):
    num = event.GetId()

    btn[num].Disable()
    
    n_txt = text[num].GetValue()
    n_en_txt = translator.trans(n_txt, *lg)
    n_jp_txt = translator.trans(n_en_txt, *rev_lg)
    
    en_text[num].SetValue(n_en_txt)
    jp_text[num].SetValue(n_jp_txt)

    btn[num].Enable()

def copy_all():         #翻訳結果の英文をクリップボードにコピーする関数

    fin_txt = ""        # クリップボードにコピーする文字列 
    fin_txts = []       # 翻訳後の英文をセルごとに格納する文字列

    for row in range(rows):
        fin_txts.append(en_text[row].GetValue()+"\r\n")     #splitlinesメソッドで失われた改行情報を復元

    for blank in blank_row:
        fin_txts.insert(blank,"\r\n")                       #空白行の場所はblank_rowに格納されているので空白行を追加

    for element in fin_txts:
        fin_txt += element                                  #改行情報を復元したので出力用文字列として結合

    return fin_txt

def OnClickCopyBtn(event):          #【翻訳完了】ボタンが押された場合
    pyperclip.copy(copy_all())

def OnClickRetransBtn(event):       #【各セルをリセットして再翻訳】ボタンが押された場合
    frame.Close()
    main()

def OnClickExitBtn(event):          #【翻訳ちゃんを終了】ボタンが押された場合
    pyperclip.copy(copy_all())
    frame.Close()
    translator.quit()


if __name__ == "__main__":
    translator=Translator()
main()
```
これを適当な名前のpythonスクリプトファイル（.py）として保存してください．こいつを右クリックから実行すると翻訳ちゃんが起動します．クリップボードに適当な日本語をコピーした状態で起動してみてください．

Seleniumに関しては
[【Python】Seleniumの使用方法メモ](https://qiita.com/motoki1990/items/a59a09c5966ce52128be)（@motoki1990様）や
[Python + Selenium で Chrome の自動操作を一通り](https://qiita.com/memakura/items/20a02161fa7e18d8a693)（@mekakura様）を，

wxpthonの使用方法に関しては
[wxPythonを使ってみる](https://qiita.com/Kenta-Han/items/5ac4f38d518e3262905f)（@Kenta-Han様）を，

自動翻訳に関しては
[SeleniumでGoogle翻訳を自動化する --晴耕雨読](https://tex2e.github.io/blog/python/selenium-google-translate)を，

それぞれ参考にしました．
組み合わせてアレンジした程度ですが，突貫工事で作成したコードなので，いろいろと構造的におかしなことになっている気がします．特にmain関数周りの構造化が怪しい．怪しくない？
もともと平文でmain化していたので，動くように急遽global化している部分もあります．
このあたりのおかしな部分は私自身がまだまだ初心者であることが原因なので，有識者の方のご教授をお待ちしております．

また，この翻訳ちゃんをヘビーユーズしてくださる素晴らしい方のために，ホットキー（ショートカットキー）での起動を実現することもできるので，お伝えしておきます．こちらを参考に作成してみてください．これに関して特に何かあたらしくインストールしたりする必要はありません．
[Pythonプログラム用ショートカットを作る（Windows10）](https://qiita.com/odanny/items/4e3a320b70df5136c622)-@Odanny様


今後のアップデートでは英語に限らず他の言語にも対応していくつもりなので，一つの関数で扱えるように学習を進めて参ります．

##付録：win10 64bit用簡易インストーラ
最後に簡易インストーラの紹介をいたします．
**必ず管理者権限で実行してください**

```bat

@echo off
rem 文字エンコードの確認
chcp 65001
setlocal

start https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Win_x64%%2F768952%%2Fchrome-win.zip?generation=1589490546798193^&alt=media
echo DLが完了したら[Enter]keyを押してください && pause
Powershell -command "Expand-Archive -Path $env:USERPROFILE\Downloads\Win_x64_768952_chrome-win.zip -DestinationPath C:\Program` Files"
del %USERPROFILE%\Downloads\Win_x64_768952_chrome-win.zip

py -m pip install chromedriver_binary==84.0.4147.30.0
py -m pip install wxpython
py -m pip install pyperclip
py -m pip install selenium
py -m pip install beautifulsoup4

endlocal
exit
```
これをなんでもいいのでバッチファイル（.bat）として保存し，**管理者権限で実行してください．**

##翻訳ちゃんの展望
この翻訳ちゃん，いろいろと機能不足がはなはだしいです．
今のところ以下の機能の実装を予定しています．

1.GUI/UXの洗練
  1-1.各セルのリサイズ機能
　1-2.セル追加ボタン
　1-3.翻訳済みのセルから順次表示
2.コンボボックスで言語選択
~~3.区切り文字を[。]から[．(全角ピリオド)][？]などに対応~~
　　　→7/29:改行情報の保持に対応する形で改善しました．
4.辞書機能をもつ単語帳（業界用語や社内用語に対応させる）　例:マル秘⇄confidental
5.常駐モード
6.単語変換機能

#参考文献一覧
何度も訪問させていただき，大変参考になった記事です．この場をお借りしてお礼申し上げます．
また，この記事に問題があればすぐに削除いたします．

・[【Python】Seleniumの使用方法メモ](https://qiita.com/motoki1990/items/a59a09c5966ce52128be) -@motoki1990様
・[Python + Selenium で Chrome の自動操作を一通り](https://qiita.com/memakura/items/20a02161fa7e18d8a693) -@mekakura様
・[wxPythonを使ってみる](https://qiita.com/Kenta-Han/items/5ac4f38d518e3262905f) -@Kenta-Han様
・[SeleniumでGoogle翻訳を自動化する --晴耕雨読](https://tex2e.github.io/blog/python/selenium-google-translate)
・[Pythonプログラム用ショートカットを作る（Windows10）](https://qiita.com/odanny/items/4e3a320b70df5136c622)-@Odanny様

コピペで動かしてくれても全然いいから，これを機にいろんな人がPythonに興味を持ってくれたらいいなア...


#更新情報
7/29
ソースコードをアップデート
`・動作の高速化
・改行情報の保持に対応
　→それにともなって区切り文字を変更（句点→改行）
ソースコードに反映済みです．
`
仕様を変更したので，翻訳ちゃんのイメージ（カスみたいな画像）を更新しました．