1
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?

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

1
Posted at

はじめに: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で各行のデータをフォーム登録

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

  • XAMLの可読性:複雑な処理になるとXAMLが肥大化し、メンテナンスが困難
  • デバッグの難しさ:エラー箇所の特定や修正に時間がかかる
  • 環境依存:UiPathのライセンスや実行環境に依存
  • 柔軟性の欠如:細かいエラーハンドリングやログ出力のカスタマイズが難しい

Python移行の目的

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

  • 保守性の向上:テキストベースのコードで、バージョン管理やレビューが容易
  • デバッグの効率化:エラーの原因特定と修正が迅速
  • 柔軟なカスタマイズ:詳細なエラーハンドリング、ログ出力、設定管理
  • AI時代の開発手法:AIエディタを活用した高速開発の実現

成果

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

  • 取得フロー(G01→G02→G03):設定読込 → Webアプリ表取得 → Excel出力
  • 登録フロー(R01→R02→R04/R05):Excel読込 → Webアプリ起動 → 行ごとに入力・確認・登録
  • config.xlsxによる設定管理:URL、保存先、待ち時間などを外部化
  • 詳細なエラー出力:どのフィールドで、どの値で失敗したかをExcelとターミナルに記録

技術スタック・全体構成

使用技術

  • Python 3:メイン開発言語
  • Playwright:ブラウザ自動操作(Edge/Chrome対応)
    • Seleniumと比較して自動待機が強力で、CDP接続も容易
    • UiPathのブラウザ自動化に近い使用感
  • openpyxl:Excelファイルの読み書き
  • tkinter:メッセージボックス、ファイル選択ダイアログ(標準ライブラリ)
  • keyring:Windows資格情報マネージャーとの連携(セキュアな認証情報管理)

二つのフロー

フロー 内容 エントリポイント
取得フロー 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

設計のポイント

  • ファイル名がステップ番号(G01、R01など)と対応
  • 各フローは独立して実行可能
  • run_*_flow.py がエントリポイント

設定ファイル(config.xlsx)

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

主なキー

  • inputFile保存先:入力Excelの保存先ディレクトリ
  • 取得元WebアプリURL:データ取得元のURL
  • 登録先WebアプリURL:データ登録先のURL
  • Edgeリモートデバッグポート:既存Edge接続用のポート番号
  • 確認クリック後待機ms:確認ボタンクリック後の待機時間
  • 登録クリック後待機ms:登録ボタンクリック後の待機時間
  • データ登録するクリック後待機ms:「データ登録する」クリック後の待機時間

サンプル作成コマンド:

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}")

ポイント

  • data_only=True で数式ではなく計算結果を取得
  • キーが空の行はスキップ
  • 単体実行でconfig内容を確認可能

取得フロー: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)

ポイント

  • 既存Edgeへの接続を優先(デバッグ用)
  • 接続できない場合は新規ブラウザ起動
  • networkidle で画面読み込み完了を待機
  • 表データを2次元リストとして取得

取得フロー: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()

ポイント

  • Excelを values_only=False で読み込み、後で書き込めるようにする
  • 各行を辞書化して扱いやすくする
  • row_idxを保持しておき、後でExcelに書き戻す際に使用

登録フロー: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()

ポイント

  • headless=False でブラウザを表示(目視確認可能)
  • 待機時間はconfigで調整可能
  • ブラウザとページオブジェクトを返し、後続処理で使用

登録フロー: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

ポイント

  • Streamlitのセレクトボックス対応get_by_test_id("stSelectboxVirtualDropdown") でドロップダウン内に限定
  • 詳細なエラーメッセージ:どのフィールドで、どの値で失敗したかを記録
  • 待機時間の調整:configから取得し、環境に応じて変更可能
  • エラー時のスタックトレース:ターミナルに詳細を出力

登録フロー: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

ポイント

  • 対話的な選択肢:tkinterで直感的なUI
  • 柔軟なファイル選択:本日日付のファイル優先、なければ選択ダイアログ
  • 結果のExcel書き込み:各行の「動作結果」列に記録
  • ブラウザ表示:登録の様子を目視確認可能

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

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

問題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("ログインしました")

セキュリティのポイント

  • 平文保存の回避:パスワードがExcelやコードに残らない
  • OS標準機能:Windows資格情報マネージャーはOS標準のセキュアストレージ
  • UiPathとの互換性:UiPathでも同じ資格情報を参照可能
  • 環境ごとの管理:開発環境・本番環境で資格情報を分離できる

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 アプリに負荷をかけたくない」という心配も具体的に書くと、適切な待機処理を提案してくれる

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

今回の実装で効果絶大だった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分で動作するコードが完成しました。

学んだこと

  • AIは「何をすべきか」より「何があればできるか」を重視する
  • 手動操作にこだわらず、AIが必要とする素材を積極的に提供する
  • エラーは詳細に記録し、AIにフィードバックすることで高速改善が可能
  • AIが最も得意なのは、「めんどくさいけど大事な処理」(エラーハンドリング、ログ出力など)の量産である

なぜPlaywrightを選んだか

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

  • 自動待機が強力:要素が表示されるまで自動で待機してくれる(UiPathの「要素が表示されるまで待機」に近い)
  • CDP接続が容易:既存のブラウザセッションに接続しやすい(デバッグ時に便利)
  • マルチブラウザ対応:Chromium、Firefox、WebKitをサポート
  • 最新の技術:現代的なWeb標準に対応し、メンテナンスも活発

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

セットアップと実行

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

前提条件

  • Python 3.9以上
  • Microsoft Edge(または Chromium ベースのブラウザ)

セットアップ

# リポジトリをクローン
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 を開き、以下を環境に合わせて編集します:

  • 取得元WebアプリURL:データ取得元のURL
  • 登録先WebアプリURL:データ登録先のURL
  • 各種待機時間:環境に応じて調整

実行

取得フローのみ実行

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が仕事をしやすいように人間が働きかける」ことが重要だと実感しました。

  • 仕様書を明確に作成し、AIに渡す
  • 操作対象のDOM情報やHTMLを積極的に提供する
  • エラーは詳細に記録し、AIにフィードバックする

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

今後の拡張の余地

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

  • networkidle タイムアウトの config 化:環境差に対応しやすくなる
  • ブラウザ選択の config 化:Edge/Chrome を切り替え可能に
  • 入力ファイル名パターンの config 化:別フォーマットへの対応が容易に
  • 登録完了後のファイル退避inputFile登録完了後保存先 を実装
  • メール通知機能完了メール送付先 を実装

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

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

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

  • オーケストレーションツールがない環境:大規模管理基盤(UiPath Orchestratorなど)が未導入
  • 部署・チーム単位の自動化:数台〜数十台規模の運用
  • 高速な要件変更が求められる:現場の状況に応じて柔軟に調整が必要
  • Git管理・コードレビューを重視:開発プロセスを標準化したい

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

大規模運用との使い分け

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

  • 数百台規模のロボット管理
  • ログの集中管理・監視
  • スケジュールトリガー実行
  • 全社標準基盤としてのガバナンス

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

最後に

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

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

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

参考リポジトリ

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

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


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

1
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
1
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?