← LOG

PYTHON2026.02.15

RPAエンジニアがPython移行で「20倍速」を実現した、AI時代の開発術

#python#playwright#rpa#uipath#自動化#cursor#ai

はじめに:AI時代のRPA開発で気づいたこと

「UiPathで6時間かけて作ったRPAが、Pythonだと20分で完成した」

この事実に、私自身が一番驚きました。

私はRPA開発者として、UiPathを使った業務自動化に携わってきました。先日、現場に近い形で実際のシナリオ(WebアプリAからデータ取得 → Excel変換 → WebアプリBへ登録)を実装したところ、約6時間かかりました。

その後、このシナリオをPython + Playwrightで再実装することを試みました。まずUiPathのXAMLファイルをAIで解析し、その仕様書をもとにCursor(AIエディタ、Claude Sonnet 4.5使用)で実装を開始しました。

最初は大失敗でした。

AIとの会話がかみ合わず、なかなか動くものが作れません。試行錯誤を繰り返すうち、ふと気づきました。

「私のやり方は、従来のRPA開発にとらわれているのでは?ツールが変わったのだから、それに合わせた開発手法に切り替える必要があるかもしれない」

そこで方針を転換しました。AIに「どのような情報があれば実装しやすいか」を質問したのです。すると、WebアプリのDOM情報やouterHTMLを渡してもらえれば効率的に開発できると回答がありました。

その方針で再スタート。操作する手順に合わせて、ボタンのDOM情報、値を取得する箇所のHTML、入力ページ全体のouterHTMLをAIに渡しました。

結果、わずか20分で動作するPython版RPAが完成しました。

「20分」の正体:実装フェーズの劇的な圧縮

この「20分」という数字について、誠実に説明しておきます。これは既存UiPathシナリオの仕様が固まっていたからこそ実現した実装時間です。

しかし、それ以上に驚いたのは、UiPathでは実装しきれなかったエラーハンドリングや詳細なログ出力、Excelへのエラー詳細記録も、AIに依頼してものの2〜3分で実装できたことです。

UiPathでこれらの実装(エラーハンドリング、ユーザー向けの対話的なUI、詳細なログ出力)を追加しようとすると、おそらく2時間以上かかっていたでしょう。それが、AIに適切に指示を出すだけで数分で完成したのは、まさに感動ものでした。

つまり、AIの真価は「めんどくさいけど重要な処理」の量産にあるのです。

デバッグも従来の10倍以上のスピードでした。エラー内容をターミナルで詳細に出力するようにしていたため、その情報をAIに渡すだけで即座に修正が完了します。

仮にゼロベースで仕様策定から始めても、AIとの対話を通じて仕様を同時並行で確定させていけば、**従来の1/6のスピード感(6時間→1時間程度)**で完結できる手応えを感じています。

この経験から学んだ最大の教訓は、「AIが仕事をしやすいように、人間が適切に働きかけることが重要」ということです。

本記事では、この実装の全体像、技術的な詳細、詰まったポイント、そしてAIを活用した開発のコツをお伝えします。

背景・目的

元のRPAシナリオ

UiPathで実装していたのは、以下のような典型的な業務自動化シナリオです:

  1. WebアプリAで表データを取得
  2. データを変換して入力用Excelを作成
  3. WebアプリBで各行のデータをフォーム登録

このフローは動作していましたが、以下のような課題がありました:

Python移行の目的

同じ処理をPythonで再現することで、以下を実現したいと考えました:

成果

最終的に以下を実装し、実用レベルまで持っていくことができました:

技術スタック・全体構成

使用技術

二つのフロー

フロー内容エントリポイント
取得フローG01 設定読込 → G02 WebアプリA で表取得 → G03 入力用 Excel 出力python -m src.run_kakutoku_flow
登録フローメッセージで「インプット作成するか/既存を使うか」確認 → R01 入力 Excel 読込 → R02 WebアプリB を開き「データを登録する」→ 行ごとに R04/R05 でフォーム入力・確認・登録python -m src.run_touroku_flow

登録フローの起動時処理

登録フローは、ユーザーに選択肢を提示する対話型の仕組みです:

  1. 「インプットファイルを作成しますか?」(はい/いいえ)
  2. 「この動作でいいですか?(選択した動作が表示される)」(OK/キャンセル)→ キャンセルなら終了
  3. 「はい」を選択:取得フロー(G01→G02→G03)を実行してから、本日日付の入力ファイルで登録フローを実行
  4. 「いいえ」を選択:本日日付の入力ファイルを探す。なければファイル選択ダイアログを表示

