Python RPA大全(pyautoguiと愉快な仲間たち)

# はじめに
Pyautoguiがあれば大概のRPAが実装できる。周辺モジュールも含めてRPAを実装するための大全をメモします。
この記事の情報でほとんどすべてのGUI操作が可能になるのではないでしょうか？

メインで使用する`pyautogui`を`pag`としてimportしておく。
```python
import pyautogui as pag
```

# キーボード操作
ユーザーIDやパスワードを有力する際に使用したり、ショートカットキーを送信するために使う。

### キーを順番に押す
GUIのボタン操作などでtab連打からのenterを押したいときに便利。
`presses`で複数回繰り返すことができる。
下の例ではTab→Enter→Tab→Enter→Tab→Enterと繰り返す。
```python
pag.press(["tab", "enter"], presses=3)
```

### キーを同時押しする
ショートカットキーを送信するときに便利。この例ではctrlを押したままaを押し、aを離したあとにctrlを離す。
```python
pag.hotkey("ctrl", "a")
```

### テキスト入力(英語)
str型のテキストを入力できる。
```python
pag.write("text")
```

### テキスト入力(日本語)
`write()`メソッドは日本度を入力できないため、一旦クリップボードを経由させる。
`pyperclip`モジュールを用いてクリップボードへアクセスする。
```python
import pyperclip as clip
```
```python
clip.copy("日本語の文章")
pag.hotkey("ctrl", "v")
```

### キー名称の確認
入力できるキーの一覧は以下のように確認することができる。
```python
print(pag.KEYBOARD_KEYS)
```
```
['\t', '\n', '\r', ' ', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '{', '|', '}', '~', 'accept', 'add', 'alt', 'altleft', 'altright', 'apps', 'backspace', 'browserback', 'browserfavorites', 'browserforward', 'browserhome', 'browserrefresh', 'browsersearch', 'browserstop', 'capslock', 'clear', 'convert', 'ctrl', 'ctrlleft', 'ctrlright', 'decimal', 'del', 'delete', 'divide', 'down', 'end', 'enter', 'esc', 'escape', 'execute', 'f1', 'f10', 'f11', 'f12', 'f13', 'f14', 'f15', 'f16', 'f17', 'f18', 'f19', 'f2', 'f20', 'f21', 'f22', 'f23', 'f24', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9', 'final', 'fn', 'hanguel', 'hangul', 'hanja', 'help', 'home', 'insert', 'junja', 'kana', 'kanji', 'launchapp1', 'launchapp2', 'launchmail', 'launchmediaselect', 'left', 'modechange', 'multiply', 'nexttrack', 'nonconvert', 'num0', 'num1', 'num2', 'num3', 'num4', 'num5', 'num6', 'num7', 'num8', 'num9', 'numlock', 'pagedown', 'pageup', 'pause', 'pgdn', 'pgup', 'playpause', 'prevtrack', 'print', 'printscreen', 'prntscrn', 'prtsc', 'prtscr', 'return', 'right', 'scrolllock', 'select', 'separator', 'shift', 'shiftleft', 'shiftright', 'sleep', 'space', 'stop', 'subtract', 'tab', 'up', 'volumedown', 'volumemute', 'volumeup', 'win', 'winleft', 'winright', 'yen', 'command', 'option', 'optionleft', 'optionright']
```

# マウス操作
基本的には相対/絶対座標を指定して移動、クリックを行う。
画面内の座標系は以下のようになっている。(公式引用)
> ```
> 0,0       X increases -->
> +---------------------------+
> |                           | Y increases
> |                           |     |
> |   1920 x 1080 screen      |     |
> |                           |     V
> |                           |
> |                           |
> +---------------------------+ 1919, 1079
> ```

https://pyautogui.readthedocs.io/en/latest/mouse.html

### マウスを動かす
`move(x, y)`で相対座標、`moveTo(x, y)`で絶対座標を指定してマウスを動かすことができる。
座標に`None`を指定するとその方向には移動しない。
```python
pag.move(10, 10)
pag.moveTo(100, 100)
```
### クリック
`pag.click()`で左クリックを行う。
`x, y`を指定すれば`move`してからクリックする。
`clicks`を指定すれば複数回の連続クリックを実行できる。
`button`に`"primary"`を指定すればいわゆる左クリック、`"secondary"`を指定すればいわゆる右クリック、`"middle"`で中クリックを行える。
```python
pag.click(x=100, y=300, clicks=2, button="primary")
```
### ドラッグ
`move`と同様に、`drag(x, y)`で相対座標、`dragTo(x, y)`で絶対座標を指定してマウスを動かすことができる。
座標に`None`を指定するとその方向には移動しない。
```python
pag.drag(10, 10)
pag.dragTo(None, 10)
```
### 画面スクロール
`scroll`というメソッドが実装されているが、引数の定義が曖昧。
+で上方向、-で下方向に移動する。

```python
pag.scroll(-3000)
```

テキストエディタなどでは使用しづらいが、場合によっては↓を複数回押したほうが良い場合がある。
```python
pag.press("down", presses=3)
```

# ソフトやアプリケーションの起動
起動系のコマンドは2つ、それぞれメリデメがある。
### subprocess.Popen()
`subprocess.Popen`メソッドを使用すれば大概のプログラムを起動できる。
また、リスト形式で引数を渡して実行することもできる。
```python
import subprocess
```

