思索的逍遥の記。

いろいろな考え事。

Pythonista 3 で Google 翻訳・改 GAS との連携

結局、前回の記事が最終回ではなくなったという。

今回の記事では、WebView を使わず appex モジュールを使用して共有シートから直接起動できるスクリプトを紹介する。

続編に至った経緯

以前、Pythonista 3 で Google 翻訳のページにアクセスするスクリプトを紹介した。

sutukeisu.hatenablog.com

しかし、iOS 13 へのアップグレードにより、Pythonista 3 とショートカットアプリの連携がうまくいかなくなってしまった。Pythonista 3 のスクリプトに引数が渡せなくなってしまったのだ。スクリプトは起動するが、明らかにサイトの URL を渡すことができなくなっていた。

拙い自作スクリプトではあったが、マルチウィンドウにできない iPhone ではそこそこ重宝していたので困ってしまった。

Pythonista フォーラムの iOS 13 についてのトピックでも、同様の報告が多数見られる。Pythonista 3 の更新は最近は停滞しているので、最悪未対応のままということも考えられる。

---- 追記(2020/04/18)----

その後、Pythonista は iOS 13 向けにアップデートされたが、ショートカットアプリで引数を渡せない不具合は残った。その代わり、URLスキームを以前より利用しやすくする改善が施された。その機能を使って、ショートカットアプリから引数を渡す方法を以下の記事で解説した。

sutukeisu.hatenablog.com

-- 追記終わり --

ということで、ショートカットアプリを使わない別の方法を考えた。

今回のアプローチ

前回までは共有シートからショートカットアプリを挟んで Pythonista 3 を起動する方法を取っていた。理由は、共有シートから直接 Pythonista 3 を起動すると、WebView を扱うスクリプトの場合、アプリ拡張に割り当てられたメモリ制限を超えてしまってアプリが落ちてしまったからである。しかし、WebView を用いない、手っ取り早い方法を思いつかなかったため、ショートカットアプリを頼った。

最初のスクリプト制作からしばらく経って、趣味人ながらも知識に蓄えができ、 WebView を扱わないスクリプト作成への解決への道筋がついた。それにより、新しいスクリプト制作に至った。

道筋とは具体的には次のようなものだ:

  • Google Apps Script(以下、GAS)で翻訳 API を作る
  • iOS のアプリで選択した文字列を appex モジュール経由で取得
  • 文字列を API に送り、翻訳結果を取得する
  • 得られた翻訳結果を表示

スクリプトの使用イメージ

上述の流れを実現すると以下の画像のような使用感になる。GAS による Web アプリの起動がやや遅いので少し待たされるが、アプリの遷移もなく翻訳結果を得ることが可能である。

f:id:sutukeisu:20191008230818g:plain

下準備

今回の肝は、以下の Qiita の記事で紹介されていた、Google 翻訳の Web API を GAS で自作する方法である(この記事がなければ今回のスクリプトは作らなかった)。

qiita.com

まずはこの Web API を自分のアカウントで同様に作成する。上記記事の作者のものを使うこともできるが、おそらく LanguageApp の使用回数に制限があると考えられるので、上記記事作者の使用回数を消費しないように、できるだけ自作することを推奨する。

GAS の始め方は以下の記事などが参考になる。

note.mu

ソースコード

上記の GAS のコードも含めて、コード全体は以下のようになっている:

The script to translate sentences by using the web ...

setting.json は、スクリプトの設定を JSON 形式で記述したファイルである。https://url.to.your.webapp の部分は自作 Web API の URL が入る。

UI 部品の解説

以下に translation2.pyui の編集画面を示した。また、画像の各 UI 部品に付した番号と、それに対応する部品名、Action に指定された関数名を表で示しておく(関数名がいい加減なのは許していただきたい)。その他の詳細は前掲の .pyui ファイル(JSON 形式)に記述されている。

(ボタンのサフィックスの順番がバラバラなのは、その順番に実装したから(汗))

番号 部品名 Action に指定された関数
button3 push_source
button1 switch_source_target
button4 push_target
label1 -
button6 add_lang
textview1 -
label2 -
button2 push_translate
button5 switch_tv_1_2
textview2 -

①と③は、それぞれ翻訳前と翻訳後の言語を指定するボタン。②は翻訳前と翻訳後の言語を入れ替えるボタン。⑤は扱う言語を追加するボタン(ロケールIDを追加する。詳しくは以下の記事が参考になる)。

[ ロケールID ] ja-jp と ja は違うの?どんな意味? ( LCID ) – 行け!偏差値40プログラマー

⑧は翻訳ボタン、⑨は上の TextView と下の TextView の内容を入れ替えるボタンである。

コードの解説

GUI 部品と関数の連携などは前回までの記事で解説したので、今回は主に前回までになかった要素を解説する。

appex モジュール

前掲の GIF 画像では、文字列を選択後、ポップアップメニューから「共有...」(英語版なら "Share...")→ "Run Pythonista Script" → 「Google 翻訳」と進んだ(「Google 翻訳」のアイコンは、"Run Pythonista Script" 画面の左上にある "Edit" ボタンからの操作で追加した)。

この方法でスクリプトを起動すると、appex モジュールを使って次のように選択した文字列を取得できる。

import appex

text = appex.get_text()

ちなみに、ポップアップメニューからではなくブラウザの共有ボタンからスクリプトを起動すると、選択された文字列の代わりに、その時開いている Web ページの URL が取得できる。

Web アプリへの URL の構築