ディレクトリ構成

UiPathToPythonPattern1/
├── 01_setteing/
│   ├── config.xlsx          # 設定(キー・値形式)
│   └── README.md
├── 02_inputFile/            # 入力Excelの保存先(configで指定)
├── docs/
│   ├── Python実装_進め方.md
│   ├── UiPath_xaml解析結果_参考資料.md
│   └── Zenn・Qiita投稿用_記録.md
├── src/
│   ├── G01_load_config.py       # 取得: 設定読込
│   ├── G02_fetch_webapp_a.py    # 取得: WebアプリA 表取得
│   ├── G03_build_input_excel.py # 取得: 入力Excel出力
│   ├── run_kakutoku_flow.py     # 取得フロー実行
│   ├── R01_load_config_and_excel.py # 登録: 設定+入力Excel読込
│   ├── R02_open_webapp_b.py     # 登録: WebアプリBを開き「データを登録する」
│   ├── R04_R05_register.py      # 登録: 1行分のフォーム入力・確認・登録
│   ├── run_touroku_flow.py      # 登録フロー実行(メッセージ→分岐→登録)
│   └── launch_edge_with_guide.py # 取得時: Edge起動+案内メッセージ
├── scripts/
│   └── create_sample_config.py
├── requirements.txt
└── README.md

設計のポイント

設定ファイル(config.xlsx)

01_setteing/config.xlsx の「config」シートに、A列=キー、B列=値(ヘッダーなし)の形式で設定を記述します。

主なキー

サンプル作成コマンド:

python scripts/create_sample_config.py

実装の詳細

ここでは、各フローの実装を詳しく解説します。

取得フロー:G01_load_config.py

設定ファイル(config.xlsx)を読み込み、辞書として返します。

import openpyxl
from pathlib import Path

def load_config():
    """
    config.xlsx を読み込み、辞書として返す
    """
    config_path = Path("01_setteing/config.xlsx")
    if not config_path.exists():
        raise FileNotFoundError(f"設定ファイルが見つかりません: {config_path}")
    
    wb = openpyxl.load_workbook(config_path, data_only=True)
    ws = wb["config"]
    
    config = {}
    for row in ws.iter_rows(min_row=1, values_only=True):
        if row[0] is not None:  # キーがある行のみ
            key = str(row[0]).strip()
            value = row[1] if row[1] is not None else ""
            config[key] = str(value).strip()
    
    wb.close()
    return config

if __name__ == "__main__":
    config = load_config()
    print("設定読込完了:")
    for key, value in config.items():
        print(f"  {key}: {value}")

ポイント

取得フロー:G02_fetch_webapp_a.py

Playwrightを使ってWebアプリAに接続し、表データを取得します。

from playwright.sync_api import sync_playwright
import time

def fetch_webapp_a(config):
    """
    WebアプリA から表データを取得
    
    Returns:
        list[list]: 表データ(行のリスト、各行はセルのリスト)
    """
    url = config.get("取得元WebアプリURL")
    if not url:
        raise ValueError("config に '取得元WebアプリURL' が設定されていません")
    
    port = config.get("Edgeリモートデバッグポート", "9222")
    
    with sync_playwright() as p:
        try:
            # 既存のEdgeに接続を試みる
            browser = p.chromium.connect_over_cdp(f"http://localhost:{port}")
            print(f"既存のEdge(ポート{port})に接続しました")
        except Exception:
            # 接続できない場合は新しいブラウザを起動
            print("既存のEdgeに接続できませんでした。新しいブラウザを起動します")
            from src.launch_edge_with_guide import launch_edge_with_guide
            launch_edge_with_guide(config)
            # 再接続
            browser = p.chromium.connect_over_cdp(f"http://localhost:{port}")
        
        context = browser.contexts[0]
        page = context.pages[0] if context.pages else context.new_page()
        
        # WebアプリAに移動
        page.goto(url)
        page.wait_for_load_state("networkidle")
        
        # 表データを取得(例:table要素から)
        table_data = []
        table = page.locator("table").first
        rows = table.locator("tr").all()
        
        for row in rows:
            cells = row.locator("td, th").all()
            row_data = [cell.inner_text().strip() for cell in cells]
            if row_data:  # 空行はスキップ
                table_data.append(row_data)
        
        print(f"取得完了: {len(table_data)}行")
        browser.close()
        
        return table_data

