はじめに
私の汎用RPAエンジンの機能追加等は終了(余りコード需要も無いようですが)としていましたが、追加機能が必要になり、私の作業メモでごめんなさい。
今回は、厄介!?な「ブラウザ内蔵のPDFビューア」です、業務システムで帳票を出力すると、EdgeネイティブのPDFビューアが立ち上がることがよくあります。この画面から「名前を付けて保存」を自動化しようと何度も試行錯誤を重ねましたが、どうしても安定(サイトによる)しませんでした。
今回も、AIさんの力を借りてようやく「安定するダウンロード手法(の一つ)」に辿り着いたので、その開発のポイントとアプローチをメモします。
PDF画面から「保存」ボタンを押す。人間なら一瞬の操作ですが、RPA(コード)からやろうとすると以下の壁にぶつかりました。
●プラグイン空間の罠: Edge内蔵のPDFビューアは、通常のHTML DOMとは完全に隔離されたネイティブ空間で描画されています。自作のShadow DOM貫通アルゴリズムを使っても、保存ボタン(#download)にはJavaScriptから一切干渉できませんでした。
●UIA(UI Automation)要素探索
DOMがダメならOS側からアプローチしようと、画面中央を物理クリック(Invoke-DesktopCenterClick)してフォーカスを奪った後、以下のようにWindows標準のUIA技術を使ってボタンを探すコードを試しました。
VBAのWin32 APIでの「画面中央を物理クリック」をPowerShellエンジン側へ
試行関数:Invoke-DesktopCenterClick (Lib-DesktopUIA_v103に追加しました。)
function Invoke-DesktopCenterClick {
param ([int]$WaitMs = 500)
$func = $MyInvocation.MyCommand.Name
try {
# 画面のフォーカスが安定するまで少し待機
Start-Sleep -Milliseconds $WaitMs
# 最前面のウィンドウハンドルを取得
$hwnd = [Win32Api.Win32Utils]::GetForegroundWindow()
if ($hwnd -eq [IntPtr]::Zero) {
throw "最前面のウィンドウが取得できません"
}
# ウィンドウの矩形領域を取得
$rect = New-Object Win32Api.RECT
$res = [Win32Api.Win32Utils]::GetWindowRect($hwnd, [ref]$rect)
if (-not $res) {
throw "ウィンドウ領域の取得に失敗しました"
}
# 画面中央の座標を計算
$centerX = [int]($rect.Left + (($rect.Right - $rect.Left) / 2))
$centerY = [int]($rect.Top + (($rect.Bottom - $rect.Top) / 2))
# マウスカーソルを中央へ移動
[Win32Api.Win32Utils]::SetCursorPos($centerX, $centerY) | Out-Null
# 物理クリックの発火 (LeftDown: 0x0002, LeftUp: 0x0004)
[Win32Api.Win32Utils]::mouse_event(0x0002, 0, 0, 0, 0)
[Win32Api.Win32Utils]::mouse_event(0x0004, 0, 0, 0, 0)
return "Clicked Center: X=$centerX, Y=$centerY"
} catch {
throw (New-EngineException -Func $func -Type "UIAエラー" -Message "画面中央の物理クリックに失敗しました" -Details $_.Exception.Message)
}
}
なぜ「画面中央を物理クリック」する?
●PDFビューアは「Webページ」ではない●「ウィンドウが前にある」=「キー入力ができる」ではない
●JavaScriptが無視される以上、人と同じようにOS(Windows)のレベルから直接「ここをクリックしたよ」という信号(マウスイベント)を送るしかない。
●PDFが表示されている画面の端の方をクリックしてしまうと、たまたま「スクロールバー」や「黒いツールバーの隙間」などに当たってしまい、フォーカスが正しくPDF本体に移らないリスクがある。
ウィンドウの「ど真ん中(縦の半分の位置、横の半分の位置)」であれば、どんなレイアウトやPDFのサイズであっても、確実にPDFの文書領域にヒットする。
' パターンA:「保存」ボタンを探してクリック
rpaEngine.RunAction "Invoke-UiaAction", CreateParams("Action", "Click", "Name", "保存")
' パターンB:「名前を付けて保存」を探してクリック
If Err.Number <> 0 Then
Err.Clear
rpaEngine.RunAction "Invoke-UiaAction", CreateParams("Action", "Click", "Name", "名前を付けて保存")
End If
これもダメでした。理由は「OSとブラウザの壁」です。EdgeのPDFビューアのツールバーは、標準的なWindowsのボタンではなく、ブラウザ内部で独自に描画された特殊なUI(PDFium)。OSのアクセシビリティ機能(UIA)からは「保存」という名前のボタンとして認識されないためタイムアウトでした。
●物理操作(SendKeys)とダイアログ捕捉
UIAでのボタン探索も諦め、 Ctrl + S のキーボード送信に頼りましたが、裏で別の画面が立ち上がったり、画面が最小化されているとキー送信が空振りします。
「Fetch API」: 画面のボタンを押すこと(物理的なアプローチ)を完全に諦める。
「現在ブラウザが開いているPDFのURL(セッション付き)を、JavaScriptのFetch APIで再取得し、強制的にダウンロードトリガーを引く」という手法を新たに追加しました。
AI(GEMINI)曰く、Fetch APIは、JavaScriptからネットワーク通信を行うための標準機能です。これを使うと大きなメリットがあるよ。
●ブラウザに「気を利かせない」
通常URLをクリックすると、ブラウザは「PDFだからビューアで開こう」と画面(UI)を切り替えてしまうがFetch APIを使うと、画面UIを完全に無視して、裏側のネットワーク層で直接「生のデータ(バイナリ)」だけを奪い取れる。
●ログイン状態(セッション)を完全共有
業務システム等は「ログインしていないとファイルをダウンロードできない」仕様になっているが、Fetch APIの最大の強みは、「現在ブラウザが開いている画面のログイン状態(Cookieやセッション)を自動的に引き継いで通信してくれる」。(VBA側で面倒な認証処理を作り直す必要がありません。)
以下が、Fetch APIを利用して裏側からPDFを取得し、自作自演でクリックを発火させるJavaScriptのコアロジックです。これをPowerShellエンジンに組み込みました。
// ① 対象のURLへ裏側でリクエストを送り、データを取得する
fetch(target)
// ② 取得したデータを「Blob(生のバイナリデータ)」としてメモリに保持する
.then(r => r.blob())
.then(b => {
// ③ メモリ上のデータにアクセスするための「一時的な内部URL」を作る
var url = window.URL.createObjectURL(b);
// ④ 画面には見えない「透明なダウンロードリンク」を仮想的に作り、自作自演でクリックする
var a = document.createElement('a');
a.href = url;
a.download = 'auto_download_temp';
document.body.appendChild(a);
a.click();
// (数秒後に一時URLや透明なリンクをメモリからお掃除して完了)
});
Invoke-WebFetchDownload を追加しました。(Lib-WebAction_v202)
# --- Fetch APIを利用した裏側でのサイレントダウンロード発火 ---
function Invoke-WebFetchDownload {
param ([string]$TargetUrl = "")
$func = $MyInvocation.MyCommand.Name
$targetUrlEscaped = $TargetUrl.Replace("'", "\'")
$js = @"
try {
// URLの指定がない場合は現在のページURLを使用
var target = '$targetUrlEscaped';
if (!target) target = window.location.href;
fetch(target)
.then(r => r.blob())
.then(b => {
var a = document.createElement('a');
var url = window.URL.createObjectURL(b);
a.href = url;
a.download = 'auto_download_temp'; // 実際のファイル名はエンジン側で強制上書きされる
document.body.appendChild(a);
a.click();
// メモリ解放とDOMのお掃除
setTimeout(function() {
window.URL.revokeObjectURL(url);
if (a.parentNode) a.parentNode.removeChild(a);
}, 1000);
});
return true;
} catch(e) {
return false;
}
"@
$res = Invoke-WebScript -Js $js
if (-not $res) {
throw (New-EngineException -Func $func -Type "JSエラー" -Message "Fetch APIを利用したダウンロードトリガーの発火に失敗しました" -Details $TargetUrl)
}
#● Write-DebugLog -Message "[$func] 情報: Fetch APIによる自己ダウンロードイベントを発火しました" -Level Info
return "Fetch Download Triggered"
}
VBA側の実行コード例
シンプルな命令を出すだけで、安全・確実なダウンロードが完了します。
Dim savePath As String
savePath = "C:\Desktop\テストデータ\Dummy_20260718.pdf"
' 1. ダイアログを出さずに保存先とファイル名を強制指定(事前予約)
rpaEngine.RunAction "Enable-SilentDownload", CreateParams("DownloadDirectory", "C:\Desktop\テストデータ", "FileName", "Dummy_20260718.pdf")
' 2. 業務システムのPDF画面へ遷移
rpaEngine.RunAction "Invoke-WebNavigation", CreateParams("Url", "https://.../dummy.pdf")
' 3. PDFの実体(embed要素)の出現待機
rpaEngine.RunAction "Wait-WebElement", CreateParams("Selector", "embed[type='application/pdf']", "TimeoutSec", 15)
' 4. Fetch APIで裏側でのサイレントダウンロード発火
rpaEngine.RunAction "Invoke-WebFetchDownload"
' 5. ダウンロード一時ファイル(.crdownload)の消失とロック解除を監視して待機
rpaEngine.RunAction "Wait-FileDownload", CreateParams("FilePath", savePath, "TimeoutSec", 30)
MsgBox "PDFのサイレント保存が完了しました!"
このアプローチのメリット
この3ステップ(事前予約 → Fetch APIで発火 → 完了待機)で、「画面が裏に隠れていても、最小化されていても成功する」ダウンロードが実現しました。
(マウスカーソルの位置や解像度に依存するUI Automationの苦手を克服し、RPA化の汎用性を高める。)
続く予定 、、 Invoke-WebFetchDownload
おわりに
これだけAIさんが身近で応援してくれて、無料で簡単な操作コードが作成出来る時代はありがたい。
今回のPDFダウンロード対応コードも、私のガラクタ倉庫(GITHUBさん)に放り込んでおきます。バグ等を教えていただけたら幸いです。
(Lib-DesktopUIA_v103 / Lib-WebAction_v202 を追加しています。もしも、利用される時はPs_Engine_Core_v202 の # 常時ロード対象モジュールの定義 $alwaysLoadLibs = @(、、、 を修正してください。)
Invoke-UiaSafeSaveAs も一部修正しています。