0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PyAutoGUI Generatorを作ってみた

0
Posted at

はじめに

PythonでRPAを作成する場合、PyAutoGUI は非常に便利なライブラリです。マウス操作やキーボード操作をPythonから簡単に実行できます。

一方で、RPAを作成するときには、

pyautogui.click(500, 300)
pyautogui.write("Hello")
pyautogui.press("enter")

のようなコードを、操作のたびに手作業で記述する必要があります。

そこで今回は、人間の操作をJSON形式で記録し、そのJSONからPyAutoGUIのRPAコードを自動生成するという仕組みを作りました。この記事では、その中のコード生成部分について紹介します。


1. 作成したもの

今回作成したのは pyauto_generator.py です。全体の流れは次のようになっています。

actions.json
    ↓
pyauto_generator.py
    ↓
generated_rpa.py

例えば、入力となる actions.json に、

{
    "actions": [
        {
            "type": "click",
            "position": {
                "x": 500,
                "y": 300
            },
            "button": "left"
        },
        {
            "type": "text",
            "text": "Hello World"
        },
        {
            "type": "key",
            "key": "enter"
        }
    ]
}

と記録されていた場合、

import pyautogui
import time

# =====================================
# RPA操作
# =====================================
pyautogui.click(500, 300, button='left')
pyautogui.write("Hello World")
pyautogui.press('enter')

というPythonコードを自動生成します。


2. なぜJSONを挟むのか

今回の仕組みでは、操作内容を直接PyAutoGUIのコードとして保存していません。

人間の操作
   ↓
操作イベント
   ↓
actions.json
   ↓
コード生成
   ↓
PyAutoGUI

という構成にしています。ここが今回の仕組みで重要な部分です。

例えば、操作を直接 pyautogui.click(500, 300) として記録してしまうと、PyAutoGUIに依存したデータになってしまいます。一方、

{
    "type": "click",
    "position": {
        "x": 500,
        "y": 300
    },
    "button": "left"
}

のようにしておけば、

         actions.json
               ↓
         ┌─────┴─────┐
         ↓           ↓
     PyAutoGUI   別のRPAライブラリ

というように、将来的に別のコード生成器を作ることもできます。つまり、操作データと実行ライブラリを分離することを狙っています。


3. pyauto_generator.py の全体像

現在のコードは以下のようになっています。

import json
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent

INPUT_FILE = BASE_DIR / "actions.json"
OUTPUT_FILE = BASE_DIR / "generated_rpa.py"


def quote(value):
    return repr(value)

key_map = {
    "left windows": "winleft",
    "right windows": "winright",
    "windows": "win",
    "win": "win",
}

def generate_action(action):
    action_type = action["type"]

    if action_type == "click":
        x = action["position"]["x"]
        y = action["position"]["y"]
        button = action["button"]
        return f"pyautogui.click({x}, {y}, button={quote(button)})"

    elif action_type == "double_click":
        x = action["position"]["x"]
        y = action["position"]["y"]
        button = action["button"]
        return (
            f"pyautogui.doubleClick("
            f"{x}, {y}, button={quote(button)})"
        )

    elif action_type == "scroll":
        x = action["position"]["x"]
        y = action["position"]["y"]
        delta = int(action["delta"])
        return (
            f"pyautogui.moveTo({x}, {y})\n"
            f"pyautogui.scroll({delta})"
        )

    elif action_type == "drag":
        button = action["button"]
        start_x = action["start"]["x"]
        start_y = action["start"]["y"]
        end_x = action["end"]["x"]
        end_y = action["end"]["y"]
        duration = action["duration"]
        return (
            f"pyautogui.moveTo({start_x}, {start_y})\n"
            f"pyautogui.mouseDown(button={quote(button)})\n"
            f"pyautogui.moveTo({end_x}, {end_y}, duration={duration})\n"
            f"pyautogui.mouseUp(button={quote(button)})"
        )

    elif action_type == "text":
        text = action["text"]
        return (
            f'pyautogui.write("{text}")'
        )

    elif action_type == "key":
        key = action["key"]
        return(
            f"pyautogui.press('{key}')"
        )

    elif action_type == "hotkey":
        converted_keys = [
            key_map.get(key.strip().lower(), key.strip().lower())
            for key in action["keys"]
        ]

        keys = ", ".join(repr(key) for key in converted_keys)

        return (
            f"pyautogui.hotkey({keys})"
        )
    elif action_type == "ime":
        return(
            "pyautogui.press('kanji')"
        )

        

    else:
        raise ValueError(f"未対応のActionです: {action_type}")


