私たちの使っているPyAutoGUIは遅い、遅すぎる

私たちの使っているPyAutoGUIは遅い、遅すぎる

久々に業務でPyAutoGUIを使う機会があったので、使ってみたのですが、やはり遅い。

そう考えた私は、PyAutoGUIを書き換えることを決意しました。

以前にも高速化をしたことはありますが、それよりも明らかに速くなりました。

環境

  • Macbook Air 2017
  • macOS Big Sur (早くMontereyにしろっていうね)
  • Python3.10.1
  • PyAutoGUI 0.9.53

PyAutoGUIの中身

GitHubのPyAutoGUIのプロジェクトの中身を見るとOSX用のシステムではこのように動いてることが確認できます。

def _normalKeyEvent(key, upDown):
    assert upDown in ('up', 'down'), "upDown argument must be 'up' or 'down'"

    try:
        if pyautogui.isShiftCharacter(key):
            key_code = keyboardMapping[key.lower()]

            event = Quartz.CGEventCreateKeyboardEvent(None,
                        keyboardMapping['shift'], upDown == 'down')
            Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
            # Tiny sleep to let OS X catch up on us pressing shift
            time.sleep(pyautogui.DARWIN_CATCH_UP_TIME)

        else:
            key_code = keyboardMapping[key]

        event = Quartz.CGEventCreateKeyboardEvent(None, key_code, upDown == 'down')
        Quartz.CGEventPost(Quartz.kCGHIDEventTap, event)
        time.sleep(pyautogui.DARWIN_CATCH_UP_TIME)

    # TODO - wait, is the shift key's keyup not done?
    # TODO - get rid of this try-except.

    except KeyError:
        raise RuntimeError("Key %s not implemented." % (key))

んで、これの何が悪いかというと、大きく分けて2つあります。

  1. shiftが効くときと効かないときがある
  2. CGEventCreateKeyboardEvent()を文字が入力される都度召喚しているため、遅い

以下では、これら2つを解決してゆきます。

shiftに依存する文字の入力ルールを変える

上記のコードは時として”!”や”?”を入力してくれないときがあります。

これは、macOSが0.01秒間のスリープ(pyautogui.DARWIN_CATCH_UP_TIMEで与えられている定数のようなもの)をしたからといってshiftを手放してくれるとは限らないということや、なぜかshiftを押してくれないからです。

なんとかして”!”や”?”などの文字を入力させたいと考えた私は、色々と調べてみました。

有効そうだと思っていたApple Developerのサイトは的外れで、このサイトのコードの通りにしてもshiftが機能したりしなかったりしたため、諦めそうになりました。

しかし、やはりstack overflowに集う人々は有能でした(いつものように!)

Quartzで定義されているCGEventSetFlags()のような関数やkCGEventFlagMaskShiftのような定数を使えば、このshift入力問題を解決できるとのこと。

ただ、提示されていたコードはSwiftのものであり、他に探してみても同様にSwiftで書かれていたため、これをPython用に読み替える必要がありました。

とりあえず、テキトーにコードを組み立ててみて確認してみました。

ev = Quartz.CGEventCreateKeyboardEvent(None, 0x2c, True) # create event to type '/' slash
Quartz.CGEventSetFlags(ev, Quartz.kCGEventFlagMaskShift) # mask event with shift
Quartz.CGEventPost(Quartz.kCGHIDEventTap, ev)            # type '?'

このコードを実行してみると、見事に”?”を入力することに成功しました。

関数の呼び出し回数を減らす

上記のshift入力問題を解決するにあたり、「もしかしたら、先にQuartz.CGEventCreateKeyboardEvent()を用意しておけば、既存のPyAutoGUIよりも速くできるかもしれない」と考えてついた私は、早速、コードを書き換えました。

# ANSI keys
codes = {
        'a': 0x00,
        's': 0x01,
        'd': 0x02,
        'f': 0x03,
        # ...
        '?': 0x2c,
        '!': 0x12,
    }
# formatting create keyboard event made by Quartz
codes = {
        key: Quartz.CGEventCreateKeyboardEvent(None, val, True) for key, val in codes.items()
    }
shift_codelist = 'ASDF!?' # ...

# masking characters needed shift key.
for key in shift_codelist:
    Quartz.CGEventSetFlags(codes[key], Quartz.kCGEventFlagMaskShift)

def write(text: str):
    for ch in text:
        Quartz.CGEventPost(Quartz.kCGHIDEventTap, codes[ch])

if __name__ == '__main__':
    write('Hello, World!')

イベントを事前に辞書に追加しておくことにより、高速化することに成功しました。

さいごに

和訳調になっちゃっているのは最近、村上春樹の海辺のカフカを読んでしまっているせいです笑

つまり、私のこの文章がオリジナルなので、「元の英語の文章はどこだよ?」と探してみてもありません。

Pythonのサードパーティーライブラリーはいろんな人が作ることができるし、いろんな人が中身を見ることができるので、「なんかこの挙動おかしくね?」と思っているみなさんは、いちど中身を確認してみるのも悪くないかもしれません。

ただし、コードを書き換えてコンピューターを壊してしまわないように注意しましょう。あくまでコードの書き換えは自己責任でお願いします。