思索的逍遥の記。

いろいろな考え事。

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 の更新は最近は停滞しているので、最悪未対応のままということも考えられる。

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

(※ URL スキームを使ったハックで頑張って動かすこともできそうだが、今回は検証していない。)

今回のアプローチ

前回までは共有シートからショートカットアプリを挟んで 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 スクリプトを普通に起動できるからである。)

Pythonista 3 で Google 翻訳を使いやすく (3) iOS ショートカットアプリとの連携

前回から随分間が空いてしまった。前回でおおよそ主要機能は完成していた(全てのコードは前々回の記事に記載)。

sutukeisu.hatenablog.com

sutukeisu.hatenablog.com

今回は Pythonista 3 と iOS のショートカットアプリの連携、そしてアラート表示について触れていきたい。しばらく放っておいてしまったこのシリーズもこの記事で完結となる。

追記:2019/09/28

iOS 13 では、ショートカットから Pythonista 3 に引数を渡せない不具合が発生している。現在は、新規に appex モジュールを使った Pythonista 3 & Google 翻訳のプログラムを書き、それを利用している。これについて後日、別の記事を投稿したいと思う。

追記:2019/10/13

記事を投稿しました。

sutukeisu.hatenablog.com

なぜ "Run Pythonista Script" を使わないのか

Pythonista の使い方をご存知の方は、iOS の共有シートから "Run Pythonista Script" のボタンを介して起動すれば良いのではと思われるかもしれない。

f:id:sutukeisu:20190907210654j:plain:w250

実際、最初はそうしたかった。アプリ内の "URL to QR Code" 等の Example を参考にすれば、Web ブラウザからの URL の取得も容易なことがお分かりいただけると思う。

しかし、問題が1つあった。それは、共有シートから Pythonista を起動するとき、通常のアプリ起動よりもメモリの割り当てが少ないことである。WebView を同時に2つ保持している拙作のスクリプトは、このメモリ制限を超えてしまうらしく、起動してもすぐに落ちてしまった。

そこで、App Extension としてではなく Pythonista を直接起動するという方法を取らなければならない。このときこれを簡単に実現する手段として、Apple 公式のショートカットアプリを利用する方法を今回は採用した。

ショートカット

ショートカット

  • Apple
  • 仕事効率化
  • 無料

ショートカットを作成する

まずはショートカットアプリを開き、ライブラリ画面の「ショートカットを作成」を選択する。ショートカットの編集画面が開くので、検索欄に「テキスト」と打ち込むと、テキストの処理に関連するアクションがたくさんヒットする。この中で、単に「テキスト」と書いてあるアクションを選択する。

f:id:sutukeisu:20190907214107j:plain:w250

このアクションには「テキストを入力...」という欄がある。ここには「ショートカットの入力」を入れる(キーボードの予測変換領域に表示されている)。これにより、このショートカットの呼び出し元から入力を取ることができる。

次に、検索欄に「Pythonista」と打ち込む。Pythonista 関係のアクションは「スクリプトを編集」と「スクリプトを実行」の2つある。今回は後者を使う。

ここで、説明文を読む(なぜか親切にも日本語。なぜアプリ自体とドキュメントは英語だけなのか……)。すると、どうやらスクリプトに変数を渡すことができ、スクリプト側ではそれをsys.argvで受け取れるという。とても馴染みのあるアクセスの仕方ができる。

f:id:sutukeisu:20190907214307p:plain:w250 f:id:sutukeisu:20190907214353j:plain:w250

スクリプトを実行」アクションには「スクリプト」の欄がある。ここには、スクリプトのパスを、Pythonista のルートディレクトリからの相対パスで入力すれば良い。拡張子は必要ない(筆者の場合は "MyTools/TranslateSet/translate_set" と入力している)。

使うアクションはこの2つ全てである。あとは右上の設定アイコン(スイッチのオンオフが交互になっているマーク)を押して、好きな名前とアイコンを決める。そして今回の場合、「共有シートに表示」をオンにする。すると「受け入れの種類」という項目が新しく生えるので、これを開く。デフォルトではどんな種類の入力も扱えるようになっているが、今回は URL のみを扱うだけなので、チェックは URL のみに入れる。

