Skip to content
Code on the Road
Go back

【自動ログイン編 後編】ARM版WindowsのWebView2アプリをUI Automationで自動操作する — pyautoguiからuiautomationへ

自動ログイン編(検証VM)で、毎朝強制ログアウトされる kabuステーション への自動2FAログインを pyautogui(画像マッチ) で組んだ。手元で1回ずつ叩くテストなら、検証VMでも本番でも通る。ところが実運用——毎朝決まった時刻に無人で走らせると、まちまちに失敗する。テストで動くことと、毎朝無人で動き続けることは別物だった。

原因と対策をたどると、最終的に画像マッチを捨てて、UI要素そのものを掴む方式(UI Automation)へ全面的に作り直すことになった。今回はその後編。環境は前回と同じ Parallels 上の ARM版 Windows 11で、ここに ARM特有の地雷も重なった。

TL;DR

なぜ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の inputbutton も 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つ:

  1. ログイン要求の「後」に届いたメールだけを対象にする(古いOTPを拾わない)。
  2. 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()

運用でハマる細かい話

学び

結果、毎朝の手作業ログインがゼロになり、無人でAPIが復帰するようになった。休場日に検証したら、2FA取得からログイン完了まで一発で通った。


シリーズ:

  1. VPS構築編
  2. アプリ運用編
  3. HTTPS化編
  4. 自動ログイン編・検証VM
  5. 自動ログイン編・後編(この記事)


Next Post
日経225先物を自作システムで自動売買して億り人を目指す — システムトレード損益公開、始める【Logical 225】