0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WebDriver BiDi for SeleniumVBA ~ Download処理を例にBiDi版のコード量が多くなる理由

0
Last updated at Posted at 2026-08-16

はじめに

WebDriver BiDi for SeleniumVBA のソースコードを見ると、クラシック版の SeleniumVBA と比較して、かなりコード量が多いことに気付きます。

一見すると、

同じブラウザを操作するだけなのに、なぜ BiDi 版ではこれほど多くのコードが必要なのか?

と思うかもしれません。

その理由の一つが、WebDriver BiDi が非同期イベントを扱うプロトコルだからです。

具体例として、Download 処理を見てみます。


利用者が書くコードは非常に短い

WebDriver BiDi for SeleniumVBA では、1件でも複数件でも同じ ExecuteDownloadsByXPath を使用します。

1件だけ Download する場合は、XPath を String で渡します。

Set result = bidi.ExecuteDownloadsByXPath( _
                "//*[@id='download-a']", _
                searchTimeoutMs:=5000, _
                timeoutMs:=30000)

複数の Download を実行する場合は、XPath の配列を渡します。

Set result = bidi.ExecuteDownloadsByXPath( _
                Array( _
                    "//*[@id='download-a']", _
                    "//*[@id='download-b']", _
                    "//*[@id='download-c']"), _
                searchTimeoutMs:=5000, _
                timeoutMs:=30000)

利用者から見れば、

Downloadボタンを指定
        ↓
クリック
        ↓
Download完了まで待機
        ↓
結果を取得

という非常に単純な処理です。

複数の場合でも、

Download Aを開始
Download Bを開始
Download Cを開始
        ↓
各Downloadを監視
        ↓
すべて終端状態になるまで待機
        ↓
Batch結果を取得

という形に見えます。

では、なぜライブラリ内部では Download 関連だけでも非常に多くのコードが必要なのでしょうか。


実は内部では「Downloadボタンをクリック」しているだけではない

ExecuteDownloadsByXPath の内部では、まず Download 関連イベントを購読します。

Me.ExecuteSessionSubscribe Array( _
    "browsingContext.downloadWillBegin", _
    "browsingContext.downloadEnd"), , True

次に、指定されたすべての XPath を解決します。

ここは重要です。

複数の XPath が指定されている場合でも、

XPath Aを解決
        ↓
XPath Bを解決
        ↓
XPath Cを解決
        ↓
すべて解決成功
        ↓
Download監視をArm

という順序になります。

つまり、途中の XPath が見つからなかった場合に、

AだけDownload開始済み
Bで要素検索失敗
Cは未実行

という中途半端な状態にならないようにしています。

すべての対象を確認した後、

ProcessRecorderQueue
ArmDownloadBatch ...

を実行し、Download Batch を監視可能な状態にしてからクリックを開始します。

概念的には、

DownloadイベントをSubscribe
        ↓
すべてのXPathを解決
        ↓
古いイベントを処理
        ↓
BatchをArm
        ↓
Click開始

という順序です。

利用者が Arm を意識する必要はありません。

ExecuteDownloadsByXPath が内部で自動的に行います。


なぜクリックする前に Arm するのか

Download は非常に高速に開始される可能性があります。

もし、

Click
    ↓
downloadWillBegin
    ↓
Download監視開始

という順序だった場合、監視開始前に downloadWillBegin が発生してしまう可能性があります。

そこで実際には、

監視準備
    ↓
現在のEvent状態を確認
    ↓
Arm
    ↓
Click
    ↓
downloadWillBegin

という順序にします。

つまり、

イベントを待つ処理は、イベントを発生させる操作より前に準備しておく必要がある

ということです。

これは Download に限らず、イベント駆動型のブラウザ制御で非常に重要な考え方です。


複数のDownloadでも、クリック自体は順番に行う

複数の Download を指定した場合でも、クリックを同時に投げるわけではありません。

概念的には、

Click A
    ↓
Click B
    ↓
Click C

と順番に dispatch します。