if __name__ == "__main__":
    from src.G01_load_config import load_config
    config = load_config()
    data = fetch_webapp_a(config)
    print("取得データ:")
    for row in data:
        print(row)

ポイント

取得フロー:G03_build_input_excel.py

取得したデータを加工し、入力用Excelファイルを作成します。

import openpyxl
from pathlib import Path
from datetime import datetime

def build_input_excel(config, table_data):
    """
    取得データから入力用Excelを作成
    
    Args:
        config: 設定辞書
        table_data: 取得した表データ(2次元リスト)
    
    Returns:
        str: 作成したExcelファイルのパス
    """
    save_dir = Path(config.get("inputFile保存先", "02_inputFile"))
    save_dir.mkdir(parents=True, exist_ok=True)
    
    # ファイル名: YYYYMMDD_input.xlsx
    today = datetime.now().strftime("%Y%m%d")
    file_path = save_dir / f"{today}_input.xlsx"
    
    # Excelファイル作成
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "入力データ"
    
    # ヘッダー行(例)
    headers = ["取引先コード", "取引先名", "営業担当", "金額", "備考", "動作結果"]
    ws.append(headers)
    
    # データ行を追加(例:table_dataの1行目はヘッダーなのでスキップ)
    for row_data in table_data[1:]:
        # データ変換のロジック(例:必要な列だけ抽出、形式変換など)
        converted_row = [
            row_data[0],  # 取引先コード
            row_data[1],  # 取引先名
            row_data[2],  # 営業担当
            row_data[3],  # 金額
            row_data[4] if len(row_data) > 4 else "",  # 備考
            ""  # 動作結果(登録時に記入)
        ]
        ws.append(converted_row)
    
    # スタイル調整(例:ヘッダーを太字に)
    for cell in ws[1]:
        cell.font = openpyxl.styles.Font(bold=True)
    
    wb.save(file_path)
    print(f"入力Excelを作成しました: {file_path}")
    
    return str(file_path)

if __name__ == "__main__":
    from src.G01_load_config import load_config
    from src.G02_fetch_webapp_a import fetch_webapp_a
    
    config = load_config()
    table_data = fetch_webapp_a(config)
    file_path = build_input_excel(config, table_data)
    print(f"完了: {file_path}")

ポイント

取得フロー:run_kakutoku_flow.py

G01→G02→G03を順次実行するエントリポイントです。

from src.G01_load_config import load_config
from src.G02_fetch_webapp_a import fetch_webapp_a
from src.G03_build_input_excel import build_input_excel

def run_kakutoku_flow():
    """
    取得フロー全体を実行
    """
    print("=== 取得フロー開始 ===")
    
    # G01: 設定読込
    print("\n[G01] 設定読込")
    config = load_config()
    
    # G02: WebアプリA 表取得
    print("\n[G02] WebアプリA 表取得")
    table_data = fetch_webapp_a(config)
    
    # G03: 入力Excel作成
    print("\n[G03] 入力Excel作成")
    file_path = build_input_excel(config, table_data)
    
    print(f"\n=== 取得フロー完了 ===")
    print(f"作成ファイル: {file_path}")
    
    return file_path

if __name__ == "__main__":
    run_kakutoku_flow()

実行方法

python -m src.run_kakutoku_flow

登録フロー:R01_load_config_and_excel.py

設定とExcelファイルを読み込みます。

import openpyxl
from pathlib import Path
from src.G01_load_config import load_config

def load_config_and_excel(excel_path):
    """
    設定とExcelファイルを読み込む
    
    Returns:
        tuple: (config辞書, workbook, worksheet, データ行リスト)
    """
    config = load_config()
    
    excel_path = Path(excel_path)
    if not excel_path.exists():
        raise FileNotFoundError(f"入力Excelが見つかりません: {excel_path}")
    
    wb = openpyxl.load_workbook(excel_path)
    ws = wb.active
    
    # データ行を取得(ヘッダー行をスキップ)
    data_rows = []
    for row_idx, row in enumerate(ws.iter_rows(min_row=2, values_only=False), start=2):
        # 行データを辞書化(例)
        row_dict = {
            "row_idx": row_idx,
            "取引先コード": row[0].value if row[0].value else "",
            "取引先名": row[1].value if row[1].value else "",
            "営業担当": row[2].value if row[2].value else "",
            "金額": row[3].value if row[3].value else "",
            "備考": row[4].value if row[4].value else "",
            "動作結果": row[5].value if row[5].value else "",
        }
        data_rows.append(row_dict)
    
    print(f"Excel読込完了: {len(data_rows)}行")
    
    return config, wb, ws, data_rows