これで、ショートカットアプリ側での設定は終了である。ショートカットの内容が下の画像のようになっていれば良い(URL のみを受け入れる設定にしたので、「このショートカットはURLを受け入れます」と表示されている)。

f:id:sutukeisu:20190907220717p:plain:w250

あとはスクリプト側で、受け取った URL を使う。

アラートの表示

前回の最後のスクリプトに手を加える。

Pytonista の console モジュールに alert 関数がある。以下の画像のように選択肢を表示できる。

f:id:sutukeisu:20190907221738p:plain:w250

公式ドキュメントによると、引数は以下のようになっている。

console.alert(title[, message, button1, button2, button3, hide_cancel_button=False])

なので、タイトル、説明文、最大3つまでのボタンのテキストを引数として渡せば良い。戻り値は、button1が選択されれば1button2が選択されれば2button3が選択されれば3である。Cancelボタンを押したときは戻り値が返らず、KeyboardInterrupt となってスクリプトが終了する。

今回のコード

sys.argvは通常通り、第1要素はスクリプトのファイル名で、第2要素以降が引数である。Pythonista で直接実行したか、ショートカット経由で起動して引数に URL を持っているかで条件分岐している。そして、受け取った URL のサイトを表示するか、あらかじめようしてある選択肢(Google のトップページか、Pythonista のドキュメント)を表示するかで、ui.WebView.load_url()に渡す URL を変更している。自分で使うことだけを念頭に書いたので、かなり付け焼き刃なコードになってしまっているのは申し訳ない。

import ui
# 以下の2つの import を加える
import console
import sys

# ...
# 中略
# ...

webview1 = v['webview1']
webview1.delegate = MyWebViewDelegate()

# ここから前回のコードとの差し替え
# 元のコードは以下の通り:
# webview1.load_url('http://omz-software.com/pythonista/docs/')
# textfield.text = 'http://omz-software.com/pythonista/docs/'
text = ''
if len(sys.argv) > 1:
    text = sys.argv[1]
if text.startswith('http://') or text.startswith('https://'):
    result = console.alert('URLを開く', '取得済みのURLがあります。開きますか?', 'はい', 'Googleを開く', 'Pythonista Docs を開く')
    if result == 2:
        webview1.load_url('https://www.google.co.jp/')
        textfield.text = 'https://www.google.co.jp/'
    elif result == 3:
        webview1.load_url('http://omz-software.com/pythonista/docs/')
        textfield.text = 'http://omz-software.com/pythonista/docs/'
    else:
        webview1.load_url(text)
        textfield.text = text
else:
    result = console.alert('URLを選択', 'どちらを開きますか?', 'Googleを開く', 'Pythonista Docs を開く')
    if result == 1:
        webview1.load_url('https://www.google.co.jp/')
        textfield.text = 'https://www.google.co.jp/'
    else:
        webview1.load_url('http://omz-software.com/pythonista/docs/')
        textfield.text = 'http://omz-software.com/pythonista/docs/'
# 差し替えはここまで

webview2 = v['webview2']
webview2.load_url('https://translate.google.co.jp/?hl=ja&tab=wT')
webview2.alpha = 0.0

webview1.bring_to_front()

v.present('sheet')

これで全ての作業は終了である。第1回の記事に掲載した GIF 画像のような流れがこれで実現する。

終わりに

本当はもっと早くこの記事のシリーズを終わらせる予定だったが、体調がままならなかったりして遅れに遅れてしまった。終わらせることができてよかった。

今でも Pythonista を生活に役立てる試みを続けている。暗記カードもどきのようなものを作って英単語の暗記に使ったり、numpy や matplotlib がデフォルトで入っていることをいいことに、統計の計算に使ったりしている。いつでもどこでも気軽に使用できるスマートフォンという環境でプログラミングを行えるということは素晴らしい。Pythonista は流石に重量級の用途には向いていないが、ちょっとした短いスクリプトでも十分役に立つと感じられる。

これからもちょっとずつ、楽しく役に立つコードを考えていきたいと思う。

Pythonista 3 で Google 翻訳を使いやすく (2) ロジック部分

前回の記事では、UI の準備まで行った。

sutukeisu.hatenablog.com

今回はアプリケーションのロジック部分の作成と、UI との結合について解説する。なお、GitHub Gist にアップロードした全コードは前回示したので、今回は省略する。