ただし、A の Download 完了を待ってから B をクリックするわけではありません。

例えば、

Click A
    ↓
Download A 開始
    ↓
Click B
    ↓
Download B 開始
    ↓
Download A 進行中
    ↓
Click C
    ↓
Download C 開始

という状態になります。

つまり、

クリックは逐次実行するが、Download transaction の生存期間は重なってよい

という設計です。

これによって、クリック操作の所有関係を分かりやすく保ちながら、複数Downloadを並行して進めることができます。


downloadWillBegin が来ても、すぐには信用できない

ここからが BiDi らしい部分です。

ブラウザから、

browsingContext.downloadWillBegin

が届いたとします。

しかし、

downloadWillBegin が来た

今回の Download Batch に属する

とは限りません。

別の Tab や別の処理から Download が開始された可能性があります。

そのため Wrapper は、Batch 全体と個々の Download transaction の状態を管理します。

例えば Batch 側では概念的に、

ExpectedCount
DispatchCount
StartedCount
TerminalCount
CompleteCount
CanceledCount
OwnerContext

などを保持します。

さらに個々の Download ごとに、

CorrelationKey
CorrelationMode
Context
DownloadId
NavigationId
SuggestedFilename
Status
FilePath

などを管理します。

したがって downloadWillBegin を受信すると、

downloadWillBegin
        ↓
今回のOwner Contextか?
        ↓
Download IDはあるか?
        ↓
Navigation IDはあるか?
        ↓
一意なCorrelation Keyを作れるか?
        ↓
既存Transactionと衝突していないか?
        ↓
今回のBatchのTransactionとして採用

という判断が必要になります。


Download ID がない場合もある

理想的には、Download 開始イベントと終了イベントの両方に、同じ Download ID が存在すれば簡単です。

downloadWillBegin
download = ABC123

        ↓

downloadEnd
download = ABC123

であれば、

ABC123 = ABC123

と照合できます。

しかしブラウザによっては、利用可能な Download ID が返されない場合があります。

その場合、Wrapper は、

Browsing Context
+
Navigation ID

を使って関連付けます。

概念的には、

Preferred

Download ID

または、

Fallback

Context + Navigation ID

です。

このため、各 transaction には correlationMode が保持されます。

利用者側からも、

Debug.Print tx("correlationMode")

によって、どの方式で関連付けられたか確認できます。


Download が複数始まった場合は、すべて個別に管理する

v4.2 の Download 処理では、複数の Download transaction を同時に管理できます。

例えば3つの trigger がある場合、

Click A
  ↓
downloadWillBegin A

Click B
  ↓
downloadWillBegin B

Click C
  ↓
downloadWillBegin C

となります。

Wrapper は、

Transaction A
Transaction B
Transaction C

を別々の状態として保持します。

概念的には、

downloads
   │
   ├─ CorrelationKey A
   │      ├─ filename
   │      ├─ navigationId
   │      ├─ status
   │      └─ filePath
   │
   ├─ CorrelationKey B
   │      └─ ...
   │
   └─ CorrelationKey C
          └─ ...

という構造です。

VBA では、このような transaction 群をネストした Dictionary で管理しています。


想定より多くのDownloadが始まったらどうするのか

例えば利用者が1つの trigger を指定したとします。

Set result = bidi.ExecuteDownloadsByXPath( _
                "//*[@id='download-a']")

この場合、期待される Download 数は1です。

ところが1回のクリックから、

downloadWillBegin A
downloadWillBegin B

と2件発生した場合、

最初に来たAだけを適当に返す

という処理はしません。

期待数と実際の開始数が一致しないため、Batch の構造自体が曖昧になります。

そのため、

Expected = 1
Started  = 2
        ↓
DownloadBatchCountMismatch

として扱います。

つまり、

一意に判断できない状態を、都合よく推測して成功扱いしない

という設計です。

これもコード量が増える理由の一つです。


downloadEnd が来ても、それだけでは完了ではない

Download 開始後には、

browsingContext.downloadEnd

が届きます。

しかし、