自作 Web API への URLに続いて、?text=翻訳したい文字列&source=翻訳前言語&target=翻訳後言語 というクエリを追加することで、API に翻訳に必要な情報を渡している。urllib3 モジュールはこれをパイソニックに構築するには打ってつけである。

urllib3 モジュールはサードパーティ製。Pythonista 3 に pip コマンドでサードパーティ製モジュールをインストールするには、StaSh というツールを使う必要があるが、幸運にも urllib3 はデフォルトで Pythonista 3 にインストールされている。Pythonista 3 には、いくつかの便利なサードパーティ製モジュールがあらかじめインストールされていて、非常に便利である。

今回のコードでは、translate 関数でこのモジュールを使っている。以下にその部分の概略だけ抜き出す。

import urllib3

# PoolManagerの初期化。
# スクリプトではmain関数で行っている
http = urllib3.PoolManager()

def translate(翻訳したい文字列):
    # クエリをPythonの辞書で表現する
    query = {
        'text': 翻訳したい文字列,
        'source': 翻訳前言語,
        'target': 翻訳後言語,
    }
    # RequestMethodsのrequestメソッドでURLを構築し、HTTP RequestをWeb APIに送る
    # 戻り値のHTTPResponseで返ってきたResponseの情報が得られる
    r = http.request('GET', Web APIへのURL, fields=query)

    # ステータスコードが 200 OK なら
    if r.status == 200:
        # HTTPResponseのdata属性に翻訳結果が入っている
        # バイト列なので文字列にデコードする
        return r.data.decode('utf-8')
     else:
        # エラーコードならNoneを返す
        return None

urllib3.request.RequestMethodsrequest メソッドが便利なのは、Python 辞書からクエリを構築できることだけではない。翻訳したい文字列にアスキーコード以外の文字が入っている場合は、手動でクエリを作るならその文字をエスケープ処理しなくてはいけなくなるが、このメソッドはそこまで自動でエンコードしてくれる。

スクリプトを書いていた最初の頃は標準ライブラリの urllib を使おうとしていて、https が使えなかったり、エスケープ処理が念頭になく URL を作れなかったりしてハマったので、urllib3 を見て本当に便利に感じた。

InsecureRequestWarning を消す

urllib3公式ドキュメントによると、証明書の検証を有効にせずに HTTPS の URL でリクエストするとInsecureRequestWarningという警告が表示されるようである。今回は無視したかったので方法を探したら、以下の taratail の Q&A があった。今回はこの回答で示されたコードをそのまま使った。

teratail.com

ソフトウェアキーボードを非表示にする

.pyui ファイルの設定と、ボタンのイベントで呼ばれる関数の定義が終わったところで、最後に困ったのは TextView で文章を編集した場合、ソフトウェアキーボードを非表示にする手段がなく、翻訳結果がキーボードに隠れて見えなくなることであった。

Pythonista のドキュメントにはキーボード非表示について具体的な説明がない。仕方なく、Objective C / Swift による一般的な iOS 開発ではどうするのか調べると以下がヒットした。

blog.personal-factory.com

上記の記事の実装を行うことで、TextView 外の領域をタップするとキーボードが非表示になる。

Pythonista は objc_util というモジュールを持っており、Objective C の API を叩ける。原理的には同じ実装が可能なはずである。この記事をヒントに調査を続けると、Pythonista フォーラムでまさに同様の実装を解説している方がいた。

forum.omz-software.com

iOS 開発に詳しいわけではないので解説は避けるが、手短に対策を記すと以下のようになる。

まず、コード中に以下のようなカスタムビュークラスを定義する:

class MyView (ui.View):
    def __init__(self):
        pass

    def touch_began(self, touch):
        # Called when a touch begins.
        for subv in self.subviews:
            if isinstance(subv, ui.TextView):
                subvo = ObjCInstance(subv)
                if subvo.isFirstResponder():
                    subvo.resignFirstResponder()

それから、.pyui ファイルを編集する。レイアウトの外側の空白の領域をタップすると、UI 部品が無選択になる。ここで i ボタンを押すと、基底の View のインスペクタが表示される。その一番下に "Custom View Class" という項目があるので、"MyView" を指定する。これにより、基底の View にはデフォルトの View ではなく新たに定義した MyView が使われるようになる。

以上の対策で、GIF 画像のように最後にソフトウェアキーボードを非表示にすることができる。

終わりに

前回までの記事とは異なり色々と端折ったが、想定以上に長い記事になってしまった。

趣味で Flask を練習していたこともあり、Web プログラミングに抵抗がなくなってきたところで、ショートカットアプリ周りのアクシデントに見舞われたので、タイミング的にはちょうどよかったとも言える。今回のスクリプト作成を通して、改めて Pythonista 3 のもつ柔軟性を感じた。願わくば開発継続されてほしい。

Pythonista の更新が滞っていることは冒頭で触れた。フォーラムでは作者に寄付をしようかという動きもあったが、「アプリの販売実績が良いので寄付は必要ないのでは」という意見もあった。しばらくの間はとりあえず、引き続き使える機能だけを使ってうまく対応する他はないようだ。

(余談:iOS 13 で Pythonista 3 からショートカットを作成すると、なぜかそのショートカットからの起動ではスクリプトが二重起動してしまうという不具合が生じてしまっている。こちらはむしろショートカットアプリを使うとうまく対応できる。引数が必要ない限りはショートカットアプリから Pythonista スクリプトを普通に起動できるからである。)