.pyui ファイルのロード・UI の表示

UI ファイルの名前が translate_set.pyui で、かつ Python スクリプトと同じディレクトリにある場合、最低限次のように記述すれば .pyui ファイルの読み込み・UI の表示が行われる(なお、最後の行の ui.View.present 関数の引数 'sheet'iPad の場合に意味のあるものだが、iPhone 版でこのように記述しても実行自体はうまく行く)。

import ui

v = ui.load_view('translate_set')
v.present('sheet')

ここまで問題なく実行できることを確認した上で、次のステップに進む。

ウィジェットへのアクセス方法

今表示される画面では、WebView が真っ白なので、何か Web サイトを表示させてみよう。

先ほどの ui.load_view 関数で取得した ui.View オブジェクトを通して、.pyui ファイルにある全てのウィジェットにアクセスできる。方法はかなり簡単で、先ほどのスクリプトであれば v['widget_name'] と辞書のようにウィジェット名をキーにしてウィジェットオブジェクトを参照できる。 v['webview1'] として webview1 を参照し、変数 webview1 でアクセスできるようにする。Pythonista の WebView のドキュメント を読むと、load_url メソッドで指定の URL 先を表示できることがわかるので、それを使う。さらに、今手前にある WebView は webview2 の方なので、webview1 を手前に出すために bring_to_front メソッドを呼ぶ。

import ui

v = ui.load_view('translate_set')
webview1 = v['webview1']
webview1.load_url('http://omz-software.com/pythonista/docs/')
webview1.bring_to_front()
v.present()

これで Pythonista のドキュメントが表示されるようになった。

ウィジェットに関数をひもづける

ここまでの状態でも、リンクを踏めばそのリンク先を表示できる。

ここからさらにブラウザ的な使い勝手にするためには、進むボタンと戻るボタンにそれぞれ ui.WebViewgo_back メソッド、go_forward メソッドをひもづけたい。 まずは、ボタンから呼び出すための関数を次のように書く。

def page_back(sender):
    webview = sender.superview['webview1']
    webview.go_back()

def page_forward(sender):
    webview = sender.superview['webview1']
    webview.go_forward()

ここで、それぞれの関数の引数 sender には、ボタン操作によって関数が呼ばれた時に、呼び出し元のボタンへの参照が入る。sender を通してアクセスしたいのはボタンではなく WebView なので、一旦ボタンが所属している上の階層の View を参照するために superview 属性を呼び出し、先ほどまでと同じように辞書と同じ方式で webview1 をキーとして webview1 を取得する。あとは go_back, go_forward メソッドをそれぞれ使用する。

最後に、一旦 Python スクリプトを離れて .pyui ファイルに戻る。button2(戻るボタン)と button3(進むボタン)それぞれの Action 項目に、ひもづけたい関数の名前を書けば、ボタンを押した時にそれぞれの関数が実行されるようになる。

WebView の Delegate を設定する

今回のスクリプトでは、先ほどの要領で button1(虫眼鏡ボタン)に jump_to_link 関数、segmentedcontrol1(「サイトビュー | Google翻訳」切替ボタン)に switch_webview 関数をひもづけていけば、Pythonista 内で完結したアプリケーションの動作は大体できたことになる。webview1 に任意の Webサイト、webview2 に Google 翻訳のページを表示すれば、切替ボタンで両者間を素早く行き来できる。

残ったのは、webview1 で新しい URL 先を表示した時に、連動して textfield1 にそのサイトの URL を表示させる機能である。これには、WebView オブジェクトが持っている delegate 属性に、自分で定義した delegate オブジェクトを渡す方法が使える。ただし、多少ややこしい問題もあった。

そのややこしい問題はひとまず置いておいて、まとめも兼ねて、ここまでの内容分のスクリプトを全て書き出してみた。

import ui


user_action = True


class MyWebViewDelegate(object):
    def webview_should_start_load(self, webview, url, nav_type):
        global user_action
        if nav_type == 'link_clicked' or user_action:
            textfield = webview.superview['textfield1']
            textfield.text = url
            user_action = False
        return True

    def webview_did_start_load(self, webview):
        pass

    def webview_did_finish_load(self, webview):
        pass

    def webview_did_fail_load(self, webview, error_code, error_msg):
        pass