downloadEndが来た
        ↓
そのまま完了扱い

とはできません。

複数の Download が同時に進行しているためです。

そこで、

downloadWillBegin A
        ↓
Correlation情報を保存

downloadWillBegin B
        ↓
Correlation情報を保存

        ↓ 時間経過

downloadEnd B
        ↓
Transaction B と照合

downloadEnd A
        ↓
Transaction A と照合

という処理が必要になります。

重要なのは、

開始順と終了順が同じとは限らない

という点です。

例えば、

START A
START B
START C
END   B
END   A
END   C

となっても、各 Download を正しく識別できなければなりません。

そのため、単純なFIFOではなく、Correlation Key による transaction 管理が必要になります。


「終了した」と「成功した」は同じではない

downloadEnd が届いても、

complete

とは限りません。

canceled

の場合もあります。

個々の transaction では、

downloadEnd
    ↓
status確認
    ├─ complete
    └─ canceled

となります。

Batch 全体では、

A = complete
B = complete
C = complete
        ↓
Batch status = complete

ですが、

A = complete
B = canceled
C = complete
        ↓
Batch status = failed

となります。

canceled はブラウザが通知した正常な終端状態なので、transaction 自体は記録されます。

一方、

開始Eventが不足
Event loss
想定以上のDownload開始
Timeout
Filename collision
Click失敗

などは、単なる canceled とは意味が異なります。

これらは Batch の構造を信用できなくするため、構造的な Error として扱います。


同じファイル名にも注意が必要

複数Downloadでは、もう一つ問題があります。

例えば、

Download A
suggestedFilename = report.pdf

Download B
suggestedFilename = report.pdf

となった場合です。

Protocol上では、

A complete
B complete

と報告される可能性があります。

しかし保存先が同じなら、実際のファイルシステムでは後のDownloadが前のファイルを上書きする可能性があります。

そこで Wrapper は、同一 Batch 内で同じ非空の suggestedFilename が確認された場合、

DownloadBatchFilenameCollision

として扱います。

つまり、

Protocol transaction が成功したことと、保存ファイルが安全に別々に存在することは同じではない

という点まで考慮しています。


イベントが来ない場合も考えなければならない

さらに難しいのが、

Downloadが終了しなかった

のか、

終了Eventを取りこぼした

のかという違いです。

そのため Download Batch を Arm するときには、非同期イベントQueueのdrop状態も記録します。

概念的には、

Arm時点のevent-loss count
        ↓
Download処理
        ↓
現在のevent-loss count
        ↓
増加していないか確認

という処理です。

関連イベントが失われている可能性がある状態で、

たぶんDownloadが終わっていない

と推測して処理を続けることはしません。

イベント欠落は、transaction の整合性そのものを壊す可能性があるためです。


クリックも勝手に再実行できない

もう一つ重要なのが、クリックの再実行です。

例えば、

Click送信
    ↓
ブラウザではClick成功
    ↓
応答受信前に通信エラー

というケースがあります。

VBA 側から見るとエラーです。

しかしブラウザではすでにDownloadが開始されている可能性があります。

このとき、

エラーだった
    ↓
もう一度Click

としてしまうと、同じDownloadを2回開始する可能性があります。

そのため ExecuteDownloadsByXPath は、クリックの結果が曖昧になった場合でも、勝手に同じ trigger を再クリックしません。

さらに複数 trigger の途中で問題が起きた場合も、後続の trigger を無条件に実行し続けません。

非同期処理では、

エラーになった

操作が実行されなかった

とは限らないからです。


Download Batch は2段階で待機する

ExecuteDownloadsByXPath は同期的な高水準APIです。

呼び出した直後に関数から戻るわけではありません。

内部では大きく、

Stage 1
Expected数のdownloadWillBeginを確認

        ↓

Stage 2
受理した全TransactionのdownloadEndを確認

という待機を行います。

例えば3件の場合、

Expected = 3

START A
START B
START C
        ↓
3件の開始を確認

        ↓

END A
END C
END B
        ↓