if __name__ == "__main__":
    from datetime import datetime
    today = datetime.now().strftime("%Y%m%d")
    excel_path = f"02_inputFile/{today}_input.xlsx"
    config, wb, ws, data_rows = load_config_and_excel(excel_path)
    print(f"読込データ: {len(data_rows)}行")
    for row in data_rows[:3]:  # 最初の3行だけ表示
        print(row)
    wb.close()

ポイント

登録フロー:R02_open_webapp_b.py

WebアプリBを開き、「データを登録する」ボタンをクリックします。

from playwright.sync_api import sync_playwright, Page
import time

def open_webapp_b(config):
    """
    WebアプリBを開き、「データを登録する」をクリック
    
    Returns:
        tuple: (browser, page)
    """
    url = config.get("登録先WebアプリURL")
    if not url:
        raise ValueError("config に '登録先WebアプリURL' が設定されていません")
    
    wait_ms = int(config.get("データ登録するクリック後待機ms", "2000"))
    
    p = sync_playwright().start()
    browser = p.chromium.launch(channel="msedge", headless=False)
    context = browser.new_context()
    page = context.new_page()
    
    # WebアプリBに移動
    page.goto(url)
    page.wait_for_load_state("networkidle")
    
    # 「データを登録する」ボタンをクリック
    register_button = page.get_by_role("button", name="データを登録する")
    register_button.click()
    
    # 待機
    time.sleep(wait_ms / 1000)
    page.wait_for_load_state("networkidle")
    
    print("WebアプリB: 登録画面を開きました")
    
    return browser, page

if __name__ == "__main__":
    from src.G01_load_config import load_config
    config = load_config()
    browser, page = open_webapp_b(config)
    input("登録画面を確認してEnterを押してください...")
    browser.close()

ポイント

登録フロー:R04_R05_register.py(最重要)

1行分のデータをフォームに入力し、確認・登録を行います。Streamlitのセレクトボックス対応など、工夫が詰まっています。

from playwright.sync_api import Page
import time
import traceback

def register_one_row(page: Page, row_dict: dict, config: dict):
    """
    1行分のデータを登録
    
    Args:
        page: Playwrightのページオブジェクト
        row_dict: 行データ(辞書)
        config: 設定辞書
    
    Returns:
        str: 動作結果("登録完了" or エラーメッセージ)
    """
    try:
        # configから待機時間を取得
        confirm_wait_ms = int(config.get("確認クリック後待機ms", "1000"))
        register_wait_ms = int(config.get("登録クリック後待機ms", "2000"))
        
        # --- R04: フォーム入力 ---
        
        # テキスト入力(例)
        page.get_by_label("取引先コード").fill(str(row_dict["取引先コード"]))
        page.get_by_label("取引先名").fill(str(row_dict["取引先名"]))
        page.get_by_label("金額").fill(str(row_dict["金額"]))
        page.get_by_label("備考").fill(str(row_dict["備考"]))
        
        # Streamlit セレクトボックス(営業担当)
        # ※重要:ドロップダウン内の選択肢のみを対象にする
        sales_value = str(row_dict["営業担当"])
        try:
            # まずセレクトボックス自体をクリックして開く
            page.get_by_label("営業担当").click()
            
            # ドロップダウン内の選択肢をクリック
            dropdown = page.get_by_test_id("stSelectboxVirtualDropdown")
            dropdown.get_by_text(sales_value, exact=True).click()
            
        except Exception as e:
            raise RuntimeError(
                f"フィールド「営業担当」(値: \"{sales_value}\") で失敗: {type(e).__name__}: {str(e)}"
            )
        
        # --- R05: 確認・登録 ---
        
        # 「確認」ボタンをクリック
        try:
            page.get_by_role("button", name="確認").click()
            time.sleep(confirm_wait_ms / 1000)
            page.wait_for_load_state("networkidle")
        except Exception as e:
            raise RuntimeError(
                f"「確認」ボタンクリックで失敗: {type(e).__name__}: {str(e)}"
            )
        
        # 「登録」ボタンをクリック
        try:
            page.get_by_role("button", name="登録").click()
            time.sleep(register_wait_ms / 1000)
            page.wait_for_load_state("networkidle")
        except Exception as e:
            raise RuntimeError(
                f"「登録」ボタンクリックで失敗: {type(e).__name__}: {str(e)}"
            )
        
        print(f"  行{row_dict['row_idx']}: 登録完了")
        return "登録完了"
    
    except Exception as e:
        error_msg = f"登録エラー: {str(e)}"
        print(f"  [登録エラー] 行{row_dict['row_idx']}: {type(e).__name__}: {str(e)}")
        traceback.print_exc()
        return error_msg