def page_back(sender):
    global user_action
    user_action = True
    webview = sender.superview['webview1']
    webview.go_back()

def page_forward(sender):
    global user_action
    user_action = True
    webview = sender.superview['webview1']
    webview.go_forward()

def jump_to_link(sender):
    global user_action
    user_action = True
    textfield = sender.superview['textfield1']
    if textfield.text:
        webview = sender.superview['webview1']
        webview.load_url(textfield.text)

def switch_webview(sender):
    idx = sender.selected_index
    webview1 = sender.superview['webview1']
    webview2 = sender.superview['webview2']
    if idx == 0:
        webview2.alpha = 0.0
        webview1.alpha = 1.0
        webview1.bring_to_front()
    else:
        webview1.alpha = 0.0
        webview2.alpha = 1.0
        webview2.bring_to_front()

v = ui.load_view('translate_set')

textfield = v['textfield1']
textfield.clear_button_mode = 'unless_editing'

webview1 = v['webview1']
webview1.delegate = MyWebViewDelegate()
webview1.load_url('http://omz-software.com/pythonista/docs/')
textfield.text = 'http://omz-software.com/pythonista/docs/'

webview2 = v['webview2']
webview2.load_url('https://translate.google.co.jp/?hl=ja&tab=wT')
webview2.alpha = 0.0

webview1.bring_to_front()

v.present('sheet')

delegate オブジェクトのテンプレートは、先ほどの WebView のドキュメントに記載されている。webview_should_start_load メソッドはページを読み込み始める時に呼ばれるメソッドで、引数にはそれぞれ、この delegate を持っている WebView オブジェクト、新しく読み込まれる URL、ページの読み込みが始まった原因を示すナビゲーションタイプが渡される。ナビゲーションタイプについては今回初めて知ったが、おそらく本家 Apple の WebKit のドキュメントにあったこれの記載に対応していると思われる。ただしこの nav_type に渡される文字列は、その記載と若干異なるものが渡されるので、print デバッグであらかじめ渡される文字列を確認してから使用した。

nav_type'link_clicked' のときと、自前のグローバル変数 user_action が True のときに限り、textfield1 に URL を反映させることにした。user_action 変数は、ボタン操作の時に呼び出される関数内で True にしておき、直後のページ読み込みで URL を textfield1 に反映させるためのものである。それが終わったらすかさず False にしている。

それ以外のケースではそのページのアドレス以外のURL(SNSボタンに関連するものなど)が読み込まれていることが多く、反映された URL が意味をなさないため、これらのケースは除外することにした。この nav_type まわりの処理をどうするかが一番ややこしかった。もっとうまい方法もあるのかもしれない。

ここまでで、基本的な機能が概ね完成した。スクリプト起動時のコンソールアラート表示や、iOS のショートカットアプリを経由する方法などがまだ残っているが、例によってこれらは次回説明することにする。

Pythonista 3 で Google 翻訳を使いやすく (1) UI を設定する

モチベーション

Pythonista 3 は高機能な iOS 向け Python 開発環境。

Pythonista 3

Pythonista 3

  • omz:software
  • 仕事効率化
  • ¥1,220

しかし、ダウンロードしたはいいものの、使い方や使い道がいまいちピンと来ていなかった。

そこで、他の方がこれまでどんなものを作ってきたのか調べてみた。

なるほど、便利そう。

プログラミングに関する情報をググるとき、少なからず海外サイトを巡ることになるので、アプリの切り替えの手間が省けるのはとても助かる(ただ、複数アプリを画面に並べられる iPad では、普通にブラウザと Google 翻訳アプリを並べるだろう)。

まずは上の記事を参考に、ほぼ同じものを作ってみたが、次の点で欲が出始めた。

  • 画面を上下分割すると、ソフトウェアキーボードが出た時に下半分が完全に見えなくなる(コピペのために上下を行ったり来たりしたい時に不便)
  • リンクの貼り付け・移動にも対応したい
  • どうせなら、ブラウザの共有メニューから Python スクリプトを起動したい

これらを踏まえて、自分なりにプログラムに改善を加えてみた。コード全体はこちら(スクロールで全体が見られます)。

The script for switching two WebViews on Pythonist ...

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

