自動ログイン編(検証VM)で、毎朝強制ログアウトされる kabuステーション への自動2FAログインを pyautogui(画像マッチ) で組んだ。手元で1回ずつ叩くテストなら、検証VMでも本番でも通る。ところが実運用——毎朝決まった時刻に無人で走らせると、まちまちに失敗する。テストで動くことと、毎朝無人で動き続けることは別物だった。
原因と対策をたどると、最終的に画像マッチを捨てて、UI要素そのものを掴む方式(UI Automation)へ全面的に作り直すことになった。今回はその後編。環境は前回と同じ Parallels 上の ARM版 Windows 11で、ここに ARM特有の地雷も重なった。
TL;DR
- 証券アプリ(ローカルAPI提供あり)が毎朝強制ログアウト+2FAされ、無人の自動売買が止まる。
- 画像マッチ(pyautogui)の自動ログインは、ログイン画面がWebフォームのため描画ゆらぎで毎朝こけた。
- 定番の pywinauto は ARM64 Windows で import 不可(
win32ui=MFC依存)。 - 要素ベースの
uiautomation(comtypes製・MFC非依存) に乗り換え。 - ただし WebView2 のフォームは既定だと UI Automation から見えない。起動時に
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--force-renderer-accessibilityを渡すと露出する。← 最大の山。 - 2FA はメールOTPをIMAPで取得。iCloud は
FETCH (RFC822)が空を返すのでBODY.PEEK[]を使う。
なぜpyautoguiでは安定しなかったか
ログイン画面のボタンや入力欄をスクリーンショットのテンプレートマッチで探してクリック/入力する、いわゆる pyautogui 方式で組んでいた。これが毎朝まちまちに失敗する。
犯人は、ログイン画面が Auth0 のWebフォームを埋め込みブラウザ(Edge WebView2)で表示していること。Webのレンダリングは日によって1pxずれたり、フォントのアンチエイリアスが変わったりする。テンプレートが一致せず ImageNotFound、15分後にタスクが強制終了——という流れだ。
画像マッチは「対象がネイティブのボタン」なら強いが、「中身がWeb」だと描画ゆらぎに弱い。
座標・画像をやめて、UI要素そのものを掴む方式に変える。これが今回の方針転換だ。
ハマり①: pywinauto が ARM Windows で死ぬ
Windows の要素ベース自動化の定番は pywinauto。だが import した瞬間にこれ:
ImportError: DLL load failed while importing win32ui: 指定されたモジュールが見つかりません。
pywinauto は内部で win32ui(=MFCベース)に依存していて、ARM64 では必要なDLLが無く import できない。ここで小一時間溶かした。
教訓: ARM Windows では「win32ui / MFC依存」のライブラリは地雷。pywinauto はこれに該当する。
uiautomation を採用する
代わりに uiautomation を使う。これは Microsoft の UI Automation を comtypes(純PythonのCOM呼び出し) で叩くライブラリで、MFC非依存・ARM64でも import できる。
pip install uiautomation
ハマり②: WebView2 のフォームが「見えない」
要素ツリーをダンプして username 入力欄を探す——が、出てこない。ログイン窓を覗くと、見えるのは RootWebArea ひとつだけ。HTMLの input も button も UIA に露出していない。
ここが本当の山場。WebView2 は既定だとアクセシビリティ(a11y)ツリーを生成しない(必要になるまで遅延する)。解決策は、アプリ起動時に環境変数で WebView2 にフラグを渡すこと:
import os, subprocess
# WebView2 にアクセシビリティ(a11y)ツリーを強制生成させる
os.environ["WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS"] = "--force-renderer-accessibility"
# このプロセスから起動したアプリが上の env を継承する
subprocess.Popen(["cmd", "/c", "start", "", KABU_EXE])
これを入れた途端、EditControl autoId='username' や各ボタンが UIA に現れる。あとは素直に要素操作:
import uiautomation as auto
win = auto.WindowControl(searchDepth=4, RegexName="ログイン")
auto.EditControl(searchFromControl=win, AutomationId="username").GetValuePattern().SetValue(USER)
auto.ButtonControl(searchFromControl=win, Name="次へ").Click()
# パスワード → ログイン →(2FAコード)→「パスキーなしで続行」… と画面検出ループで進める
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--force-renderer-accessibilityは「WebView2製アプリを UI Automation で操作したい」全ケースで効く一手。これ単体で記事になるくらいのキモ。
固定手順ではなく「画面検出ループ」にする
「速い再ログインだと2FAがスキップされて直接パスキー画面になる」といった分岐がある。だから固定の手順ではなく、いまどの画面かを要素の有無で判定して進めるループにしておくと安定する。2FA画面/パスキー案内/ログイン完了のどれが来ても対応できる。
ハマり③: 2FAのメールOTPをIMAPで取る(iCloudの罠)
6桁コードはメールで届く。これを IMAPで読んで正規表現で抜く。注意点が2つ:
- ログイン要求の「後」に届いたメールだけを対象にする(古いOTPを拾わない)。
- iCloud のIMAPは
FETCH (RFC822)だと本文が空()で返る。BODY.PEEK[]を使う。
# NG: typ, data = imap.fetch(num, "(RFC822)") # iCloud だと空
typ, data = imap.fetch(num, "(BODY.PEEK[])") # OK
raw = data[0][1]
code = re.search(rb"\b(\d{6})\b", raw).group(1).decode()
運用でハマる細かい話
- uiautomation が root logger を乗っ取る:
logging.basicConfigが効かなくなり、勝手に@AutomationLog.txtが生える。自前ロガーにFileHandlerを直接付けてpropagate=Falseにして回避。 - UIAは描画非依存だが、SendKeys(入力)はセッションが要る: 画像キャプチャは不要になったが、キー入力は「ログオン中・ロック解除済みのセッション」が必要。RDP切断やロックは避ける(自動ログオン推奨)。
- タスクスケジューラはコンソールセッションで: アプリ利用可能時刻の直後にトリガーする。
学び
- Web中身のデスクトップアプリを自動化するなら、画像マッチより UI Automation(要素ベース)。
- ARM Windows では pywinauto(win32ui/MFC)は不可 → uiautomation(comtypes) を選ぶ。
- WebView2 は
--force-renderer-accessibilityで UIA に露出させる。これが最大のキモ。 - 2FA は IMAP(iCloud は
BODY.PEEK[]) で機械的に取得できる。
結果、毎朝の手作業ログインがゼロになり、無人でAPIが復帰するようになった。休場日に検証したら、2FA取得からログイン完了まで一発で通った。
シリーズ:
- VPS構築編
- アプリ運用編
- HTTPS化編
- 自動ログイン編・検証VM
- 自動ログイン編・後編(この記事)