3件すべて終端
        ↓
Return

となります。


Download中は完了まで次のVBA処理へ進まない

例えば、

Set result = bidi.ExecuteDownloadsByXPath( _
                "//*[@id='download-a']", _
                searchTimeoutMs:=5000, _
                timeoutMs:=30000)

Debug.Print result("status")

と書いた場合、Debug.Print が実行されるのは Download transaction が終端状態になった後です。

概念的には、

ExecuteDownloadsByXPath(...)
        ↓
Click
        ↓
downloadWillBegin
        ↓
Download進行中
        ↓
        ↓ ここで待機
        ↓
downloadEnd
        ↓
complete / canceled 判定
        ↓
Batch結果作成
        ↓
Return
        ↓
次のVBA処理

です。

複数Downloadでも同じです。

ExecuteDownloadsByXPath(...)
        ↓
A / B / Cを開始
        ↓
A進行中
B進行中
C進行中
        ↓
全Transactionが終端するまで待機
        ↓
Return

したがって、

Download開始
    ↓
まだDownload中
    ↓
次のページへNavigate

あるいは、

Download開始
    ↓
まだDownload中
    ↓
Browserを終了

といった競合を、通常の同期利用では避けやすくなっています。


待機は固定秒数ではない

この「待機」は、

Sleep 30000

のような固定秒数待機ではありません。

待機中も Wrapper はBiDiイベントを継続的に処理しています。

待機
    ↓
Event Queue確認
    ↓
downloadWillBegin ?
    ↓
どのTransactionか?
    ↓
downloadEnd ?
    ↓
どのTransactionか?
    ↓
全件終端したか?
    ↓
No → 待機継続
Yes → Return

例えば timeoutMs:=30000 を指定していても、Download が5秒で完了すれば30秒待つ必要はありません。

timeout = 30秒

0秒     Click
1秒     downloadWillBegin
5秒     downloadEnd / complete
        ↓
        Return

逆に、終端条件を確認できないまま上限時間に到達すれば Timeout になります。

したがって timeoutMs は、

必ずその時間だけ待つ

という指定ではなく、

必要なイベントを待つことができる最大時間

です。


結果も単なる1個のstatusではない

ExecuteDownloadsByXPath の戻り値は Batch を表す Dictionary です。

例えば、

status
expectedCount
dispatchCount
startedCount
terminalCount
completeCount
canceledCount
context
asyncEventsDroppedSinceArm
downloads

などが含まれます。

そして downloads の中には、各 transaction が格納されています。

概念的には、

result
 │
 ├─ status = complete
 ├─ expectedCount = 3
 ├─ startedCount = 3
 ├─ terminalCount = 3
 │
 └─ downloads
      │
      ├─ Transaction A
      ├─ Transaction B
      └─ Transaction C

となります。

利用者は必要に応じて、

Set downloads = result("downloads")

For Each key In downloads.Keys
    Set tx = downloads(key)

    Debug.Print tx("suggestedFilename")
    Debug.Print tx("status")
    Debug.Print tx("correlationMode")
    Debug.Print tx("filePath")
Next

のように個々の結果を確認できます。


Browserが返したfilePathと、ファイルシステム上の保証は別

downloadEnd には filePath が含まれる場合があります。

しかし Wrapper は、

filePathが返された
        ↓
そのファイルが確実に存在する

とは判断しません。

filePath はあくまで、

ブラウザが報告した保存先情報

です。

実際に、

  • ファイルが現在存在するか
  • サイズが期待通りか
  • Hash が一致するか
  • 他プロセスにRenameされていないか
  • 後続処理で上書きされていないか

まで保証するものではありません。

業務上そこまで確認する必要がある場合は、Download transaction 完了後に別途ファイルシステムを確認します。


Download後のCleanupも必要

Download先を変更する場合、

bidi.SetDownloadFolder downloadFolder

によってブラウザ側の Download behavior を変更します。

これは VBA の変数ではなく、ブラウザ側に存在する状態です。

そのため処理後には、

bidi.ClearDownloadBehavior