完成品の使用イメージはこんな感じ。

Image from Gyazo

ブラウザの共有メニューから iOS のショートカットアプリを経由して、Pythonista に URL を渡しつつ起動する。これをどう実現したのかを解説していきたい。今回は UI の構築方法に焦点を当てる。

.pyui ファイルに UI 設定を入れる

Gist のコードでわかるように、.pyui ファイルは json 形式になっているが、Pythonista のアプリ上では専用のエディタが起動するので、項目ごとに入力して行くと良いだろう( json の key / value が編集画面の項目 / 設定値に対応している)。

全体的なレイアウトは次のような感じ。上の+ボタンで各ウィジェットを追加でき、ドラッグで大まかな位置に配置する。位置や大きさの微調整は i ボタンで表示される入力画面で数値入力することで行える。

WebView が webview2 の1つしかないように見えるが、webview1 がこの裏にあり、ボタン(SegmentedControl、ここでは「サイトビュー | Google翻訳」という表示)で最前面に出る WebView を切り替えている。次の編集画面を見れば、2つの WebView が同じ座標にあるのがわかる。

webview1

webview2

これで、ソフトウェアキーボードに画面下半分が隠れる問題は解消された。また、リンクを表示・編集するためのテキストフィールド、WebView の履歴を進めたり戻ったりするためのボタンも追加している。

これら以外にもいろいろなウィジェットが存在する。詳しくは Pythonista 公式のドキュメントを参照。

ここまで準備できたら、見栄えは完成である。このあとは、この .pyui ファイル を Python スクリプトで呼び出し、各ウィジェットに関数を紐付けていく。記事が長くなったので、その解説は次回にしようと思う。

怠惰は悪ではない

 怠惰であることは悪いこととされている。しかし忙しない現代人は、今一度、怠惰の効用を見直すべきではないだろうか。

 というのも、インターネットが普及している今、人間が晒されている情報の波は以前よりも圧倒的に高い。また、インターネット上の情報は整理されているようで、実は体系的でないものであふれている。そのような環境にあって、脳にかかる負荷は意識しているよりもかなり大きなものとなっているように思える。

 であればこそ、休みは重要である。何もしないぐうたらが今こそ重要である。現代人は、休みと称してネットサーフィンしたり動画漁りをしたりSNSで活動したりと、むしろ忙しくしている。それはそこに容易に手に入れられる快感があるからだが、容易とは言え忙しいには違いない。無意識に、自分の休息の時間を縮小してしまっているのだ。

 マインドフルネスだとか、デフォルトモードネットワークだとかを取り上げるまでもなく、暇でぼんやりするしかない時間を、現代人は意図的に作り出すべきだ。社会生活に追われている時こそ、真に自分が自由となる時間を創出するべきだ。今過ごしている環境にある楽しみは、必ずしも自分の味方とはならない。楽しみの味は甘いがゆえ、つい食べ過ぎてしまい、脳の生活習慣病となるだろう。断食とまでは言わないが、快感の裏に密かな疲労を感じたら、実はやめどきかもしれない。続きを後でできるものなら、一度取り置くのが良いだろう。

 怠惰と聞いて思い浮かぶのは「めんどくさ」という感覚である。この「めんどくさ」さを感じた時に休めば良いのではないか、とも考えたが、しかしこの「めんどくさ」は、必要な時に機能するとは限らない。人間が「めんどくさ」と思う物事の基準は、その物事にかかる工程の数ではない。工程の数と、その工程によって得られる快感の総量のバランスである。しかし、疲れは快感によって帳消しにはならない。快感は人を騙すだけである。騙されることが悪いというわけではない。騙されればこそ、人はある作業について「苦労を払ってもよい」と思えるものだ。その見返りに見合った苦労なら、喜んでしよう。しかし快感はときに必要以上に人を騙すことがあることも、覚えておきたい。その場合、必要以上に疲労するだろうから。また一方で、必要でないときに「めんどくさ」さを感じることがあるのも、お忘れなく。これらは総じて、認知の歪みによって生じる現象である。

 快感が「コスト支払い」のインターフェースであるとすれば、怠惰は「コスト未払い」のインターフェースである。怠惰を有効に活用すれば、余計な快感への余計な出費を抑えられる。契約関係により仕事を遂行したり楽しみを享受したりする義務があるのでなければ、現代人はより積極的に怠惰になった方が良いのではないか。その方が、自分が本当に楽しみたいと思った対象を、より存分に楽しめるだろう。細かいことにとらわれないで、自分の本当に大切にしたいものを見極めることが、積極的な怠惰への第一歩かもしれない。