if __name__ == "__main__":
    # テスト用(手動実行時はR02と組み合わせて使う)
    pass

ポイント

登録フロー:run_touroku_flow.py

メッセージボックスでユーザーに選択肢を提示し、登録フローを実行します。

import tkinter as tk
from tkinter import messagebox, filedialog
from pathlib import Path
from datetime import datetime
from src.run_kakutoku_flow import run_kakutoku_flow
from src.R01_load_config_and_excel import load_config_and_excel
from src.R02_open_webapp_b import open_webapp_b
from src.R04_R05_register import register_one_row

def run_touroku_flow():
    """
    登録フロー全体を実行
    """
    print("=== 登録フロー開始 ===")
    
    # tkinterのルートウィンドウ(非表示)
    root = tk.Tk()
    root.withdraw()
    
    # メッセージ1: インプットファイル作成の確認
    response = messagebox.askyesno(
        "確認",
        "インプットファイルを作成しますか?\n\n"
        "はい: 取得フローを実行して新しいファイルを作成\n"
        "いいえ: 既存のファイルを使用"
    )
    
    action = "create" if response else "use_existing"
    
    # メッセージ2: 動作確認
    confirm = messagebox.askokcancel(
        "確認",
        f"動作: {'新規作成' if action == 'create' else '既存ファイル使用'}\n\n"
        "この内容で実行しますか?"
    )
    
    if not confirm:
        print("キャンセルされました")
        return
    
    # ファイルパスの決定
    today = datetime.now().strftime("%Y%m%d")
    default_path = Path(f"02_inputFile/{today}_input.xlsx")
    
    if action == "create":
        # 取得フローを実行
        print("\n--- 取得フロー実行 ---")
        excel_path = run_kakutoku_flow()
    else:
        # 既存ファイルを使用
        if default_path.exists():
            excel_path = str(default_path)
            print(f"本日日付のファイルを使用: {excel_path}")
        else:
            messagebox.showinfo(
                "ファイル選択",
                "本日日付のファイルが見つかりません。\nファイルを選択してください。"
            )
            excel_path = filedialog.askopenfilename(
                title="入力Excelファイルを選択",
                initialdir="02_inputFile",
                filetypes=[("Excelファイル", "*.xlsx")]
            )
            if not excel_path:
                print("ファイルが選択されませんでした。終了します。")
                return
    
    # R01: 設定+Excel読込
    print("\n[R01] 設定+Excel読込")
    config, wb, ws, data_rows = load_config_and_excel(excel_path)
    
    # R02: WebアプリB を開く
    print("\n[R02] WebアプリB を開く")
    browser, page = open_webapp_b(config)
    
    # R04/R05: 各行を登録
    print("\n[R04/R05] 各行を登録")
    for row_dict in data_rows:
        result = register_one_row(page, row_dict, config)
        
        # 結果をExcelに書き込む
        row_idx = row_dict["row_idx"]
        ws.cell(row=row_idx, column=6).value = result  # F列(動作結果)
    
    # Excelを保存
    wb.save(excel_path)
    print(f"\nExcelに結果を保存しました: {excel_path}")
    
    # ブラウザを閉じる
    browser.close()
    
    print("\n=== 登録フロー完了 ===")

if __name__ == "__main__":
    run_touroku_flow()

実行方法

python -m src.run_touroku_flow

ポイント

詰まったポイントと解決策

実装中に遭遇した主な問題と、その解決方法をまとめます。

問題1: Streamlit セレクトボックスの strict mode violation

現象

セレクトボックスで「田中」を選ぼうとすると、以下のエラーが発生:

Error: strict mode violation: get_by_text("田中") resolved to 2 elements

原因

Streamlitのセレクトボックスは、native の <select> 要素ではありません。画面上には以下の2つの要素が存在します:

  1. フォーム上に表示されている現在の選択値
  2. ドロップダウン内の選択肢

page.get_by_text("田中") だけでは両方にマッチしてしまい、Playwrightのstrict modeでエラーになります。

解決策

ドロップダウン内の選択肢のみを対象にするため、get_by_test_id("stSelectboxVirtualDropdown") で限定します:

# まずセレクトボックスをクリックして開く
page.get_by_label("営業担当").click()