```python
app = r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe"
url = "https://www.google.com/"

subprocess.Popen([app, url])
```

### os.startfile()
`os.startfile`メソッドを使用することで`subprocess.Popen`では開けないショートカットを開くことができる。
```python
import os
```
```python
app = r"C:\Users\Public\Desktop\shortcut.url"

os.startfile(app)
```

# 待機する
GUIに対してアクションをおこなった後、何かしら応答が返ってくるまで待ってから次の動作実行しないと予期せぬ暴走を引き起こしかねない。
ここではいくつか待機方法について紹介する。

### シンプルに時間で待つ
予め決められた秒数だけ待ってから次の動作を実行する。`time.sleep`を使用するか、`pyautogui`の`sleep`メソッドを使用する。
```python
import time
```
```python
time.sleep(30)
pag.sleep(30)
```

### 画像認識
画像を認識させ、予め準備しておいた画像と一致する部分があったら次の処理を行う。
`pics`に認識させたい画像のパスのリストを渡すと、見つかった位置とインデックスを返す。

画像の準備方法も下の方で紹介しています。

どの画像が見つかったかをインデックスで判断できるため、条件分岐の起点として使用できる。
```python
import datetime
import time
```
```python
def wait_pics(pics, dt=0.0, upto=600.0):
    abort_time = datetime.datetime.now() + datetime.timedelta(seconds=upto)
    while datetime.datetime.now() < abort_time:
        for index, pic in enumerate(pics):
            pag.failSafeCheck()
            loc = pag.locateCenterOnScreen(pic)
            if loc is not None:
                return loc, index
        time.sleep(dt)
    raise TimeoutError()
```
`datetime`モジュールを使用して一定時間過ぎた場合は`while`文を抜けて`TimeoutError`を出力する。
`pics`を一つ一つ`locateCenterOnScreen`で探す。
見つからなかった場合は`None`が返ってくるため次の画像を探す。
見つかった場合はその座標と画像のインデックスを返す。

使用例は下記の通り。
```python
# test1かtest2の画像がウィンドウに現れるまで待つ
loc, index = wait_pics(["test1.png","test2.png"])

# どちらのインデックスが見つかったか
print(index)
# 見つかった画像の位置へマウスを移動する
pag.moveTo(loc)
```

### ウィンドウタイトルで判断
指定された文字列をタイトルに含むウィンドウが出現するまで待つ。基本的な思想は画像認識待ちと同様。
`titles`に検索したいウィンドウ名の文字列リストを渡すと、見つかった位置とインデックスを返す。
```python
import datetime
import time
import pygetwindow as gw
```

```python
def wait_title(titles, dt=0.0, upto=600.0):
    abort_time = datetime.datetime.now() + datetime.timedelta(seconds=upto)
    while datetime.datetime.now() < abort_time:
        for index, title in enumerate(titles):
            pag.failSafeCheck()
            win = gw.getWindowsWithTitle(title)
            if win != []:
                return win[0], index
        time.sleep(dt)
    raise TimeoutError()
```

使用例は下記の通り。
```python
# "Outlook"か"Chrome"を含むウィンドウが出現するまで待つ
win, index = wait_title(["Outlook", "Chrome"])

# どちらのインデックスが見つかったか
print(index)
# すべてのウィンドウを最小化
pag.hotkey("win", "d")
# 見つかったウィンドウをアクティベートして最大化
win.activate()
win.maximize()
```

### マウスカーソルの状態で判断
マウスカーソルが砂時計状態ではなくなるまで待機し続ける。
適当な座標を入力するとカーソルを移動し、砂時計状態が終了したらカーソル状態を返す。
```python
import datetime
import time
import win32gui
```

```python
def wait_hourglass(x=None, y=None, dt=0.0, upto=600.0):
    abort_time = datetime.datetime.now() + datetime.timedelta(seconds=upto)
    while datetime.datetime.now() < abort_time:
        pag.failSafeCheck()
        pag.moveTo(x, y)
        cursor = win32gui.GetCursorInfo()[1]
        if cursor not in [65543, 65561]:
            return cursor
        time.sleep(dt)
    raise TimeoutError()
```

使用例は下記の通り。
```python
wait_hourglass()
```
運用上は
画像を認識→マウス移動→ポインター待ち
が安定しそう。

ちなみに私調べでは
```
通常状態:65539
テキスト入力状態:65541
砂時計とポインター:65561
砂時計:65543
```
となっていました。このインデックスの調べ方に詳しい方ぜひ教えてください。

# 画像認識用の画像準備方法
1. shift + win + sで認識したい画像をキャプチャする
2. ペイントを起動する
![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2716347/9c16524d-612f-c51c-a9d9-4eeae6e99538.png)
3. トリミングする
なるべく小さく切り取ったほうが認識速度が上がるっぽい。
本来認識に関係ない外側の部分に外乱が入って認識できないという残念なエラーを防ぐためにもきわきわで切り取っておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2716347/ccfeda6d-c8f4-8ac8-6ad0-03b6e9581178.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2716347/390474d7-3449-75c3-8c7f-7ac37f86835c.png)
4. *.png形式で保存する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2716347/80e9e439-84e9-b62b-4820-ebb56cee985e.png)