後記

 長い間ブログを更新せずにいたが、実は精神的にかなり疲労してしまい、療養のような期間を過ごしていた。自分の理想のために、自分を無意識に縛り過ぎていたために起こったことだ。その理想も今考えればどこか幻想にとらわれていたような内容である。折を見てこのことについて振り返っていきたいと思うが、まずはもう少し脳の休養を続けたい。この記事自体、自分の反省を織り込んでいるところがある。だいぶ調子が戻ってきたとは思うので、このようにリハビリのような文章を起こしつつ、もう少し静観していきたいと思う。

Rustでライフゲームを作ってみた

モチベーション

ニコニコ動画で面白いプログラミング動画を見つけましたが、その動画で C で実装されていたライフゲームを Rust で書き直すと練習になりそうだったので、ちょっと書いてみました。ちょうど ncurses の rust 向け wrapper があったので良かったです。

免責事項: 筆者は趣味でプログラミングをしている者なので、間違いが含まれている可能性があります。ご注意ください。

元ネタの動画様

業界のネタも散りばめられていて楽しめます。自分は業界人ではありませんが、プログラミングを勉強しているとついつい目にしてしまう情報の数々……。

ソースコード

所詮初心者が書いたコードなので、Rust っぽくなかったり、至らないところがあると思います。ご容赦ください。

Cargo.toml

[package]
name = "life_game-rs"
version = "0.1.0"
authors = ["SuitCase <foo@example.bar>"]

[dependencies]
ncurses = "5.0"

main.rs

2018/09/07 更新:条件分岐をいくつか match に置き換えました

extern crate ncurses;

use ncurses::*;
use std::char::from_u32;

const W: usize = 80;
const H: usize = 20;

fn show(buf: [[i32; W]; H]) {
    for y in 0..H {
        mv(y as i32, 0);
        for x in 0..W {
            match buf[y][x] {
                0 => { addch(' ' as u32); },
                _ => { addch('*' as u32); },
            }
        }
    }
}

fn wrap(x: i32, bound: usize) -> i32 {
    let mut ret_x = x;
    while ret_x < 0 {
        ret_x += bound as i32;
    }
    while ret_x >= bound as i32 {
        ret_x -= bound as i32;
    }
    ret_x
}

fn step(buf: &mut [[[i32; W]; H]; 2], cur_buf: usize) {
    for y in 0..H {
        for x in 0..W {
            let mut neighbors = 0;
            for dy in -1..2 {
                for dx in -1..2 {
                    if dy == 0 && dx == 0 {
                        continue;
                    }
                    let ny: usize = wrap(y as i32 + dy, H) as usize;
                    let nx: usize = wrap(x as i32 + dx, W) as usize;
                    if buf[cur_buf][ny][nx] != 0 {
                        neighbors += 1;
                    }
                }
            }
            match buf[cur_buf][y][x] {
                0 => { buf[cur_buf^1][y][x] = (neighbors == 3) as i32; },
                _ => { buf[cur_buf^1][y][x] = (neighbors == 2 || neighbors == 3) as i32; },
            }
        }
    }
}

fn main() {
    let mut buf = [[[0; W]; H]; 2];
    let mut cur_buf = 0;
    initscr();
    noecho();

    let mut cursor_y: usize = 0;
    let mut cursor_x: usize = 0;

    loop {
        show(buf[cur_buf]);
        refresh();
        mv(cursor_y as i32, cursor_x as i32);

        let ch: char = from_u32(getch() as u32).expect("Invalid char");

        match ch {
            'q' => { break; }
            'h' => { cursor_x -= 1; }
            'l' => { cursor_x += 1; }
            'j' => { cursor_y += 1; }
            'k' => { cursor_y -= 1; }
            's' => { buf[cur_buf][cursor_y][cursor_x] = 1; }
            'c' => { buf[cur_buf][cursor_y][cursor_x] = 0; }
            'n' => { step(&mut buf, cur_buf); cur_buf ^= 1; }
             _  => { continue; }
        }
        cursor_x = wrap(cursor_x as i32, W) as usize;
        cursor_y = wrap(cursor_y as i32, H) as usize;
    }
    endwin();
}