# ドロップダウン内の選択肢をクリック
dropdown = page.get_by_test_id("stSelectboxVirtualDropdown")
dropdown.get_by_text(sales_value, exact=True).click()

この対応により、すべての行が正常に登録できるようになりました。

問題2: 「登録先WebアプリURL」がないとエラー

現象

登録フローを実行すると、以下のエラーが発生:

ValueError: config に '登録先WebアプリURL' が設定されていません

原因

config.xlsxにキーが存在しないか、未設定。

解決策

  1. サンプルconfig作成スクリプトにキーを追加
  2. エラーメッセージに対処法を明記:
if not url:
    raise ValueError(
        "config に '登録先WebアプリURL' が設定されていません。\n"
        "01_setteing/config.xlsx を確認するか、\n"
        "python scripts/create_sample_config.py を実行してください。"
    )

問題3: エラー箇所の特定が困難

現象

登録エラー時、「動作結果」列に「登録エラーあり」とだけ記録され、どこで失敗したかわからない。

解決策

例外メッセージにフィールド名・値・例外種類を含めるようにしました:

try:
    dropdown = page.get_by_test_id("stSelectboxVirtualDropdown")
    dropdown.get_by_text(sales_value, exact=True).click()
except Exception as e:
    raise RuntimeError(
        f"フィールド「営業担当」(値: \"{sales_value}\") で失敗: "
        f"{type(e).__name__}: {str(e)}"
    )

これにより、Excelの「動作結果」に以下のように詳細が記録されます:

登録エラー: フィールド「営業担当」(値: "高橋") で失敗: TimeoutError: Timeout 30000ms exceeded.

ユーザーはExcelを見るだけで、どの列・どの値で問題が起きたか把握でき、改善箇所を特定しやすくなりました。

セキュリティ強化:Windows資格情報マネージャーの活用

実務環境では、認証情報(ID/パスワード)をExcelに平文で保存するのは推奨されません。私の現場でもオーケストレーションツールがないため、Windows資格情報マネージャーを活用してセキュアに管理しています。

keyringライブラリによる実装

Pythonでは keyring ライブラリを使うことで、UiPathの「資格情報を取得」アクティビティと同等の機能を簡単に実現できます。

インストール

pip install keyring

Windows資格情報への保存(初回のみ、または手動で設定):

import keyring

# Windows資格情報に保存
# 第1引数: サービス名(インターネットアドレス/ネットワークアドレス)
# 第2引数: ユーザー名
# 第3引数: パスワード
keyring.set_password("WebAppA_Login", "admin_user", "your_password_here")

または、Windows資格情報マネージャーのGUIから直接設定することもできます:

  1. Windowsの検索で「資格情報マネージャー」を開く
  2. 「Windows資格情報」→「汎用資格情報の追加」
  3. インターネットアドレス/ネットワークアドレス:WebAppA_Login
  4. ユーザー名:admin_user
  5. パスワード:実際のパスワード

コードでの取得

import keyring

def get_credentials(service_name, username):
    """
    Windows資格情報マネージャーから認証情報を取得
    
    Args:
        service_name: サービス名(例: "WebAppA_Login")
        username: ユーザー名
    
    Returns:
        str: パスワード(取得できない場合はNone)
    """
    password = keyring.get_password(service_name, username)
    
    if not password:
        raise ValueError(
            f"資格情報が見つかりません: サービス名='{service_name}', ユーザー名='{username}'\n"
            f"Windows資格情報マネージャーで設定するか、\n"
            f"keyring.set_password('{service_name}', '{username}', 'パスワード') で登録してください。"
        )
    
    return password

# 使用例
if __name__ == "__main__":
    try:
        password = get_credentials("WebAppA_Login", "admin_user")
        print("パスワードの取得に成功しました")
    except ValueError as e:
        print(f"エラー: {e}")

ログイン処理への統合例

from playwright.sync_api import Page
import keyring

def login_to_webapp(page: Page, config: dict):
    """
    Webアプリにログイン(Windows資格情報を使用)
    """
    # configから認証情報のキーを取得
    service_name = config.get("認証サービス名", "WebAppA_Login")
    username = config.get("認証ユーザー名", "admin_user")
    
    # Windows資格情報から取得
    password = keyring.get_password(service_name, username)
    
    if not password:
        raise ValueError(f"資格情報が取得できません: {service_name}/{username}")
    
    # ログインフォームに入力
    page.get_by_label("ユーザー名").fill(username)
    page.get_by_label("パスワード").fill(password)
    page.get_by_role("button", name="ログイン").click()
    page.wait_for_load_state("networkidle")
    
    print("ログインしました")