def generate_rpa_code(data):
    actions = data["actions"]

    lines = [
        "import pyautogui",
        "import time",
        "",
        "",
        "# =====================================",
        "# RPA操作",
        "# =====================================",
        "",
    ]

    for action in actions:
        delay = action.get("delay", 0)

        if delay > 0:
            lines.append(f"time.sleep({delay})")
            lines.append("")

        lines.extend(generate_action(action).splitlines())
        lines.append("")

    return "\n".join(lines)


def main():
    input_path = Path(INPUT_FILE)
    output_path = Path(OUTPUT_FILE)

    if not input_path.exists():
        print(f"エラー: {INPUT_FILE} が見つかりません。")
        return

    try:
        with input_path.open("r", encoding="utf-8") as f:
            data = json.load(f)
    except json.JSONDecodeError as e:
        print("エラー: actions.jsonのJSON形式が正しくありません。")
        print(e)
        return

    if "actions" not in data:
        print('エラー: actions.jsonに"actions"がありません。')
        return

    try:
        code = generate_rpa_code(data)
    except KeyError as e:
        print(f"エラー: Actionに必要な項目がありません: {e}")
        return
    except ValueError as e:
        print(f"エラー: {e}")
        return

    output_path.write_text(code, encoding="utf-8")

    print("========================================")
    print(" PyAutoGUI Generator")
    print("========================================")
    print()
    print(f"入力 : {input_path}")
    print(f"出力 : {output_path}")
    print()
    print("PyAutoGUIコードを生成しました。")
    print("========================================")


if __name__ == "__main__":
    main()

4. Actionの種類

現在は以下のActionに対応しています。

Action 内容
click マウスクリック
double_click ダブルクリック
scroll スクロール
drag ドラッグ&ドロップ
text 文字入力
key 単一キー
hotkey 複数キーの同時押下
ime IME切り替え

5. 各Actionの変換詳細

5.1. click

例えば、

{
    "type": "click",
    "position": {
        "x": 500,
        "y": 300
    },
    "button": "left"
}

を pyautogui.click(500, 300, button='left') に変換します。

if action_type == "click":
    x = action["position"]["x"]
    y = action["position"]["y"]
    button = action["button"]
    return f"pyautogui.click({x}, {y}, button={quote(button)})"

5.2. double_click

ダブルクリックにも対応しています。

{
    "type": "double_click",
    "position": {
        "x": 500,
        "y": 300
    },
    "button": "left"
}

↓
pyautogui.doubleClick(500, 300, button='left')

5.3. scroll

スクロールの場合は、マウスカーソルを指定位置へ移動してからスクロールします。
ただしdeltaのスクロール移動量は、ソフトによって変わるため、そのままでは期待する移動をしません。そのため手動で移動量を変える必要があります。

{
    "type": "scroll",
    "position": {
        "x": 500,
        "y": 300
    },
    "delta": -1
}

↓

pyautogui.moveTo(500, 300)
pyautogui.scroll(-1)

5.4. drag

ドラッグ操作は、

  1. 開始位置へ移動
  2. マウスボタンを押す
  3. 終了位置へ移動
  4. マウスボタンを離す

という4段階に変換しています。

{
    "type": "drag",
    "button": "left",
    "start": {
        "x": 100,
        "y": 200
    },
    "end": {
        "x": 500,
        "y": 400
    },
    "duration": 1.0
}

↓

pyautogui.moveTo(100, 200)
pyautogui.mouseDown(button='left')
pyautogui.moveTo(500, 400, duration=1.0)
pyautogui.mouseUp(button='left')

単純な dragTo() ではなく、mouseDown() → moveTo() → mouseUp() としているのがポイントです。

5.5. text

文字入力は pyautogui.write() に変換します。

{
    "type": "text",
    "text": "Hello World"
}

↓
pyautogui.write("Hello World")

キーボード操作を1キーずつ保存するのではなく、連続した文字入力を text として扱えるようにしています。

5.6. key

EnterやBackspaceなどの単一キーは key として扱います。

{
    "type": "key",
    "key": "enter"
}

↓
pyautogui.press('enter')

5.7. hotkey と Windowsキーの対応

複数キーの同時押下には hotkey を使用します。

{
    "type": "hotkey",
    "keys": [
        "ctrl",
        "c"
    ]
}

↓
pyautogui.hotkey('ctrl', 'c')

また、Windowsキーについては、操作記録側で使われる名称とPyAutoGUIで使用する名称が異なる場合があります。そこで、以下の変換表を用意しています。