感想

前に C++ を勉強して挫折しました。Python でプログラミングに慣れた後に、またコンパイラ型言語を勉強しようと思って、一方で「また C++ はモチベが上がらない。何か新しい言語で代替物は……」という気持ちもあって、登場時にちょっと気になってた Rust を勉強することにしました。

Rust も書いてみると気をつけることは多くて、結構躓きます。このプログラムでは以下の点で立ち止まってちょっと考えました。

  • C言語ではif文の条件に int 型を使って良いが、Rustでは bool 型にしなければならない。
  • 配列のインデックスは usize 型にしようね、とコンパイラに言われたので、扱う数値型が i32 になったり usize になったりした。
  • ncurses の getch 関数の戻り値が u32 だとコンパイラに教えられた( ncurses-rs のドキュメントがない……)。
  • 今回のプログラム中の step 関数は、元ネタ様では引数に inbuf, outbuf( buf 配列内の2つの配列)を指定していたが、Rust でそれをやると、inbuf と outbuf を表現するのに cur_buf(元ネタ様では current_buf )を2回使うことになり、Rust コンパイラ「2回借用してるんだけど」と怒られたので、buf を丸ごとと、cur_buf を関数に直に渡して、関数内で2つの buf を表現することで回避した。C で悩まなくてもいいところで悩むポイントの1つだなと実感した。

しかし、今回 Rust プログラムを書いてみて、ちゃんと動いて楽しかったのも事実です( match がとても気持ちいい)。もっと練習を積み重ねて、作りたいツールを作れるようになったらなあ……と感じた次第です。

意欲の湧かないタスクに対して

繰り返す過ちへ、如何にアプローチするか?

事あるごとに反省を繰り返している。同じ失敗を何度も重ね、その度に工夫しようと思うのに、結局実行できないか、考えるのを諦めてしまう。しかし、失敗の原因はいつも同じもののように思うので、それらにフォーカスすると、何か見えてくるかもしれない。

この記事の時点では、まだ問題は解決していない。とりあえず、現時点での分析を覚書程度にここに記しておき、時間を置いて再度この投稿への反省を記事にしてみたいと思う。

過ちの具体的な姿を見極めてみる

大雑把に言えば、時間管理に関係する問題である。やるべきことは決まっているのに、それを遂行する気力が起きない。そして気がつくと、やるべきことが終わらないまま、期限間近となってしまう。そしてやるべきことが終わっていないか、中途半端な出来となってしまうために、期限が来ると失敗が確定してしまう。失敗する案件は多くの場合、自分で自ら課した課題ではなく、他人から要請された目標に対してのタスクである。

失敗する要因として考えられるもの

失敗へと至る道筋の中で、悪さを働いている(様に見える)原因を、思いつく限り挙げてみる。

  1. そもそも自分が能動的にやろうとしているタスクではないため、取り掛かるためのモチベーションがなく、上げるのも難しい。
  2. 他にやりたいことがあり、そちらのモチベーションが上回っているため、作業が脱線してしまう。
  3. 疲労や眠気など、生物学的な限界が立ちはだかっている。

以下では、今挙げた問題について細かく考察していきたい。

やりたくない、興味のないタスクに向き合うために

ある行動を起こしたいという欲求は、生物学的には学習によって増強されたり、抑制されたりする。この現象に関わるシステムは複雑であろうが、今はその様に単純に考えよう。 上記の考え方に従うと、あるタスクについて意欲が湧かないということは、その時点までにそのタスクに関係する事象を経験した時に(あるいは現在進行形で)、肯定的な情動的評価がなされていない、ということが考えられる。そのタスクに対して、肯定的でない情動を抱くに至る要因はなんだろうか? 以下に考えられるものを挙げてみよう。

  • その事象に対して、嫌悪感を覚えたことがある。
  • その事象に対して、自分が興味を持つものに比較して、より弱い印象しか持たなかった。
  • その事象に全く興味がないわけではないが、向き合うための物理的・精神的余裕がない。