セキュリティのポイント

configへの設定例

config.xlsx には、パスワードではなくサービス名とユーザー名だけを記載します:

キー
認証サービス名WebAppA_Login
認証ユーザー名admin_user

これにより、configファイルをGitで共有しても、実際のパスワードは漏洩しません。

オーケストレーション不在の環境に最適

私の現場のように、UiPath Orchestratorなどの大規模管理基盤がない環境では、このアプローチが最適です。軽量で自由度が高く、かつセキュアな仕組みを、OS標準機能だけで実現できます。

現場の機動力を最大化しつつ、商用環境に耐えうるセキュリティを確保する。これがPython RPAの強みです。

AI活用のコツ:開発手法の転換

今回の実装で最も重要だったのは、AIとの協働方法を見直したことです。

成功を分けた3つの「黄金律」

20分という爆速実装を支えたのは、AIへの「情報の渡し方」でした。特に以下の3点は劇的な効果がありました:

  1. 「仕様」より「素材(DOM)」を渡す

    • 動きを説明するより、outerHTML をそのまま貼るほうがAIは正確にセレクタを選べます
    • 開発者ツールで右クリック→「Copy outerHTML」で取得したHTMLを貼り付けるだけ
  2. 「どこで落ちているか」を可視化させる

    • いきなり正解を求めず、まず「失敗箇所を特定するコード」を書かせるのが近道
    • 「1〜3行目は通るはずなのに通っていない理由を探したい」と伝えるだけで、詳細なエラー出力を実装してくれる
  3. 「待機時間」の不安を具体化する

    • 「1秒待ちたい」「登録ボタン押したあと2秒待ちたい」など、現場の感覚を数値で伝える
    • 「Web アプリに負荷をかけたくない」という心配も具体的に書くと、適切な待機処理を提案してくれる

:::message さらに詳しいプロンプト術を知りたい方へ

今回の実装で効果絶大だった6つのプロンプトパターンと、実際の会話例を別記事で詳しく解説しています。RPA開発に限らず、Webアプリの自動化やAI開発全般で使えるテクニックです。

📝 関連記事もうAIを迷わせない。ブラウザ操作の自動化を20分で完結させる「聞き方」の技術(公開後リンク追加予定) :::

従来のRPA開発との違い

観点従来のRPA開発AI活用のPython開発
開発手法手動で操作を記録・調整AIに仕様と素材(DOM情報など)を渡す
デバッグGUIで1ステップずつ確認エラーログをAIに渡して即座に修正
試行錯誤画面を見ながら手作業で調整AIが複数パターンを自動で試行
実装時間6時間20分(方針転換後)

AIに渡すべき情報

成功の鍵は、AIが仕事をしやすい形で情報を提供することでした。

1. UiPath XAMLの解析結果(仕様書)

まず、既存のUiPath XAMLファイルをAIで解析し、処理フローを仕様書化しました。この仕様書をCursorに読ませることで、全体像を理解させました。

2. WebアプリのDOM情報

各操作ポイント(ボタン、入力欄など)のDOM情報を取得してAIに渡しました:

<!-- 例:「データを登録する」ボタンのDOM -->
<button role="button" class="st-emotion-cache-1234">
  データを登録する
</button>

Playwrightでは、開発者ツールで要素を右クリック→「Copy」→「Copy outerHTML」で簡単に取得できます。

3. ページ全体のouterHTML

入力フォームなど、複数の要素を一度に操作する画面では、ページ全体のouterHTMLを渡しました:

// ブラウザのコンソールで実行
console.log(document.documentElement.outerHTML);

この情報があれば、AIは各フィールドのlabelやtest-idを自動で判別し、適切なセレクタを選んでくれます。

4. エラー内容の詳細

エラー発生時は、ターミナルのスタックトレース全体をAIに渡しました。例:

TimeoutError: Timeout 30000ms exceeded.
    at R04_R05_register.py:45
    ...

AIはこれを見て、待機時間の調整やセレクタの見直しを即座に提案してくれます。

AIとの対話例

実際の会話の流れ(簡略版):

  1. :「このXAML解析結果を読んで、Pythonで実装する方針を教えて」
  2. AI:「G01〜G03の取得フローと、R01〜R05の登録フローに分けるのが良さそうです」
  3. :「WebアプリAの表データ取得部分、このDOM情報で実装して」(DOM情報を添付)
  4. AI:「G02_fetch_webapp_a.pyを作成しました」
  5. :「実行したらエラーが出た。これを解決して」(スタックトレースを添付)
  6. AI:「wait_for_load_stateを追加しました。再実行してください」
  7. :「動いた!次は登録フォーム。このouterHTMLで入力処理を作って」(outerHTMLを添付)
  8. AI:「R04_R05_register.pyを作成しました。Streamlitのセレクトボックスには特殊対応が必要です」

