pyautoguiで「これ動かないな？」となったときの確認点まとめ

# 目的

以前、[タイピングを高速するとの目的でpyautoguiを触ってみた](https://qiita.com/mickie895/items/ff26c1ea3fb0ebf4770b)が、先日仕事としてpyautoguiを触ることになった。
その際に得た知見を、自分なりにまとめておく。

# 仕事の中身

簡単に言うと、csv形式のファイルをwebアプリ経由でサーバにアップロードするだけの仕事。
ただし、2025年の壁を超えれない企業の作ったアプリなため、下記のような制限がかかる。

+ アップロードされたcsvを1行処理するのに30秒～1分かかる
+ csvの行数が長すぎると上記処理がタイムアウトする
+ 2窓すると片方の処理がもう片方の処理にコピーされてバグる

どうやったらこんな作りになるのか一切僕には理解できなかったが、
ただの派遣の作業員の身である自分には正す義理も義務も権限もない。

できるだけ自分の知識と評価にするために自動化し、後続の人にも使いやすいコードを残しておくことにした。

# コード
## 動かなかったコード
ざっくり書くと、下記のようなコードが最初に作った駄目な例。

([セイウチ演算子](https://qiita.com/nicco_mirai/items/c1810ed2a6fc8c53c006#-%E4%BB%A3%E5%85%A5%E5%BC%8F38)って便利だね)

```python
import pyautogui as gui


def wait_and_get_pos_from_image(image_path: str, interval: float = 1, max_try: int = 10):
    """特定の画像が表示されるまで待機する

    :param image_path: 比較対象の画像
    :param interval: 確認する感覚
    :param max_try: 最大確認回数(この回数超えても見つけられなければエラー)
    :return: 見つけた場所の中央を返す
    """

    for _ in range(max_try):
        if (position := gui.locateCenterOnScreen(image_path)) is not None:
            return position

        gui.sleep(interval)

    raise TimeoutError("画像を指定時間内に見つけられませんでした。")


def set_locate(path: str):
    """Ctrl+L→パス出力
    ブラウザもファイルダイアログも同様の処理でOKなためまとめる。

    :param path: 書き込みたいパス
    :return:
    """

    gui.hotkey("ctrl", "l")
    gui.write(path)
    gui.press("enter")


if __name__ == '__main__':
    # ※ブラウザはすでに開かれているものとする。

    # 開きたいwebアプリへのパスを書き込む
    set_locate(r"https://qiita.com/")

    gui.click(wait_and_get_pos_from_image("「投稿する」ボタンの画像"))
    gui.click(wait_and_get_pos_from_image("「記事」の画像"))

    gui.click(wait_and_get_pos_from_image("画像をアップロードするボタン"))

    gui.write("画像へのファイルパス")
    gui.press("enter")

```

## 何がいけなかったか

主に下の3点。

+ [pyautoguiは英語以外のキーボード(もちろん日本語も含む)でコロンをタイプしようとすると別の文字が出る。](https://teratail.com/questions/97663)日本語だったら*に化けるようだ。
+ ファイル選択ダイアログなど、ダイアログの表示にアニメーションが付いている物がある。間髪入れずに次の動作をさせようとしてもうまく行かない。
  + Webページの読み込みもコンテンツがすべて読み込まれるまでレイアウトが変わる可能性がある。
+ [例によって](https://qiita.com/mickie895/items/ff26c1ea3fb0ebf4770b)テキストの位置を扱う際には要注意。同じPC、同じディスプレイであってもボタンを構成するピクセルが常に一定であることの保証は出来ない。

## 修正したコード

上記の注意点を元に修正を施したら期待通りの動作をするようになった。
具体的には下記のコードのような修正を行っている。

```python
import pyautogui as gui
import pyperclip as clip


def wait_and_get_pos_from_image(image_path: str, interval: float = 1, max_try: int = 10):
    """特定の画像が表示されるまで待機する

    :param image_path: 比較対象の画像
    :param interval: 確認する感覚
    :param max_try: 最大確認回数(この回数超えても見つけられなければエラー)
    :return: 見つけた場所の中央を返す
    """

    for _ in range(max_try):
        # 若干の画像のブレは許容する
        if (position := gui.locateCenterOnScreen(image_path, confidence=0.9)) is not None:
            return position

        gui.sleep(interval)

    raise TimeoutError("画像を指定時間内に見つけられませんでした。")


def set_locate(path: str):
    """Ctrl+L→パス出力

    :param path: 書き込みたいパス
    :return:
    """

    # パスの入力はクリップボード経由で行う
    gui.hotkey("ctrl", "l")
    clip.copy(path)
    gui.hotkey("ctrl", "c")
    gui.press("enter")


if __name__ == '__main__':
    # ※ブラウザはすでに開かれているものとする。

    # 開きたいwebアプリへのパスを書き込む
    set_locate(r"https://qiita.com/")

    gui.click(wait_and_get_pos_from_image("「投稿する」ボタンの画像"))
    gui.click(wait_and_get_pos_from_image("「記事」の画像"))

    gui.click(wait_and_get_pos_from_image("画像をアップロードするボタン"))

    # ダイアログの表示待ちを行う
    gui.sleep(1)
    # パスの入力はクリップボード経由で行う
    gui.hotkey("ctrl", "l")
    clip.copy("画像のファイルパス")
    gui.hotkey("ctrl", "c")
    gui.press("enter")

```

このコードを元にしたツールを使ってcsvファイルを流し込むことにより、膨大な待ち時間とそれを確認する不毛な作業をもう少し生産性の高い作業に置き換えることが出来た。