これらについて考えていく。まず、その事象に対して嫌悪感を覚えているなら、一人で事態を打開することは難しい。この場合、他人にその嫌悪感の存在を打ち明け、遂行を手伝ってもらうしかない。また、おそらくその事象にあまり印象を持っていない時も、同じことが言えるだろう。一人でなんとかできる可能性のあるものは、3つ目の、その事象に向き合うだけの余裕がないパターンだけかもしれない。

余裕がない、という状況は、シンプルに言えば「他の事象によって邪魔されている」ということに尽きる。他の事象に、なぜ邪魔されうるのだろう。それは、その時自分が、その他の事象にコントロールを奪われているからである。例えば同時並行で、何かに対する執着や悩みを感じており、その感情を処理する作業に忙殺されれば、必然的に他のタスクには向き合えなくなる。また、期限まで余裕がない場合、時間がないことへの焦りばかりを感じて、無計画に、無節操にタスクに取り組んでしまえば、上手くいかずに投げ出すことだってあるだろう。そうした、気づきづらい自分の膠着した感情を明らかにして、解決してからタスクに取り掛からなければ、上手くいかない状態はいつまでも解決しない。

厄介なのは、自分がより興味を持つものに対する執着、という原因がありうるということである。自己肯定感・自己効力感が乏しいとき、それを満たすために、自分が得意なものに打ち込むだろう。得意なものの方が、少ない努力でより良い結果を残せるので、取り組みやすいのである。そして、その得意なことに対しても自分が能力上の不満を抱いているなら、よりその対象へ没頭してしまうだろう。他のことがダメならなおさらである。要するに、自分の最も興味関心のある対象、自分のホームグラウンドが、自分自身の存在意義を存続させるための最後の砦なのである。この様になってしまった場合、自分の精神状態の良悪の変化は、その自分の興味の対象にコントロールを掌握されてしまう。自分が心の拠り所に思っている対象への思いが強ければ強いほど、この問題は顕著になってしまい、解決は難しくなる。

解決策として考えられるのはまず、決してその執着を無碍にせず、自分が何に対して執着を抱いているのかをできるだけ詳細に理解するところから始めることである。そして、その執着の手綱を時折、一時的に緩めることができるように、意図的に訓練していく。執着を緩めることができれば、その時、何か心が軽くなった様に感じる反面、執着し続けたものへの愛着を一時的に封印することへの寂しさを感じることだろう。完全に執着を捨ててしまう、という手もある。しかし、その執着を捨ててはならないものだと感じる場合、無理に捨ててはならない。捨てるなら、ある程度自分で悩んで納得してからである。緩めた手綱をまた打ち振るったり、また緩めたりすることでモードチェンジできる様になれば理想的である。ただしそれを覚えすぎても、ダブルスタンダードになった時にまた別の精神的問題が噴出しかねないので、その執着の対象と、現在心を向けるべきタスクの両立に整合性があるかどうかも、考慮する必要があるかもしれない。

ただし、上記の様に考えすぎても疲れる一方なので、考えるならしっかり休養をとって、頭を空っぽにしてからである。そして、タスクが多ければ多いほど考えることも増えるので、後回しにできるタスクを、取り組むべき適切なタイミングまで考えてから、一度放置しておくことを覚えたい。そうすることで、適切な休養も取れる様になるだろう。一方で、執着している対象をあまりにも放置しすぎると、執着の度合いが返って増して、問題がさらに悪化するかもしれない。それに対しては、他のタスクでの成功をより確実にする工夫をすることと、執着の対象に向き合う時間を確保することを同時並行で行っていくことで、自己効力感を温存し、問題を和らげることができるかもしれない。

本記事のまとめ

結局、自分が何に対してどれくらい工夫し頑張るのか、エネルギーの振り分けを考えるのが大事だと、今のところ考えている。自分の中の一定時間あたりのエネルギーは有限である。しかし長い目でみれば、生活していく中でエネルギーは外部から供給され続ける。だから考えなしにエネルギーを浪費し続けるのではなく、長期に渡って安定的にエネルギーを燃やして行くことを考えれば、成せることも増えていくことだろう。

冒頭に記したように、自分の場合まだ問題は解決していない。だからこの考察は終わりではない。今後、この考察をより改善していければと思う。