この流れで、約20分で動作するコードが完成しました。

学んだこと

なぜPlaywrightを選んだか

SeleniumではなくPlaywrightを選んだ理由は、以下の点でRPA開発に適していたためです:

特に、自動待機機能により、UiPathで手動調整していた待機時間の多くが不要になり、コードがシンプルになりました。

セットアップと実行

実際にこのコードを試してみたい方向けに、手順を記載します。

前提条件

セットアップ

# リポジトリをクローン
git clone https://github.com/your-repo/UiPathToPythonPattern1.git
cd UiPathToPythonPattern1

# 仮想環境を作成・有効化
python -m venv .venv
.\.venv\Scripts\Activate.ps1

# 依存パッケージをインストール
pip install -r requirements.txt

# Playwrightのブラウザをインストール
playwright install chromium

# セキュリティ対応:Windows資格情報マネージャーへの登録
# ※パスワードをコードに書かないよう、対話的に設定
python -c "import keyring; import getpass; pwd = getpass.getpass('WebAppA_Loginのパスワード: '); keyring.set_password('WebAppA_Login', 'admin_user', pwd); print('資格情報を保存しました')"

# サンプル設定ファイルを作成
python scripts/create_sample_config.py

設定ファイルの編集

01_setteing/config.xlsx を開き、以下を環境に合わせて編集します:

実行

取得フローのみ実行

python -m src.run_kakutoku_flow

登録フロー実行(メッセージで選択):

python -m src.run_touroku_flow

まとめと今後の展望

実装を通じて得られた成果

  1. 実装フェーズの劇的な圧縮

    • 基本フロー:仕様が固まった状態から20分で実装完了
    • エラーハンドリング・UX向上:2時間以上かかる処理が2〜3分で完成
    • ゼロベースでも:AIとの対話で仕様策定と実装を並行すれば、従来の1/6(6時間→1時間)で完結できる見込み
  2. デバッグ効率の向上:エラー箇所の特定と修正が10倍以上高速化

  3. 保守性の向上:テキストベースのコードで、Git管理やレビューが容易

  4. 柔軟なカスタマイズ:詳細なエラーログ、Excel出力など、要件に応じた調整が簡単

  5. セキュリティの確保:Windows資格情報マネージャーで、商用環境に耐えうるセキュアな運用

AI時代の開発手法

今回の経験から、AI時代の開発では「AIが仕事をしやすいように人間が働きかける」ことが重要だと実感しました。

このアプローチにより、従来の手動開発とは比較にならないスピードで実装が可能になります。

今後の拡張の余地

現在の実装でも実用レベルですが、さらに以下のような拡張が可能です:

これらはすでにconfigにキーが用意されているため、必要になったタイミングで実装を追加すればOKです。

今回のアプローチが最適な環境

本記事で紹介したPython RPAは、以下のような環境で特に威力を発揮します:

私の現場がまさにこの状況であり、軽量で自由度が高く、かつセキュアな仕組みが求められていました。Python + AIエディタの組み合わせは、こうした環境において最大の機動力を発揮します。

大規模運用との使い分け

一方、以下のような大規模運用では、UiPath Orchestratorなどの専用基盤が依然として有効です:

Python RPAと従来RPAツールは対立ではなく、環境や規模に応じた使い分けが重要です。目の前の現場課題を最速で解決するための選択肢として、Python + AIの可能性を示せたことが、今回の大きな収穫でした。

最後に

UiPathなどのRPAツールは素晴らしいツールですが、Python + AIエディタの組み合わせは、開発速度・保守性・柔軟性の面で新たな可能性を示してくれました。

特に、「AIとどう協働するか」という視点で開発手法を見直すことで、想像以上の生産性向上が実現できることを体感しました。

本記事が、RPA開発やPython自動化に取り組む方々の参考になれば幸いです。

参考リポジトリ

本記事のコードは、以下のリポジトリで公開予定です:

https://github.com/ukiajp/zenn-content

(※記事投稿時に実際のリポジトリURLを記載してください)


関連タグ: #Python #Playwright #RPA #UiPath #自動化 #AI #Cursor

Zennでも読む ← LOGに戻る