はじめに: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で実装していたのは、以下のような典型的な業務自動化シナリオです:
- WebアプリAで表データを取得
- データを変換して入力用Excelを作成
- 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 |
登録フローの起動時処理
登録フローは、ユーザーに選択肢を提示する対話型の仕組みです:
- 「インプットファイルを作成しますか?」(はい/いいえ)
- 「この動作でいいですか?(選択した動作が表示される)」(OK/キャンセル)→ キャンセルなら終了
- 「はい」を選択:取得フロー(G01→G02→G03)を実行してから、本日日付の入力ファイルで登録フローを実行
- 「いいえ」を選択:本日日付の入力ファイルを探す。なければファイル選択ダイアログを表示
ディレクトリ構成
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つの要素が存在します:
- フォーム上に表示されている現在の選択値
- ドロップダウン内の選択肢
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にキーが存在しないか、未設定。
解決策:
- サンプルconfig作成スクリプトにキーを追加
- エラーメッセージに対処法を明記:
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から直接設定することもできます:
- Windowsの検索で「資格情報マネージャー」を開く
- 「Windows資格情報」→「汎用資格情報の追加」
- インターネットアドレス/ネットワークアドレス:
WebAppA_Login - ユーザー名:
admin_user - パスワード:実際のパスワード
コードでの取得:
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点は劇的な効果がありました:
-
「仕様」より「素材(DOM)」を渡す
- 動きを説明するより、
outerHTMLをそのまま貼るほうがAIは正確にセレクタを選べます - 開発者ツールで右クリック→「Copy outerHTML」で取得したHTMLを貼り付けるだけ
- 動きを説明するより、
-
「どこで落ちているか」を可視化させる
- いきなり正解を求めず、まず「失敗箇所を特定するコード」を書かせるのが近道
- 「1〜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との対話例
実際の会話の流れ(簡略版):
- 私:「このXAML解析結果を読んで、Pythonで実装する方針を教えて」
- AI:「G01〜G03の取得フローと、R01〜R05の登録フローに分けるのが良さそうです」
- 私:「WebアプリAの表データ取得部分、このDOM情報で実装して」(DOM情報を添付)
- AI:「G02_fetch_webapp_a.pyを作成しました」
- 私:「実行したらエラーが出た。これを解決して」(スタックトレースを添付)
- AI:「wait_for_load_stateを追加しました。再実行してください」
- 私:「動いた!次は登録フォーム。このouterHTMLで入力処理を作って」(outerHTMLを添付)
- 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
まとめと今後の展望
実装を通じて得られた成果
-
実装フェーズの劇的な圧縮
- 基本フロー:仕様が固まった状態から20分で実装完了
- エラーハンドリング・UX向上:2時間以上かかる処理が2〜3分で完成
- ゼロベースでも:AIとの対話で仕様策定と実装を並行すれば、従来の1/6(6時間→1時間)で完結できる見込み
-
デバッグ効率の向上:エラー箇所の特定と修正が10倍以上高速化
-
保守性の向上:テキストベースのコードで、Git管理やレビューが容易
-
柔軟なカスタマイズ:詳細なエラーログ、Excel出力など、要件に応じた調整が簡単
-
セキュリティの確保: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