で元のDownload policyへ戻します。

Wrapper 自身も Shutdown 時にbest-effort cleanupを行います。

ここでも、

VBA側の処理が終わった

ブラウザ側の状態も自動的に消えた

とは限りません。

ブラウザ側の状態まで管理する必要があります。


利用者には数行、内部ではこれだけ動いている

利用者が見るコードは、1件ならこれだけです。

Set result = bidi.ExecuteDownloadsByXPath( _
                "//*[@id='download-a']", _
                searchTimeoutMs:=5000, _
                timeoutMs:=30000)

複数でも、

Set result = bidi.ExecuteDownloadsByXPath( _
                Array(xpathA, xpathB, xpathC))

程度です。

しかし内部では概念的に、

DownloadイベントをSubscribe
        ↓
すべてのXPathを事前解決
        ↓
古いEventを処理
        ↓
Owner Contextを確定
        ↓
Event-loss baselineを保存
        ↓
Download BatchをArm
        ↓
Triggerを順番にTrusted Click
        ↓
各downloadWillBeginを受信
        ↓
Owner Context確認
        ↓
Download ID / Navigation ID確認
        ↓
Correlation Key作成
        ↓
Foreign Download除外
        ↓
Expected Count確認
        ↓
Filename Collision確認
        ↓
各TransactionをDictionaryへ登録
        ↓
すべての開始Eventを確認
        ↓
各downloadEndを受信
        ↓
開始TransactionとのCorrelation確認
        ↓
complete / canceled判定
        ↓
全Transaction終端確認
        ↓
Event loss確認
        ↓
Timeout / Count mismatch / Collision診断
        ↓
Batch結果Dictionary作成
        ↓
Browser側状態Cleanup
        ↓
内部状態Reset

という処理が行われています。


Downloadだけの話ではない

この構造は Download だけではありません。

Popup / New Tab / New Window

Arm
→ contextCreated
→ Owner確認
→ originalOpener確認
→ Baselineとの差分
→ Foreign Context除外
→ Multiple判定
→ 確定

File Dialog

監視準備
→ Click
→ fileDialogOpened
→ Element取得
→ input.setFiles
→ Interception Cleanup
→ SPA同期

Navigation

navigationStarted
→ responseStarted
→ NavigationとのCorrelation
→ HTTP status
→ Context状態確認
→ SPA同期

SPA同期

Network Activity
+
DOM Activity
+
Completion Signal
+
Stable Window

このように、BiDi 版ではさまざまな場所に小さな状態機械が存在します。

そして複数のtransactionを扱う処理では、その状態機械をさらに識別して管理する必要があります。


まとめ

WebDriver BiDi では、ブラウザから多くのイベントを取得できます。

これは非常に強力です。

しかし BiDi が提供するのは基本的に、

「何が起きたか」というイベントと、その関連付けに使える情報

です。

そのイベントが、

今回のどの操作によるものなのか

を判断し、

利用者が扱える一つの処理結果

へ組み立てるのはクライアントライブラリ側の仕事です。

Download 処理はその典型です。

利用者から見れば、

ExecuteDownloadsByXPath(...)

だけです。

しかしその裏側では、Download開始前から監視を準備し、複数の開始・終了イベントをtransactionごとに関連付け、Foreign Downloadを除外し、件数不整合やイベント欠落、同名ファイル衝突を検出し、TimeoutやCancelを判断し、最後にBatch結果へまとめています。

さらに、クリックのような副作用を持つ操作は、通信結果が曖昧だからといって安易に再実行することもできません。

したがって、BiDi 版のコード量がクラシック版より多いことは、単純に、

実装が複雑になった

と見るだけでは十分ではありません。

むしろ、

複雑さを利用者側に漏らさないために、ライブラリ内部で状態管理とCorrelationを引き受けている。

と考える方が実態に近いでしょう。

The complexity is inside the library so that it does not leak into user code.

これが、WebDriver BiDi for SeleniumVBA の状態管理とコード量の関係を最も端的に表しています。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?