key_map = {
    "left windows": "winleft",
    "right windows": "winright",
    "windows": "win",
    "win": "win",
}

例えば、

{
    "type": "hotkey",
    "keys": [
        "left windows",
        "r"
    ]
}

の場合、pyautogui.hotkey('winleft', 'r') を生成します。

操作記録側のキー名
        ↓
     key_map
        ↓
PyAutoGUIで使用するキー名

5.8. IME

IME切り替えについては、専用のActionとして用意しています。

{
    "type": "ime"
}

↓
pyautogui.press('kanji')(日本語Windows環境でIMEの切り替え操作を記録するためのもの)

5.9. 操作間の delay

各Actionには "delay": 1.0 のような待ち時間を設定できます。

{
    "type": "click",
    "position": {
        "x": 500,
        "y": 300
    },
    "button": "left",
    "delay": 2.0
}

↓

time.sleep(2.0)
pyautogui.click(500, 300, button='left')

これにより「クリック → 2秒待つ → 次の操作」というRPAが生成できます。


6. コアロジックの解説

generate_action()

プログラムの中心となるのがこの関数です。Actionの type を確認し、それぞれ対応するPyAutoGUIコードへ変換します。

action["type"]
      │
      ├── click
      ├── double_click
      ├── scroll
      ├── drag
      ├── text
      ├── key
      ├── hotkey
      └── ime
              ↓
      PyAutoGUIコード

対応していないActionが渡された場合は、raise ValueError(f"未対応のActionです: {action_type}") としてエラーにします。

generate_rpa_code()

複数のActionをまとめてPythonコードに変換します。ファイルのヘッダー部分を作成した後、各アクションを順次処理して結合します。

エラー処理

  • JSONファイルが存在しない場合のエラーチェック
  • JSONの形式不正 (json.JSONDecodeError) のキャッチ
  • 必要な項目がない場合 (KeyError) のキャッチ

7. 実行方法

  1. actions.json と pyauto_generator.py を同じフォルダに配置します。
project/
├─ actions.json
└─ pyauto_generator.py
  1. スクリプトを実行します。
python pyauto_generator.py
  1. 成功すると以下のように表示され、generated_rpa.py が生成されます。
========================================
 PyAutoGUI Generator
========================================

入力 : actions.json
Output : generated_rpa.py

PyAutoGUIコードを生成しました。
========================================

プロジェクト構成:

project/
├─ actions.json
├─ pyauto_generator.py
└─ generated_rpa.py

8. 全体の構成イメージ

今回のプログラムは単なるコード生成器ですが、最終的には次のような構成を想定しています。

                 ┌─────────────────┐
                 │   マウス操作     │
                 │   キーボード操作 │
                 └────────┬────────┘
                          ↓
                 ┌─────────────────┐
                 │  操作レコーダー  │
                 └────────┬────────┘
                          ↓
                 ┌─────────────────┐
                 │   actions.json  │
                 └────────┬────────┘
                          ↓
              ┌───────────┴───────────┐
              ↓                       ↓
     ┌───────────────────┐     ┌─────────────────┐
     │PyAutoGUI Generator│     │  別Generator    │
     └────────┬──────────┘     └────────┬────────┘
              ↓                       ↓
     generated_rpa.py             別のRPAコード

この構成にすることで、操作を記録する部分と、実際に操作を実行するライブラリを綺麗に分離できます。


9. 今後の課題・展望

現時点では基本的な操作をPyAutoGUIコードへ変換できるようになりましたが、今後は以下のような機能拡張を予定しています。

  • 画像認識Actionへの対応
  • ウィンドウ待機処理
  • pyautogui 以外のRPAコード生成 (pywinauto, mouse など)
  • JSONのスキーマ検証

操作データそのものを特定のライブラリに依存しない形で設計しておけば、将来的に多様なRPAバックエンドへ展開が可能になります。


まとめ

今回は、操作内容をJSON形式で保存し、そのJSONからPyAutoGUIのコードを自動生成する pyauto_generator.py を作成しました。

  • 操作データを共通フォーマット(JSON)として保存する
  • JSONと実行ライブラリを分離する
  • Actionの種類ごとにコードを綺麗に自動生成する
  • マウス・キーボード操作(hotkeyやIME切り替え含む)に対応する
  • delay を使って操作間の待ち時間を再現する

単に「マウス操作をPythonコードに変換する」だけでなく、操作を共通フォーマットで記録し、そこから任意のライブラリ向けコードを生成するというアプローチを取ることで、柔軟性の高いRPA基盤に発展させられそうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?