5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIは、Excel VBAをPuppeteerやPlaywright並みのコアエンジンに育てられるのか?〜Excel VBA単体でEdgeを安定自動操作させるまでの、半年間の記録〜

5
Last updated at Posted at 2026-07-12

【1. きっかけ】失われた「1行の魔法」を求めて

かつて、WindowsとExcelを愛するすべての開発者の手元には、1本の「魔法の杖」がありました。

Set objIE = CreateObject("InternetExplorer.Application")

たったこの1文を書くだけで、外部ライブラリのインストールも、事前の環境構築も一切不要。Excel単体でInternet Explorerがひとりでに立ち上がり、Web上のあらゆるデータを引っこ抜き、人間の代わりにボタンをクリックし、日常の煩雑な事務作業を自動化してくれました。

VBA開発者にとって、これほど手軽で、ポータブルで、魅力的な機能はありませんでした。

しかし、時代の流れは残酷です。IEは正式にサポートを終了。現在はEdgeの「IEモード」によってかろうじて延命措置が取られているものの、この猶予期間も残りわずか数年で終わりを迎えようとしています。

image.png

「あの頃の、インストール不要でブラウザを意のままに操れる手軽さを、現代の技術でなんとか復活させることはできないだろうか……🥺」

そんな尽きないロマンと、迫り来るサポート終了カウントダウン。それが、すべての始まりでした。


【2. 先駆者たちの足跡と、無人島での選択】

SeleniumやWebDriverを導入すれば、EdgeやChromeを自動操作できる」

image.png

それは誰もが知っている常識です。しかし、セキュリティが厳重な現代の企業環境において、「社外のEXEファイルやインストーラを勝手にダウンロードして配置する」という行為は、ポリシーによって固く禁じられているケースがほとんどです。
実際、うちもそのルートに入ってしまったのです🫠

調査を進めると、やはり同じロマンを追い求めた先駆者たちの足跡が、QiitaやGitHubにいくつか残されているのを見つけました。
どうやら、現代のChromium系ブラウザ(EdgeやChrome)を外部ツールなしで操るには、以下の2つの「深淵のルート」があるようでした。

  1. WebSocketルート:WinHttpWebSocket系APIを使い、公式のデバッグポート経由でブラウザと直接通信するアプローチ
  2. パイプルート:OSの匿名パイプを介して、ブラウザプロセスと直接JSONデータを送り合うアプローチ

しかし、見つけたレポジトリはいずれも活発な更新が行われずストップしており、最新の環境でそのまま動かすには、不安定だったり、機能不足に直面したりと、実用には程遠い状態でした🥲

「AIのブームも来ていることだし、このAIを技術的な相談相手(バディ)にして、眠っている先人たちのコードを現代版に改修させてみたら面白いんじゃないか?😳」

そう思い立った私が、最初に手を伸ばしたのは 「パイプ(標準入出力)ルート」 でした。

理由は非常にシンプル。「ソースファイル数が少なく、パッと見で理解しやすそうだったから」です。

image.png

のちに知ることになりますが、この「まずはネットワークのパズル(WebSocket)を避け、通信ルール(CDP)の癖だけをパイプでじっくり学ぶ」という段階を踏んだことが、結果として、後々の大きなブレイクスルーを生む最高のロードマップとなりました。


【3. Ver 1.0.0:寄せ集めのツギハギモンスター】

最初は、とにかく「動くこと」だけを目標にした、泥臭い寄せ集めからのスタートでした。

どうせ自動化ツールを作るなら、ブラウザを動かすだけでなく、高速にデータを取得できる仕組みも一緒に1つのクラスに統合しておきたい。そう欲張った私は、手元にある技術を強引に合体させました。

  • デベロッパーツールのネットワークタブから隠しAPIを直撃するための 「REST-WebAPI (WinHttp5.1)」
  • 先人のレポジトリから切り出し、なんとかVBAに移植した 「パイプ通信制御(CDPエンジン)」
  • 認証の甘いデモサイトなどで使うための 「WebSocket接続のコア」

お互い異なる設計思想で書かれたコードを、力技で1つの塊へと統合する。
こうして、粗削りながらも「Excel単体でブラウザと対話する」ための記念すべき初期型、Version 1.0.0 が産声を上げました。

image.png

しかし、この時点ではまだ、高負荷テストをかけた瞬間に待ち受けている「低レイヤの理不尽な洗礼」を、私は知る由もありませんでした。

【4. 非同期イベントの収集】ブラウザの「心拍」をリアルタイムに聴く

VBAからブラウザを操作するライブラリとして有名なSeleniumVBAですが、基本的にこれは「こちらから指示を出し、その結果(レスポンス)を待つ」という一方通行の設計です。

しかし、世の中にはその限界を突破しようとする先駆者がいました。hanamichi77777 氏が公開している WebDriver-BiDi-for-SeleniumVBA という拡張ライブラリです。

この拡張がもたらす最大の特徴は、双方向(BiDi)通信
こちらからコマンドを投げて待つだけでなく、ブラウザの内部で何かが起きた瞬間に(例えば、ページの読み込みが完了した、非同期でデータが届いた、エラーが発生したなど)、ブラウザ側から発信されるリアルタイムな出来事(イベント)をその場で回収できるという、次世代の自動化に欠かせない機能でした。

image.png

「この双方向のイベント監視を、自分のExcel単体CDPエンジンにも宿らせたい🥺」

そう考えた私は、AIに内部ロジックの徹底的な解析を依頼し、パイプ通信の受信ループにメスを入れました。

image.png

イベントが届くたびにVBAの Dictionary 変数へ非同期にその情報を蓄積し、任意のタイミングで取り出せる仕組み。この心臓部となるロジックを粘り強く組み立て、ついにVersion 1.1.0 にて「非同期イベント収集機能」の基礎が完成しました。

image.png

これにより、私のツールは「ただ指示を出すだけの操り人形」から、「ブラウザの心拍をリアルタイムに監視できる双方向システム」へと進化を遂げたのです。

【5. 文字コード変換】ADODB.Streamへの違和感と、低レイヤAPIの洗礼

本家ライブラリのバージョンアップに伴うマージや、幾度ものリファクタリングを繰り返す中で、避けては通れない「第1の壁」が立ちはだかりました。

それが、 「日本語の文字化け問題」 です。

VBAはよくも悪くも、Windowsの古い設計を引きずった Shift_JIS(ANSI)の世界で生きています。しかし、対話する相手であるWeb(Chromium)の世界は、UTF-8が絶対の当たり前です。
この2つの異なる言語を翻訳しなければ、Chromiumに届くデータは日本語だけ文字化けとなってエラーとなります。

{"method":"Runtime.evaluate","params":{"expression":"document.getElementById(\"var_身長\")"},"sessionId":"DB89197F1C754AEFBE1C510512CAD965","id":26}
{"error":{"code":-32700,"message":"JSON: invalid string at position 52"}}

VBAで「UTF-8変換」と検索すると、必ず登場するお馴染みのオブジェクトがあります。
ご存じ、ADODB.Stream です。

Sub Sample1()
    Dim buf As String, Target As String
    Target = "D:\Work\UTF-8のテキスト.txt"
    With CreateObject("ADODB.Stream")
        .Charset = "UTF-8"
        .Open
        .LoadFromFile Target
        buf = .ReadText
        .Close
        MsgBox buf
    End With
End Sub

しかし、私はこのオブジェクトに対して、どうしても拭えない「モヤモヤ(違和感)」を抱いていました。

「『Stream(データの流れ・ストリーム)』という名前のオブジェクトなのに、なぜテキストの文字コードを変換するためだけに使わされているんだ……?🤔」

名前と目的の不一致。ただのおまじないとして使うには、あまりにも不格好に思えました。
さらにそのまんま使うと、BOM付きで出てしまうという面倒要素もありました🫠
あの泥臭いPosision操作もしないといけないと思うと必然とこうなるのです。

「もっとクリーンで、OS本来の力を使ったスマートな翻訳方法はないのか?🤔」

泥臭く調べた結果、Windowsのカーネルが提供する生のAPI WideCharToMultiByteMultiByteToWideChar の存在にたどり着きました。

この生のAPIを扱うには、C言語などの低レイヤプログラミングでお馴染みの「少々面倒な儀式」が必要になります。

  1. まず、変換後のデータがどれくらいの大きさになるか、サイズを確認するためだけに1回APIを呼ぶ
  2. 返ってきたサイズを元に、VBA側できっちりその大きさのByte配列を用意する
  3. 準備した配列(受け皿)を渡し、ようやく本番の変換処理として、もう1回APIを呼び出す

この「2往復の儀式」は一見すると非常に手間ですが、処理速度は ADODB.Stream などの高級オブジェクトを仲介するよりも圧倒的に高速で、あの謎のPosision操作も不要です。

「これこそ、OSを直接叩いているプログラマらしい、最も美しいコードだ」

そう確信した私は、このネイティブAPIによる文字コード変換関数群をビルド。文字化けを完全に克服した Version 1.4.0 をリリースしました。

image.png

――しかし、このとき「名前が気に入らない」と敬遠したはずの ADODB.Stream が、のちに「VBAの受信バッファの限界」を救う最強のメモリストリームとして、奇跡的な形で大復活を遂げることになるのですが……それはまだ、少し先のお話です。


【6. 1.5.x系統・Issuesの洗礼】セキュリティの番兵をすり抜ける、低レイヤAPIの選択

文字化けを克服して基本構造が固まった頃、私はふと、本家リポジトリの「Issues」を覗いてみました。そこには、実際に業務の現場でこのライブラリを使おうとして、壁にぶつかった開発者たちのリアルで生々しい叫びが転がっていました。

その中で最も深刻だったのが、 Edge起動のサイレント失敗 です。

企業のガチガチに管理されたPC環境では、グループポリシーによってEdgeの「リモートデバッグRemoteDebuggingAllowed」が禁止されているケースがあります。
これに引っかかると、Edgeは一切のエラーメッセージを吐くことなく、 起動自体はするが、やり取りにサイレント失敗 します。何が起きているのか分からず、原因特定のために数日間の貴重なデバッグ時間をドブに捨てる悲劇が、現場で頻発していました。

image.png

「このポリシー制限を自動で検知して、エラーを教えてくれるアシスト機能を付けたい🤗」

知らない人でも、最初から安全に、迷わずに使えるツールにしたい――そう考えた私は、レジストリのポリシー情報を読み取る機能の実装に着手しました。

最初に試したのは、コマンドの reg query や、VBAから手軽にシステム情報を覗ける winmgmts(WMI)でした。個人PCでのテストでは見事にポリシーを取得でき、「これでいける!」と確信しました。

image.png

しかし、いざセキュリティの厳しい職場のPCでテストした瞬間、冷酷な現実(洗礼)が待ち受けていました。

「セキュリティソフト(EDR)による強制ブロック」です。

厳格なエンタープライズPCにとって、VBA(マクロ)からWMIやコマンドラインを使ってレジストリを巡回する行為は、「悪意のあるマルウェアの偵察活動」と全く同じ挙動として判定され、セキュリティの番兵に一瞬で誤検知されて失敗してしまったのです🥲

「WMIやシェルコマンドが駄目なら、Windowsのシステムに怪しまれない、最もお行儀の良い低レイヤAPIを使うしかない🫠」

ルートを根本から考え直し、後のバージョンから、Windowsが提供する読み取り専用のレジストリ操作API:RegGetValueWを直接呼び出す手法に切り替えました。
このクリーンなシステム直撃ルートは、セキュリティソフトを1ミリも刺激することなく、ポリシーの制限を完璧に暴き出すことに成功。サイレント失敗の罠を未然に防ぐ、強固なセーフティネットが完成しました。

image.png


【7. プロトコル・ホワイトリスト】Excelのシートを「安全装置」にする

Issuesに書かれていた、もう一つの生々しい問題。
それが、 file:// やローカルファイル(HTML)へのアクセスがしづらい」 という問題でした。

image.png

本家のコードは、安全のために InStr(appUrl, "http") = 0 という条件式を挟んでおり、URLの先頭が http から始まらないプロトコル(ローカルファイルを指す file:// など)への遷移を制限していました。

image.png

「本家の安全のための配慮は尊重したい。けれど、内部のVBAコードを無理やり書き換えなければローカルファイルのデバッグすらできないというのは、あまりにも不自由だ」

モヤモヤしながら、何かスマートな解決策はないかとExcelを眺めました。そこで閃いたのが、Excelならではの強みを活かしたハックでした。

「Excelシート上の『テーブル』を、プロトコルのホワイトリスト(安全装置)として使えばいい」

image.png

Excelのシート上に「許可するプロトコル一覧(http, https, file など)」を登録するテーブルを用意し、これとVBAを連携させる仕組みを構築しました。
これにより、未登録の怪しいスキームへの遷移を試みた際はVBA側で安全にエラー停止させつつ、必要であればシート上のテーブルに file://ftp:// を1行追記するだけで、内部のVBAロジックに1文字も触れることなく、安全にプロトコルの拡張が可能になりました。

VBAのコードの堅牢性と、ExcelシートのUIとしての手軽さ。この2つが見事に調和した、非常にExcelらしい解決策となりました。


【8. CDPの深化】アラートの罠と、Ver 1.1.0 の覚醒

Issuesを解決し、ツールの完成度が高まるとともに、私はさらに一歩進んだ「CDP(Chrome DevTools Protocol)の実行能力の強化」に乗り出しました。

本家ライブラリには、生のCDPコマンドを送るための非常に優秀なメソッドがすでに実装されていましたが、実戦に投入してみると、まだいくつかの限界が見えてきました。

image.png

1つ目の課題は、 「ターゲットセッション(SessionID)なしでの、ブラウザ直下へのコマンド送信」 です。
通常は特定のタブ(SessionID)に対して「ページを遷移しろ」と送りますが、ブラウザ全体の「拡張機能のインストール」などを行う場合、SessionID付きで送るとエラーになってしまいます。

{"method":"Extensions.loadUnpacked","params":{"path":"C:\\Users\\Okitsune\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\\Extensions\\cnpniohnfphhjihaiiggeabnkjhpaldj\\4.5.3_0"},"sessionId":"6811FB3467638263061B8D4B7A2256C4","id":14}
{"id":14,"error":{"code":-32000,"message":"Method not available."},"sessionId":"6811FB3467638263061B8D4B7A2256C4"}

これに対応するため、SessionIDを明示的に省いてブラウザそのものに直接語りかけるブラウザ直下実行のメソッドを拡張しました。

image.png

そして2つ目の課題が、自動化開発者を最も悩ませる悪魔の仕様、 「JavaScriptの alert(...) 表示によるフリーズ」 でした。

通常のCDP実行は、「コマンドを送ったら、Chromeからの返信JSONを同期的に待つ」という動きをします。
しかし、自動操作の過程でJavaScriptのアラート(ダイアログ)が表示されてしまうと、Chromeはユーザー(人間)が「OK」を押すまで、Pipeの返信を一切返さずに完全に沈黙してしまいます。
そうなると、VBA側はいつまでも返事を待ち続け、システム全体が永遠にタイムアウト(ハングアップ)して沈没してしまいます。

「コマンドを送るだけで、返事をその場では待たずに処理を次の行へ進める、非同期のCDPが必要だ🙄」

私はAIとキャッチボールを重ね、投げっぱなしを可能にする非同期実行メソッド ExecuteCDPAsync (旧 invokeMethodAsync)を作成しました。

image.png

「でも、コマンドを投げっぱなしにした後、アラートが出現したこと自体はどうやって検知すればいい?🤔」

そこで、かつて Version 1.1.0 で血を吐いて構築した「非同期イベント収集Dictionary」が、劇的な形で覚醒する瞬間が訪れました。

コマンドは非同期(invokeMethodAsync)で素早く投げ、Chromeから非同期に発信される「アラートを開いたぞ(Page.javascriptDialogOpening)」というイベントを、あの Dictionary バッファが裏でそっとキャッチして回収する。
AIにこの仕組みを使ったスマートなアラート検知のデモを作ってもらったとき、すべてのパズルがカチッとはまり、過去の苦労が実戦の最強の武器として完璧に蘇ったのです。

「AIのパーツ出力の早さ」と、「自分の描いた理想のフロー」がガッチリと噛み合い、ツールはどんどん強くなっていきました。

        '非同期でコマンド実行
        '※この瞬間、JavaScriptの`alert`関数が発動されます
        Dim AsyncID As Long
        AsyncID = .invokeMethodAsync("Input.dispatchMouseEvent", paramsCDP, alwaysBrowserContext:=False)

        ' --- 6. イベントキャプチャを有効化 ---
        Set .BrowserEvents = New Dictionary

        ' --- 7. 特定のイベント名が出るまでループ ---
        Const SearchEventName As String = "Page.javascriptDialogOpening"
        Do
            '非同期イベントを取り出す
            .TakeEvents

            'イベント名の確認
            If .BrowserEvents("EventMethods").Exists(SearchEventName) Then
                '出ているダイアログの情報の確認
                Dim tmp
                For Each tmp In .BrowserEvents("EventMethods")(SearchEventName)
                    Debug.Print "url    :"; tmp("params")("url")
                    Debug.Print "message:"; tmp("params")("message")
                    Debug.Print "type   :"; tmp("params")("type")
                Next

                '見つかったので抜ける
                Exit Do
            End If
        Loop While True

        ' --- 8. ダイアログに反応しておく ---
        Debug.Print "wait 2second..."
        .sleep 2
        paramsCDP.RemoveAll
        paramsCDP.Add "accept", True
        Set resCDP = .invokeMethod("Page.handleJavaScriptDialog", paramsCDP)


【9. v1.6.x 系統・BiDiへの挑戦】未来の宣告と、Excel単体へのこだわり

ある日、私は「SeleniumVBA」のGithubの Discussionsを覗いていました。そこで、私と全く同じ思想―― 「ドライバを一切使わずに、VBAから直接生CDPを叩いてブラウザを操る(DriverLess)手法を実現してみてはどう?」 という、うちと同じ思想の情熱を持つユーザーの白熱したスレッドを発見しました。

image.png

もちろん、自由な環境があるなら SeleniumPlaywright などの完成されたツールを使えばいい。それは誰もが知っている正論です。
しかし、Discussionsのやり取りの中に、見事に私と同じ「現場の現実」を代弁する言葉がありました。

image.png

通常はSelenium、Puppeteer、Playwrightを使うべきです。これらは成熟したソフトウェアでコミュニティのサポートがあります。しかし、追加のソフトウェアをインストールできない場合や、ユーザーが追加ソフトをインストールする必要のない自己完結型のソリューションを望む場合は、この方法が選択肢となります。

「そうだ、その通りだ。だからこそ自分は、この泥臭いPipeとパケットパースのエンジンを作っているんだ」と、強く背中を押される思いでした。

しかし、その後に続く、海外の優秀なコントリビューターたちの言葉が、私の胸に深く、鋭く突き刺さりました。

image.png

「ただ、将来的(Futureproof)な観点から見れば、CDPはいずれW3C標準の『WebDriver BiDi』に置き換わる。CDPには未来がない」
「そもそも、社内でローカルソフトウェアのインストールが禁止されているような厳しい環境なら、Chromeのインストールすら許されないはずだ。だから、そんな環境においてCDPサポートが自動化のブレイクスルーになるとは思えない」

それは、一見すると論理的でぐうの音も出ない正論でした。

しかし、実務の現場は違います。
「社内標準ブラウザとしてEdgeは最初から入っている。けれど、野良の『WebDriver.exe』をフォルダに置くことだけは、セキュリティポリシーで厳重に禁止されている」
これが、私たちが戦っている日本のオフィス環境の生々しいリアルです。だからこそ、このExcel単体の直接制御には、計り知れないブレイクスルーの価値があるのです。

しかし、「CDPには未来がなく、BiDi(双方向プロトコル)が今後の世界標準(未来)になる」という指摘は真実でした。
もし数年後、世の中が完全にBiDiにシフトし、ChromiumのCDPが表舞台から消える未来が来たら、自分がこれまで作ってきたツールは、ただの「時代遅れの遺物(隠れた存在)」として歴史の闇に消え去ってしまう🥲

「……だったら、やってやろうじゃないか🦊🔥」

自分の中に、狂気とも言える挑戦の火が灯りました。

「外部のドライバやツールを一切使わずに、Excel(VBA)単体だけで、未来の世界規格である『WebDriver BiDi』を完全稼働させてやる」

前代未聞の、0から1を生み出す孤独な開発が始まりました。


【10. 頼りは本家のソースコードのみ:AIエージェントとの対話】

当然ですが、ネットを検索しても、通常のAI(ChatGPTなど)に尋ねても、返ってくるのは「まずは公式サイトからWebDriver.exeをダウンロードして、PATHを通して……」という、何の役にも立たない「一般的な常識」ばかりでした。
「外部のEXEを使わずに、生のWebSocketだけでBiDiを動かす方法」など、世界中のどこにも転がっていません。

image.png

頼れるものは唯一、GoogleのChrome開発チーム(GoogleChromeLabs)が公開している、本家のオープンソースリポジトリchromium-bidiの生コード群だけでした。

しかし、TypeScriptやPythonで複雑に構築された本家のリポジトリは、VBAにしか知識がない私にとっては、まさに「未知の言語で書かれた巨大な迷宮」でした。読めども読めども、何がどう動いているのか、さっぱりチンプンカンプンです🫠🫠

ここで、開発の奇跡が起きました。
ちょうどこの頃、AIの世界に 「AIコーディングエージェント(リポジトリの複数ファイルを一括で読み込み、開発のコンテキストを理解して対話する技術)」 の大きな波が押し寄せていたのです。
例えば下記のことです。

「この chromium-bidi の何十個ものソースファイル群を、丸ごとすべてAIエージェントに読み込ませて、『このライブラリはどうやってブラウザを直接BiDi化しているのか?』を徹底的にディスカッション(壁打ち)すれば、何かヒントが得られるかもしれない😳」

私はAIを最強の「軍師(対話相手)」として従え、迷宮の解読に乗り出しました。
そしてついに、膨大なTypeScriptのコードの中から、1つの決定的な「心臓部」を突き止めたのです。

【11. 主役はバイナリではなく、1枚のテキストデータだった】

BiDiを動かすための本質は、お堅いコンパイルされたEXE(バイナリ)や、他のスクリプト言語...ではなく、リポジトリに隠されていた mapperTab.js という、1枚の巨大なJavaScriptファイル(テキストデータ)でした。

image.png

Chromeの中に、このJavaScriptをマッパーとして流し込んで常駐させることで、ブラウザ自体を強制的に「BiDiモード」に覚醒させる。その驚くべき手順が、AIとの対話から浮かび上がってきました。

【Excel単体でBiDiを起動する4つのステップ】

image.png

  1. 特権の付与(Target.exposeDevToolsProtocol
    CDPコマンドを使い、Chromeのタブ(マッパー用の特殊なタブ)に、ブラウザを直接操作できる隠された「特権」を付与して解放する。
  2. 通信窓口の確保(Runtime.addBinding
    VBA(ソケットの外側)と、これからタブ内で動かすJavaScript(ブラウザの内側)が、ダイレクトにメッセージをやり取りするための「フック(窓口)」を定義する。
  3. マッパーの注入と起動(Runtime.evaluate
    あの巨大な mapperTab.js のコードを丸ごと文字列としてタブに注入して実行し、ブラウザの内部で「BiDiエンジン」を起動する。
  4. 双方向非同期通信の確立
    VBAからBiDiコマンドを送ると、ブラウザ内でマッパーがそれを処理し、その非同期レスポンスを Runtime.bindingCalled イベントを介して外側に発信。これをVBAのWinSock受信ループで回収する。

「……そうか! 主役がJavaScript(テキストデータ)なら、 Excelのシート上のセルや、VBAの文字列変数の中にそのまま置いておけるじゃないか!!! 🫨」

image.png

なお、VBA内での定数保持は現実的ではないため、テーブルに分割で置いてそこから取り出す手法で採用しました

これが、私の脳内に閃いた電撃のような気づきでした。

私はこの難解な手順を、VBAの生CDP命令として組み立てるようにAIに的確に指示を出しました。
「まずは特権を付与して、次にこのJSを注入して、このバインディングを監視して……」

何度も、何度もキャッチボールを繰り返し、ついにその瞬間が訪れました。

Excelの起動ボタンを押した瞬間。
外部のEXEを一切使わず、Windowsの標準APIだけで、Edgeブラウザの内部にBiDiマッパーが走り出し、世界規格の「WebDriverBiDi」の非同期メッセージが、VBAのPipeを通じて次々と、完璧にExcelのバッファに吸い込まれていったのです。

image.png

「できちゃったよ……本当にExcel単体でBiDiが動いた……🫨🫨」

ネットの検索や、普通のAIの一問一答では絶対にたどり着けなかった「0から1のブレイクスルー」。
「 chromium-bidi の生ソースコードの参照」と、「自分のひらめき・執念の設計指示」、そして「AIエージェントの圧倒的な翻訳・対話力」が、奇跡の三位一体となって、VBAの歴史を塗り替える扉を開けた瞬間でした。
これまでの開発の中で、最も「AIを最強の相棒として活かしきった」と、魂が震えるほどの達成感を感じた瞬間でした。

これにより、私のツールは、
「現在広く使われているCDP(Chromeの仕様)」
「未来の世界標準であるBiDi(W3Cの仕様)」
のどちらの未来に世界が転がったとしても、100%の極限性能を発揮して戦い抜ける、 VBA史上最強の「ハイブリッド・キメラ自動化エンジン」 として完成したのです。

WebDriver.exe はどうしてるのか?

Excelルートなら、mapperTab.jsをセルに配置でしたがじゃ~WebDriver.exeはどうしてるか?
試しに、バイナリエディターでmapperTab.jsに含まれる文字列で検索したら...

image.png

exe に文字列定数あるいは配列に、ハードコーディングされてました😂

言語も環境(バイナリ、Node.js、Excelマクロ)も全く違うのに、ブラウザの裏口(CDP)を開けて翻訳機を忍ばせるという本質的なアプローチは、見事に共通した瞬間でした😳


【12. v1.7.x ~ 1.8.x 系統・コア刷新】SPAを制覇する、CDPパラメータの「全載せ」とVBAの底力

これまでのバージョンアップは、言うなれば「便利な機能の継ぎ足し」でした。本家ライブラリの偉大な面影(土台)を保ちつつ、周りに飾り付けをしているような状態です。
しかし、この頃になると、私のCDPやPipeに対する理解度は、当初とは比べ物にならないほど深くなっていました。

低レイヤの仕様が頭に入ってきた今、改めてこれまでのコアコードを見つめ直すと、本家ライブラリの「とりあえず動くようにしただけ」の簡素な記述が、どうしても目立ち、気になり始めました。

「もっとCDP本来の、圧倒的な潜在能力を解き放てるはずだ🤠」

まず着手したのが、自動化の勝敗を分ける最重要機能、JavaScript(Runtime.evaluate / functionDeclaration)の実行オプションの拡張でした。

最近のReactやVueなどのSPA(シングルページアプリケーション)が支配するモダンなWebサイトにおいて、ブラウザの内部状況に最も精通しているのは、やはりJavaScript自身です。
そのため、CDPの仕様書には、JavaScriptを実行するためのAPIに膨大な「制御用パラメータ」が用意されていました。

image.png

しかし、本家の実装は、ただ「JavaScriptのコード(文字列)」を1つ送るだけの、非常にシンプルな設計になっていました。これでは宝の持ち腐れです🥺

image.png

私はCDPのドキュメントを貪り読み、実務で絶対に役立つ強力なパラメータを片っ端からピックアップし、すべて実装コードに叩き込みました。

  • objectId:特定のDOM要素などのオブジェクトを直接基準にしてJavaScriptを実行する
  • objectArgumentsobjectId の関数に対して、VBA側から引数を安全に引き渡す
  • returnByValueTrue にすることで、JavaScriptが返してきた複雑なオブジェクトを、VBA側でパース可能な具体的な「値(JSON構造)」として受け取れる。
  • awaitPromiseTrue にすると、JavaScript内の非同期処理(Promiseの解決など)が終わるまでVBA側を待機させ、最終的な結果をスマートに同期的に受け取れる。
  • userGestureTrue にすることで、そのJS実行を「人間が手動でクリックやキーボード操作を行った結果」であるとChromeに偽装して認識させる。
  • allowUnsafeEvalBlockedByCSPTrue にすると、Webサイト側がCSP(コンテンツセキュリティポリシー)で外部スクリプトの実行をガチガチに禁止している環境であっても、それを完全に無視してすべてのJavaScriptの実行を強制許可する。

これらをすべて満載したため、VBAのメソッドの引数定義は、改行マーク(, _)がこれでもかと並ぶ凄まじい弾幕のようなシグネチャになりました😂

image.png

これにより、戻ってきたデータが数値だろうが、テキストだろうが、あるいはパースされたDictionaryやCollectionであっても、シームレスにVBA側で受け取れるようにしたのです。

万が一、JavaScriptの実行中にエラー(例外)が発生した場合、VBAの標準エラー値オブジェクトである CVErr(xlErrValue) を返して即座にエラーを報告させ、別途用意したプロパティに例外のスタックトレースの詳細をきれいに記録する仕組みを構築しました。VBAが持つ「Variantの柔軟性」と「エラー値オブジェクト」という、泥臭くも強力な足腰(言語仕様)をフルに引き出した設計です。

これで、どんな高度なJavaScript操作もVBAから自由自在に実行できる、無敵のJS実行環境が完成しました🤠

image.png


【13. テセウスの船:不自然な変数の駆逐と「objectId方式」への大手術】

コア刷新の波は、ツール全体の最大の長所である「要素(Element)操作クラス」にも及びました。

本家ライブラリには、XPathなどで検索した要素を基準にしてクリックや入力をしたり、さらにその要素の下層にある子要素を検索したりする、非常にありがたい要素操作用の「Classオブジェクト(Elementクラス)」がすでに備わっていました。

しかし、その「検索した要素(Element)」の保持方法に、CDPを深く理解した今だからこそ、どうしても許せないまどろっこしさがありました。

本家は、取得した要素(例:ID要素)を以下のように管理していたのです。

Public Function getElementByID(strID As String) As CDPElement
   'Get a new unique varID
    Dim varID As String: varID = newVarID	' ランダムな文字列

   'Get the object and assign to the varID in the browser
    Dim varJS As String: Dim result As String
    varJS = varID & " = document.getElementById(""" & strID & """)"	'VBA側で一時的なランダムな変数名を生成し、グローバル(window)に保存させていた
    result = jsEval(varJS)

   'Return result
    Set getElementByID = New CDPElement
    getElementByID.init Me, varIDs, varID, varJS, result

End Function

Private Function newVarID() As String
    Dim newID As String

   'Init the ID collection
    If varIDs Is Nothing Then Set varIDs = New Scripting.Dictionary

    Do: newID = "varID" & Format(Rnd * 100000, "000000")
    Loop Until Not varIDs.Exists(newID)
    varIDs.Add newID, newID

   'Return the new varID
    newVarID = newID

End Function

ブラウザ側のグローバル空間(window)に、VBA側がランダムな変数名を勝手に生やして要素を代入し、VBA側のDictionaryでその変数名を管理する。
当初は動けばいいと思っていましたが、CDPに精通した今となっては、あまりにも不格好で、不自然なアプローチに思えました🙃

さらに、この window グローバル空間に、不自然なグローバル変数が大量に生成されていく挙動」 は、近年高度化しているセキュリティ・スクレイピング検知システム(ボット検知)にとって、「一発でプログラムによる自動操作だと見破られて、通信を遮断(ブロック)される」という最大のリスクでもありました🫠

「本物のブラウザ自動操作は、window を1文字も汚すことなく、ブラウザの裏側のメモリだけで静かに行われるべきだ🤠」

解決策は明確でした。CDPが標準で提供している objectId 方式に、要素管理のすべてを移行することです 。

image.png

要素を取得した際、Chromeが内部メモリで管理している一意のオブジェクト識別子(objectId)を受け取り、VBA側はそのIDだけを静かに保持して操作を指示する。

しかし、この要素操作クラスの根底を書き換えるのは、ツールのすべての要素操作系メソッドを一度分解して組み立て直すような、途方もない大手術を意味していました。手作業でやれば、どこかで必ず破綻します。

ここで再び、私は「AIコーディングエージェント」という相棒のメスを借りました。

「この要素操作クラスの変数管理システムを、本家のインターフェース(使いやすさ)は1ミリも変えずに、内部をすべてCDP標準の objectId 方式に美しく移管して、リファクタリングして」

AIは、この私のひらめきと緻密な設計指示を完ぺきに理解し、クラスファイルの最深部にある何百行ものコードを、驚くほど美しく、クリーンな形へと一気に大手術してくれました。

外見(使い慣れた顔)は全く同じなのに、中身は完全に最新のクリーンな設計に入れ替わっている。
まさに、歴史的なパラドックスである 「テセウスの船」 のように、私の自動化エンジンは、本家ライブラリの面影を残したまま、中身は全く別の「完全ステルス仕様の最先端クラス」へと生まれ変わったのです🤠


【14. 複数タブ×非同期の壁】「反復横跳び」を拒む、プログラマの矜持】

「CDPの同期・非同期実行」「非同期イベントのキャッチ」「要素操作のobjectId化」――。
ブラウザ自動化のための基礎体力は、もう十分に整った。当時の私は、そう自負していました。

あの日、社内の上司から、とある「現場の悲鳴」を相談されるまでは。

🫅「ねぇ、ちょっと相談なんだけど、今社内Webから毎月請求書データを手動でダウンロードしてExcelに転記してるんだよね。これ、マクロで自動化できない?」
🦊「(お、CDPエンジンの実戦テストに丁度いい実験台が……!)できますよ!」
🫅「本当に!? 助かるよ〜。はい、これがログイン情報ね。5アカウント分あるから本当に毎月大変なんだよね……よろしく!」
🦊「(5アカウント分か。よし、1アカウントずつ順番に処理するんじゃ時間がかかる。ここは自慢のCDP非同期実行をフルに活かして、5つ一斉にブラウザを走らせる『並行処理』といこうか!)」

自信満々でコードを書き、いざ動かしてみたその瞬間。
……システムは一瞬で、無残な大混乱(ハングアップ)を起こしました。

🦊「あれ……動かない。データが混ざり合って、エラーになる……まずい……」

ブラウザの自動化処理は、大きく分けて以下の4つのマトリクスに分類されます。

単体タブ 複数タブ
同期
当然、最も簡単で完璧に動く

複数のタブを順番に切り替えながら処理する。実態は1つずつしか動いていない「反復横跳び」のようなもの。遅いけれど、問題なく動く。
非同期
コマンドを投げっぱなしにし、コマンドIDをDictionaryに退避して管理する。これも問題なく動く。

今回激突した、絶望の壁

5つのタブを同時に非同期で走らせた瞬間、ネットワークから怒涛のように届く「ページの読み込み完了(Page.loadEventFired)」や「CDPレスポンス」のイベント群。
シングルスレッドのVBA側のメモリは、「このイベントは、今5つのうちのどのタブ宛てに届いたものなんだ!?」というパズルの迷宮に陥り、完全に大パニックを起こして破綻してしまったのです⛓️‍💥

「安全に、1アカウントずつ順番に処理する『反復横跳び(同期)』に逃げようか……🥲」

妥協の選択肢が一瞬、頭をよぎりました。しかし、私のプログラマとしての矜持が、それを頑なに拒みました。

「画面が順番に変わっていくんじゃなくて、5つの画面が一斉に、同時に変わっていくあの美しい景色が見たいんだ🥹」

どうすればVBAで、この「イベントの交通整理」ができるのか。
悶々と悩み抜いた末、私の脳裏の引き出しから、VBA標準でありながらこれまで一度も実戦で使ったことのない、あの「眠れる巨人」のような機能が呼び覚まされました。

―― EventRaiseEvent、そして WithEvents です。


【15. 劇的ビフォーアフター:1人1役の「責務分離」大手術】

それまでは、サンプルコードを見ても「ふーん、VBAにもイベントを発信する機能があるんだな😗」程度にしか思わず、おまじないのように読み飛ばしていた機能でした。

image.png

しかし、低レイヤのパズルを解いてきた今の私には、この機能が全く異なる「最強の配線図」に見えました🤩

「コアクラスが受信したすべての生イベントを、一度 RaiseEvent で一斉に上流に発信(ブロードキャスト)する。そして、各タブを管理する個別のクラス側で WithEvents を使って待ち受け、自分宛てのセッションIDのイベントだけをフィルターして引き取れば……すべての交通整理が完璧にできるんじゃないか!?🫨」

ひらめきと同時に、お腹の底が冷たくなるような事実に気づきました。

「これをやるということは、クラス設計を文字通り根底から『全解体』して、組み直さなければならない、劇的ビフォーアフターの大改造になるぞ……🫥」

それまでのクラス構成は、本家ライブラリの設計をそのまま引き継いだ、以下のようなものでした。

  • CDPCore.cls:Pipeなどの接続制御を行うコア
  • CDPbrowser.cls:ブラウザの起動と、「個々のタブの制御」を1人2役でやっている(これが諸悪の根源であり、イベントが混ざる最大の原因でした)
  • CDPElement.cls:要素操作の専門クラス

イベント駆動モデル(WithEvents)を美しく、かつ完璧に配線するためには、クラス同士の関係を「1人1役」にする必要がありました。ソフトウェア設計における基本原則、「単一責任の原則(責務分離)」 です。

1から手作業でプロシージャを切り分け、配線し直すのは、気が遠くなるような作業です。
しかし、私には「AIコーディングエージェント」という、手先が器用で、指示通りに動く世界一優秀な「大工(相棒)」がいました。

私は映画監督のように、AIにクラスの解体と再配置の指示を出しました。

「ブラウザを管理する CDPBrowser.cls から、個々のタブを管理する CDPContext.cls を独立させて、1人1役にする。そして、コアから発信された RaiseEvent を、各 CDPContextWithEvents で確実にキャッチして仕分けるための配線(ルーティング)を組んで」

これまでの小さな手直しとは異なり、この大手術には1週間以上の激しいデバッグと、AIとの幾度もの作戦会議を要しました🫠
ですが、苦労は最高の形で報われました🥹

メジャーバージョンアップとなる Version 2.0.0 の誕生。
そして、ついに夢にまで見た 「非同期実行 × 複数タブ」の完全なる克服 です。

image.png

Excelの起動ボタンをポチッと押した、あの瞬間。
5つのブラウザ画面(Edge)が一斉に起動し、一斉にログインページを開き、1ミリの混線もフリーズも起こすことなく、それぞれが独立した意志を持ったかのように、同時に、爆速で請求書データを回収していく。

「動いた……😭本当に、あのExcel VBAだけで、ブラウザがシンクロして同時に動いている……!🥺」

PCの画面上で一斉にEdgeのページが切り替わっていくその圧倒的に美しい景色を眺めながら、私は言葉を失うほどの深い感動に包まれていました。


【16. 「本当に??」に応える証明:Puppeteerの最深部と、VBAのシンクロ】

前述のリリース文にはしれっとこういう記述があります。

世界中で使われているPlaywrightやPuppeteerといったモダンなWeb自動化の「コアエンジン」がやっているのと同じ設計思想

「それはさすがに、お大げさな表現ではないか?🤔」

そう思う方も多いでしょう。そこで、Googleが開発するPuppeteerの実際の最深部コードと、今回私がビルドしたVBAコードを、鏡のように照らし合わせて証明しましょう。

例えば、Puppeteerのソースコードを this.emit で検索すると、以下のような処理がヒットします。

// Puppeteer (Connection.ts) 内のイベント発信処理
this.emit(CDPSessionEvent.SessionDetached, session);
this.emit(CDPSessionEvent.Disconnected, undefined);

これは、Chromeから「デバッグセッションが切れた」「切断された」という生パケットが届いた際、Node.jsの emit(イベント発信)を使って上流に仕分ける処理です。

対して、私がVBA側で実装した、Chromeからの切断等のイベントを仕分けるためのプロシージャ(一部抜粋)がこちらです。

Private Sub PipeCore_CDPBrowserEvent(methodName As String, RawJson As String)
    Const FromProcedureName As String = ThisClassName & ".PipeCore_CDPBrowserEvent"


    '------------------ 1. この`Context`のSessionステータスの更新を行います ------------------
    Dim tmp As BiDiCDPJson
    Select Case methodName
        '`SessionID`が切れたが、`targetId`は生存
        Case "Target.detachedFromTarget"
            '1-1. 自分の`SessionID`か確認し、そうであれば、フラグ更新
            If BiDiCDPJson.Parse(RawJson).NodeKey("params").StringKey("sessionId") = brTab.sessionID Then ControlStatus.detachedFromTarget = True: printMsg WARN_, "Your session has expired. You need to renew it.", FromProcedureName

        Case "Target.targetDestroyed"
            '1-1. 自分の`targetId`か確認し、そうであれば、フラグ更新
            If BiDiCDPJson.Parse(RawJson).NodeKey("params").StringKey("targetId") = brTab.targetID Then ControlStatus.targetDestroyed = True: printMsg ERROR_, "The tab has been closed. This Class object is no longer usable. Please discard it.", FromProcedureName

言語や思想の違いで、ロジックは完全同一ではないが、イベント駆動といった思想は一致してるではないでしょうか?

Chrome(デバッグポート)から発信される "Target.detachedFromTarget""Target.targetDestroyed" といった生のイベントメソッド名をキャッチし、自分宛て(SessionID / TargetID)のメッセージであるかを確認して状態を同期していく。

まさに、Puppeteerが emit で行っている状態管理と、1ミリのブレもなく完全に同じ論理でパケットをさばいていることがお分かりいただけるかと思います🥹


【17. 進化の第二波:8年の沈黙を破る「JSONパーサー」の世代交代】

イベント駆動モデル(WithEvents)への大改修を成功させ、非同期複数タブの並行処理を手に入れたのも束の間、システム全体をさらに別次元の安定性へと導く「第2の革命」が訪れました🤩

VBAでWebAPIやWeb通信を少しでも扱ったことがある開発者なら、間違いなく一度はお世話になっている伝説的なライブラリがあります。
それが、VBA-JSON です。

VBAでJSONデータを操作する際、これほど扱いやすい標準的なライブラリはありません。しかし、このライブラリは8年以上前から完全に更新がストップしていました。

image.png

当時としては画期的なシステムでしたが、CDPやBiDiのような「ミリ秒単位で巨大なパケットを連打でやり取りする最先端の自動化」を強いると、徐々にこの伝説的ライブラリが持つ、設計上の致命的な弱点が浮き彫りになっていったのです😣

その弱点とは、「要らない情報まで、すべてを一度にDictionaryやCollectionへ変換しようとする設計」 でした。

VBA-JSON は、届いたJSON文字列のすべてを再帰処理で解析し、VBAの DictionaryCollection オブジェクトをメモリ上に山ほど生成して、そこに値を詰め替えるという仕組み(完全コピー方式)をとっています。

普通の小さなAPIを叩く程度なら問題ありません。しかし、この自動化エンジンで以下の極限状態を迎えたとき、このパーサーは一瞬で悲鳴を上げて沈没しました。

  • スクリーンショットを連打したとき:数MBもの巨大なBase64画像文字列(JSON)をパースしようとした瞬間、メモリがパンクしてExcelが完全にフリーズする。
  • 高度なJavaScriptの実行(returnByValue := True)時:JavaScript側から返されたネスト(階層)の深い複雑なオブジェクトをパースしようとした瞬間、再帰処理の深さにVBAのスタック領域が耐えきれず、 「スタック領域不足(実行時エラー 28)」 を吐いて強制終了する。

「せっかく通信コアをWithEventsで爆速のイベント駆動にしたのに、パケットの中身を解析する最深部のJSONパーサーが、8年前のボトルネックのまま引きずられている……🫠」

長らくこれにメスを入れる手段がなく、私は渋々この古いパーサーを使い続けるしかありませんでした。

しかし、イベント駆動への大手術を終えてからわずか数日後。私はGitHubの海で、今の私のシステムと「奇跡的」と言っていいほど相性抜群の、最新のJSONパーサーと出会ってしまったのです。

それが、vbacollective-json でした😳

image.png


【18. ゼロコピー・インデックス方式:VBAに舞い降りたモダン言語の知恵】

vbacollective-json の設計思想は、これまでのVBAの常識を覆すほどに革新的でした。
一言で言えば、「ゼロコピー・インデックス方式」 です。

従来の VBA-JSON は、パズルのピースをすべて手作業で別のお皿(DictionaryやCollection)に載せ替えて整理していました。
これに対し、vbacollective-json は、元のJSON文字列(メモリ空間)を1文字もコピー(解体)しません。

代わりに、「どの文字(何バイト目)から、何バイト目までに、どんなデータがあるか」という位置情報(インデックス)というデータベースではお馴染みの方式で、最初に超高速でスキャンして構築します。
そして、VBA側が「このキーの値が欲しい」と要求したその瞬間に、そのインデックス(案内図)を参照して、元のJSON文字列から直接その位置の文字列だけを切り取って(あるいは値に変換して)返します。

この「ゼロコピー・インデックス方式」こそが、Go言語やRust、C++といった現代の超高速なWebサーバーで使われている最新のJSONパーサーが採用している、最も無駄のない仕組みそのものです🥹

この「Dictionary/Collectionへの完全なコピー・変換コスト」を完全にゼロにしたことで、このパーサーは異次元の処理速度を叩き出しました🫨

  • スクリーンショットの巨大な画像データが降ってこようが、コピーを一切行わないため、メモリを1MBも消費せずに一瞬で処理が完了する。
  • どれだけネストの深いJavaScriptの実行結果が降ってこようが、再帰処理によるスタックメモリの消費が極小なため、絶対に「スタック領域不足(エラー28)」で落ちることがない。

「……なんてタイミングのいい発見なんだ。これこそ、私のCDP/BiDiエンジンに欠けていた最後のパズルのピースだ!🥳」

私は迷わず、この最新のパーサーへと最深部のJSON解析エンジンをリプレースしました。

すべてのボトルネックが焼き尽くされ、名実ともにVBAにおける最高峰の安定性と極限のスピードを手に入れた。この運命的なパーサーの刷新は、私の自動化エンジンに、二度と壊れない完全なる命を吹き込んでくれたのです。

image.png

【19. 最後の壁:Port(WebSocket)の魔力と、オーバースペックのロマン】

長らく、私の自動化エンジンは --remote-debugging-pipe(パイプ通信)一本に絞り、その処理を極限まで研ぎ澄ましてきました⚙️
しかし、開発の過程で目にした、とあるGoogleの最新ニュースが、私のプログラマとしての探求心に火をつけました。

それは、「起動済みの既存のブラウザセッションをそのままCDPで再利用してデバッグする(Chrome DevTools MCPサーバーなどの登場)」 という画期的な機能の発表でした。

既存のブラウザを途中から乗っ取って操る。この魅力的な挙動は、新しくプロセスを立ち上げて直接パイプを繋ぐ --remote-debugging-pipe では、物理的に絶対に不可能な芸芸でした。
さらに調べると、ポート(WebSocket)経由でのCDPデバッグは、恐ろしいほどの可能性(ロマン)を秘めていることがわかりました🫨

正直に言えば、実務の業務自動化においては、完全なオーバースペック(過剰性能)です😵‍💫
特定の自動化専用ブラウザをパイプで動かせば、それだけで実務の99%は満たされます🙂

しかし、「知ってしまった以上、実装せずにはいられない」のが、プログラマの、ハッカーの性です😂

「Excel単体から、ポート(WebSockets)もパイプ(Pipe)も、どちらでも完全制御できるようにしてやる!🔥」

再び、私の手によって、受信コアエンジンの「大工事(劇的ビフォーアフター)」が始まりました。


【20. 二つの異なるルールと、引退したはずの「ADODB.Stream」の覚醒】

同じCDP制御であっても、使うモードが違えば、データの境界線(受信ルール)は180度異なります👮

  • --remote-debugging-pipe(パイプ):境界線は、メッセージの末尾にある vbNullChar(0x00) というシンプルなマーク
  • --remote-debugging-port(ポート):境界線は、ヘッダーに書かれたバイト数(WebSocketの仕様ルール準拠)

これまで極限までパイプに最適化してきたため、私の受信ロジックは完全にパイプ寄りになっていました。「スマホやWebView2もExcelから操りたい🥺」という欲望のためには、この2大ルールを1つの受信フローに美しく合流させなければなりません。

最大の難所は、どちらのAPI(ReadFileWinHttpWebSocketReceive)からも、データが「Byte配列(バイナリ)」として届くという点でした。

VBAは、文字列(String)であれば、標準の MidSplit といった極めて高速な文字列操作ステートメントが用意されています。しかし、VBAには「Byte配列」をそのまま Mid のように位置指定で切り出したり、動的に高速に後ろへ結合(アペンド)したりするスマートなステートメントが存在しません。

「VBAに、Byte配列のままで、まるで Mid のようにインデックス位置を指定して高速アペンドやシークができる仕組みはないか?😣」

メモリの断片化を招く ReDim Preserve を避け、どうにかして高速にバイナリを蓄積したい――そう頭を抱えた私の脳裏に、ある閃きが走りました。

位置を指定してシークする ➡ .Position プロパティ🙂
バイナリモード ➡ Type = adTypeBinary 😳
この機能を持つオブジェクト ➡ CreateObject("ADODB.Stream") !?🫨

「そうか! かつて私が『名前と用途(UTF-8文字コード変換)が不一致でダサい』と嫌煙して、低レイヤAPI(WideCharToMultiByte)へ切り替えたあの ADODB.Stream は……文字コードの変換器なんかじゃない。その名の通り、Windowsのメモリ上で最高速度で動作する、極めて優秀な『インメモリ・バイナリバッファ(メモリストリーム)』そのものだったんだ!」😂

おまじないの仮面を剥ぎ取った瞬間、ADODB.Stream の真の姿と凄まじい有能さに、私は大いなる覚醒を遂げました。
ドキュメントを調べると、ポインタ(カーソル)操作の基本となる .Position によるシーク や、受信データの終端をピタッとマークする .SetEOS メソッド など、低レイヤのメモリ管理をC++層で代行してくれるメソッドが、最初から完備されていたのです🥹

私は、この ADODB.Stream を「緩衝地帯(バッファ)」として中間に挟み、ポートとパイプの境界線を取り払う、驚くほど美しい共通設計を考案しました。

【Port と Pipe を美しく統合するバッファ・ハック】

【--remote-debugging-pipe】   ───> (そのままアペンド) ──────────────┐
                                                             │ ──> 【ADODB.Stream】 ──> (一括UTF-8変換) ──> (Midステートメントによる高速カーソル処理)
【--remote-debugging-port】   ───> (IsFINなら、末尾に0x00を付加) ───┘
  1. Pipe版のデータを受信した場合
    そのまま ADODB.StreamWrite で流し込んで蓄積する。
  2. Port版(WebSocket)のデータを受信した場合
    ヘッダーサイズをパースし、メッセージの終わりであることを確認後、コッソリと vbNullChar(0x00) をその場でドッキングさせてからADODB.StreamWrite で流し込んで蓄積する。
  3. 共通の後続処理
    両者のパケットが溜まった ADODB.Stream から、一括でUTF-8テキストに超高速変換。変換後のテキストに対し、VBAが誇る伝家の宝刀 Mid ステートメントを使って高速にカーソル(cursor)操作を走らせ、vbNullChar を区切り文字として1件ずつ安全にパースして取り出す。

「Port版のデータを、その場で『Pipe版のフォーマット(末尾Null文字)』に翻訳してストリームに流し込む」🤠

このハックを思いついたことで、私は複雑なWebSocket専用の難解な受信パースロジックを新規に書き加えることなく、既存の高速なPipe受信フローと 「ほぼ完全なコピペレベルの一元化(共通化)」 に成功したのです。

AIと対談しながら、ミリ秒のカーソル(cursor)管理のデバッグに手こずり、泥臭く調整を重ねた末、ついに「Port」と「Pipe」が完全に合流した、最強の受信エンジンが稼働可能な状態に整いました🥳


【21. Microsoftの甘い罠:WinHttpWebSocketReceiveの欺瞞とハルシネーション】

話はいよいよ終盤――自動化エンジンの心臓部である通信方式を決定づける、最も過酷で最もドラマチックな「WinSock(生ソケット)への完全移行」のお話です。

VBAでWebSocketを制御しようと調べてみると、ほぼ確実に、Windows標準の winhttp.dll が提供する「WinHttpWebSocket○○」系のAPIを使った手法にたどり着きます。当初、私も「これを使えば一番楽に作れるはずだ😃」と信じて疑いませんでした。
しかし、いざ実装を進めると、すぐに奇妙な「違和感」に襲われました。

「おかしいな……データをソケットから消さずに覗き見する WinHttpWebSocketPeek みたいな命令は、APIの一覧にないのかな?……いくら探しても、本当にないのか?🫥」

嫌な予感は的中しました。かつてパイプ(Pipe)でも ReadFile をそのまま直接呼ぶと「データがない時にVBAのスレッド全体がフリーズする」という問題がありましたが、この WinHttpWebSocketReceive も、データがない状態で呼び出すと、スレッドを巻き込んで完全にフリーズ(ブロッキング)してしまうのです😣

「やっぱりフリーズするじゃないか🙃。これじゃあ使い物にならない。データがあるかどうかを事前に調べるAPIは……」

ここで、AIの「ポンコツぶり(ハルシネーション)」が顔を出しました。
相談する私に対して、AIは自信満々にこう答えました。

🤖「WinHttpSetTimeoutsWinHttpSetOption で、WebSocketの受信タイムアウトを設定すれば、フリーズせずにエラーで戻ってきますよ!」

若干の強引さは感じつつも、「Webの世界は非同期が基本だし、タイムアウト設定が実質のPeek(データ有無確認)の役割なのかな🙄」と思い、言われた通りにパラメータを試しました。しかし、数ミリ秒、数十ミリ秒と設定を変えてテストしても、ことごとくタイムアウトは無視され、Excelはフリーズし続けました。

「AIの嘘つきめ!🙃 なぜ効かないんだ?」

怒りを覚えてMicrosoftの公式ドキュメント(MSDN)を血眼で読み漁り、衝撃的な事実にたどり着きました。

  • WebSocket接続に対して、タイムアウトを設定するパラメータそのものが存在しない
  • さらに、WinHttpSetOption の設定一覧のどこを読んでも、「これらのオプションはWebSocketには適用されません(例外です)」という記述が一切書かれていない

そう、悪いのはAIではありませんでした。Microsoftのドキュメントの「圧倒的な不親切さ(書かれていない暗黙の仕様)」 だったのです。ネット上の表面的なドキュメントを学習しただけのAIは、この「不親切な仕様」を読み切れず、嘘のタイムアウト設定を吐き出し続けていたのでした🙃

「VBAからの利用なんて、Microsoftは最初から1ミリも想定していないんだ🥲」

冷たい現実を突きつけられました。
実は、この WinHttpWebSocket 系列のAPIは、データがない状態でのフリーズを防ぐために、基本的に 「コールバック運用(非同期のイベント駆動)」 を大前提として設計されていたのです。

しかし、VBAにおける「コールバック(AddressOf)」の相性は最悪です。デバッグ中にマクロを途中で止めたり、実行時エラーが発生したりした瞬間に、メモリの保護違反を起こしてExcelプロセスごと一瞬で即死(強制終了)します🫥

さらに絶望的なことに、このWinHTTPのWebSocketコールバックは、通常のシンプルなコールバックではなく、「ワーカースレッド(VBAの実行スレッドとは完全に物理的に異なるスレッド)」から直接割り込んでくる仕様でした📨
VBAとは全く異なるスレッドから、VBAのメモリ空間に対して「データが届いたぞ!」と非同期に直接コールバックが割り込んでくる。この仕組みは、シングルスレッドで動くVBAのメモリ空間にとって「猛毒」であり、Excelの強制終了を避けることは技術的に今のところ、100%不可能です。

AIと対話し、VBAの最深部にあるマニアックなメモリ保護裏テクニック(仮想メモリの割り当てやフック、サンクなど)を総動員して試行錯誤を繰り返しましたが、結局、このWinHTTPによるクラッシュ問題は最後まで解決できませんでした。

「WinHTTPは、VBAにとって完全な『死のルート』だ。これを使うことは絶対にできない😑」


【22. 生ソケットへの逆襲:ネットワーク版PipeのWinSock】

WinHTTPの欺瞞に満ちた甘い罠を前にして、私はついに、かつて名前だけは知っていた最古参のネットワークAPI 「WinSock」 の存在を思い出しました。

WinSockは、実態としては「Pipeにネットワークの送受信機能を足したような、生ソケットの土台API群」です。
しかし、WinSock自体はただの「ネットワーク版データの通り道(土管)」を開くだけの存在です。WebSocketの仕様やフォーマットなど、高水準なルールは一切解決してくれません。
ネット上を見渡しても、「独自の終端目印を決めてPC間でTCP通信をするサンプルがあるくらいで、ルールは完全に自由自在な、むき出しの土管でした。

つまり、WinSockを使ってEdgeと対話するためには、「WebSocket仕様書(RFC 6455)に書かれたパケットフレームのルールを、自分自身で解き明かして、VBAで1からパース処理を実装しなければならない」 という、絶望的な難易度の壁が立ちはだかることを意味していました😶‍🌫️

だからこそ、私はなんとかWinHTTPの高級APIで頑張ろうと足掻いていたのです😔しかし、コールバックによるExcel即死クラッシュを回避できない以上、私に残された道は、この生ソケット(WinSock)を力ずくで握る以外にありませんでした。

「待てよ……。WebSocketの仕様書は『世界標準の厳格なルール』として、ガチガチにネット上に公開されている。これだけルールがハッキリ決まっているのなら……🥺」

私はある強力な閃きに思い至りました。

「『RFC 6455に100%準拠した、WebSocketのフレーム構築やマスク処理(XOR)を行うVBAコード』を、AIに丸ごと作らせてしまえばいいんじゃないか?」

AIにとって、このように「世界共通の仕様書(ルール)がすでに100%完成しているコード」を出力させることは、最も得意な分野(真骨頂)です。
指示を出すと、AIはまるであらかじめ「注文用の完成部品」が倉庫に保管されていたかのように、実用的なWebSocketフレームの構築・マスク処理用プロシージャ群を一瞬で、完璧に生成して私に引き渡してくれました🥹

「なんだ……! 必要なパーツ(プロシージャ)は、一瞬でAIが吐き出してくれるじゃないか! あとは、このバラバラのパーツを、VBAのフローの中でどう美しく組み立てるか(設計)だけだ!🤠」


【23. 最後の難関:カメレオンのように姿を変える「ヘッダー処理」】

無事にAIからWebSocketフォーマットのための部品を揃え、大喜びしたのも束の間。私は最後の、そして最も脳みそを引きちぎられるような難関に直面しました🫠

それが、「WebSocket特有の受信解析ロジック(ヘッダー処理)の組み立て」 でした。

WebSocketから送られてくるデータは、先頭のタグ情報(ヘッダー)が、データの大きさに合わせて「小箱(2バイト)」「中箱(4バイト)」「大箱(10バイト)」と、カメレオンのように動的にサイズを変化させます。
さらに、ネットワークの混雑や高負荷によって、データがパケット単位で「細切れ(断片化)」になって届きます🫥

image.png

「今ソケットに届いているデータは、ヘッダーの情報を読み込んでいる最中なのか? それとも、すでにヘッダーを読み終わって、残りのデータ(ペイロード)を回収している最中なのか?」

この、目まぐるしく変化する受信データの「状態(ステート)」を、フリーズさせることなく1バイトの過不足もなく管理するパズル。

この最後のパズルを解き明かすために、私はAIを相手に、これまでのVBA開発の中で最も激しく、最も知的なキャッチボール(格闘)を始めることになるのです。

ここで、初期のツギハギモンスター時代に、私を救ってくれたあの救世主、@kabkabkab のWebSocket受信ロジックを思い出しました。
先人は、以下のような独自の構造体を定義して、受信状態をコントロールしようと格闘していました。

【先人の知恵】@kabkabkab 氏が定義した通信状態管理構造体

''Websocket受信に使用(自作構造体)
Private Type response
    Buffer(4095) As Byte    '実際に受け取る箱
    BufferLength As Long    '箱の残量
    ReceiveBytes As Long    'WebSocketから受け取ったバイト数
    Status As Long          'WINHTTP_WEB_SOCKET_BUFFER_TYPE
    CurrentPointer As Long  '現在の書き込み位置
    result As Long          'API自体の実行結果
    collect As Collection   '断片データ蓄積用
End Type

「そうか! この『構造体を使って現在の通信のステートをきっちり記録しながら進む』という先人のアプローチを、生WinSockとPort/Pipe統合のための新しい設計にカスタマイズすれば、絶対に綺麗な状態で管理できるはずだ!🤠」

私は、先人の素晴らしい知恵に心から感謝しつつ、このアプローチをさらに進化させる決意をしました。

目指すべきは、単なるコードの移植ではありません。
関数の内側に重い While などのブロッキングループを絶対に置かず、データの受信を1回ごとにサクッと終わらせ、すべての受信処理の主導権を最外周のイベントループ(TakeWebSocket 側)へと美しく委ねる、完全な非同期対応のパラメーター管理です。

AIと何度もキャッチボールを重ね、脳みそを千切るような試行錯誤の末にひねり出した、私だけの独自の通信状態管理構造体がこれです。

【独自進化】VBA-CDPエンジンの受信ステート管理構造体

'WebSocketのFrame単位を管理する独自構造体
Private Type WebSocketReceiveFrameManage
    PayloadLength   As Long     'Chromium側から実際に送られるサイズ
    CurrentCursor   As Long     '`PayloadLength`のうち、どこまで書き込んだ?
    ValidTags       As Boolean  '有効なタグ中か?ペイロードデータ自体の欠け対策です
    isFIN           As Boolean  'FINフラグ
    opcode          As Long     '「これはテキストだよ(1)」「バイナリだよ(2)」「切断要求だよ(8)」などのデータ種類
    FrameHeaderSize As Long     '今回のフレームヘッダーサイズ
End Type
Private m_WebSocketFrame As WebSocketReceiveFrameManage
Private WebSocketReceiveStream  As Object                       'WebSocketのペイロードバイトストリームを蓄積するオブジェクト
  • CurrentCursor:現在データの何バイト目までを吸い上げたか。これがあるから、パケットがどんなにちぎれて届いても、1バイトのズレもなく続きからデータを結合できる。
  • ValidTags:今、有効なフレームの最中(ヘッダーを読み込み終わっている状態)か。このフラグと、引数の ByRef による減算ハックを組み合わせることで、余分にバッファを読み込んでスレッドをフリーズさせるバグを100%防ぐ。
  • WebSocketReceiveStreamそして、かつて文字コード変換器として嫌煙した、あの ADODB.Stream これを、インメモリのバイナリデータを高速にアペンドするためのメモリストリームとして、クラスの最深部にピタッと配置しました。

先人たちが残してくれた技術の灯火をバトンとして受け取り、自分の理想のフローを信じて、AIと共に極限までスマートに磨き上げたこの構造体🥹

こうしてついに...🤩

Excel単体(PowerShellなし、アドインなし)から、Androidブラウザも操るオーバースペックツールが誕生しました🥳

Androidブラウザ制御

WinSock.gif

厳密には、Google公式の開発者用ツールAndroid Debug Bridge (ADB)が必要です。
なぜなら、VBAのWinSock(TCP/IP)はWindowsPCのネットワーク内にしかアクセスできないため、USBケーブルという物理的な壁を越えてスマホ内部のポートに直接繋ぐことはできないからです。

そのため、PC側にGoogle公式の adb を配置し、コマンドプロンプトで以下の転送(フォワーディング)コマンドを一度だけ実行しておく必要があります。

adb forward tcp:9222 localabstract:chrome_devtools_remote

テスト用端末ではないことを証明するため別途、普段遊んでるアプリを起動させています。

このコマンドを実行すると、スマホ内のChromeのデバッグ窓口が、パソコン側のローカルホスト(127.0.0.1:9222)にまるで魔法のようにマッピングされます。
一度この「物理的な橋」さえ架けてしまえば、あとはExcel側には一切の追加アドイン(XLL)やPowerShell、Seleniumなどの外部ライブラリを1ミリも導入することなく、私たちが磨き上げたVBAのWinSockクラスだけで、スマホの中のChromeやEdgeが本当にそのまま爆速で走り出します🤩

今目の前のブラウザ制御

WinSock2.gif

もし、特に何もデバッグソフト動かしてないのに、「リモート デバッグを許可しますか?」が出たら迷わず、キャンセルして下さい。
動画の通り、「許可」をしたら最後、乗っ取られたように、ブラウザが制御されます😱

WebView2制御

test.avif

オーバースペックですね🤣


【24. 終章:Excel単体のゲリラ、未来の海へ】

IEオブジェクトという魔法を失ったあの日から、半年。

私のExcelの中には、

  • Windowsシステム最深部を直接叩いてTCP/Pipe通信を行う「WinSock/ReadFile API」
  • どんな巨大データもOSレベルのC++層で高速蓄積する「ADODB.Stream(バイナリバッファ)」
  • モダン言語の超高速パーサーと同じ思想をVBAに持ち込んだ「vbacollective-json」
  • 複数のタブを一斉にフリーズせず並行制御する、完全な「WithEvents(イベント駆動モデル)」
  • そして、Googleのchromium-bidiを解読し、Excelの中に完全密輸して動かした、次世代の世界標準「WebDriverBiDi」

という、世界中のどこを探しても絶対に存在しない、VBAの限界を遥かに超越した「キメラ自動化エンジン」が、静かに、そして圧倒的な戦闘力を持って息づいています。

「Excelマクロなんて、セルのコピーや事務作業を自動化するだけの古いおもちゃでしょ🙂」

世間はそう言うかもしれません。
しかし、この1ファイルのExcelブック(xlsm)をメールやチャットで1枚ぽんと送るだけで、追加ソフトやドライバ(※Androidブラウザ制御は除く)、アドインのインストールを一切必要とせず、相手のPC上の最新Edgeを、あるいはUSBケーブルの向こうにあるAndroid実機のブラウザを、10タブ同時に非同期で完全にハックして動き出す。

この圧倒的なポータビリティと、100%クリーンな透明性、そして何より「絶対に突然壊れない堅牢さ」は、PlaywrightやPuppeteerがどれだけ逆立ちしても真似できない、Excel唯一無二の、究極の機能美です。

プログラミングの本当の楽しさは、便利なライブラリをただ消費して動かすことではありません。
「これ以上は不自由で何もできない」という制限だらけの砂漠(VBA)の中で、自分の知恵と、AIという最強の相棒を駆使し、規格(プロトコル)をハックして、本来できないはずの最高峰のシステムを1から自分の手で作り上げていく。

あのIEObjectを初めて動かした日の感動を超える、極上の知的な冒険。

この1ファイルに、しかし世界で最も尖ったコードという「最強の相棒」を胸に、私は今日も、厳格な企業セキュリティの砂漠を、涼しい顔で爆走し続けています🤠


【最後に:失われた「魔法の杖」を、あなたの手元に】

もしあなたが今、

  • 「Seleniumを導入したいけれど、社内のセキュリティポリシーで野良EXE(WebDriver)の実行を弾かれてしまう」
  • 「ブラウザが自動アップデートされるたびにマクロが動かなくなって、毎週のようにドライバの入れ替え作業に追われるのに疲れた」
  • 「Power Automateは便利だけれど、動作の遅さや変数の不自由さに、どこか開発者として手足を縛られているようなもどかしさがある」

そんな、お堅いビジネスの砂漠で、自動化の限界に直面して諦めかけているなら……どうか一度、このツールを試してみてください。

ここにあるのは、かつて私たちが愛した「IEオブジェクト」のあの圧倒的な手軽さを、現代のEdge/Chrome(Chromium)を舞台に、そしてPlaywright並みの極限の非同期マルチタスク性能で完全に蘇らせた、完全な「純VBA製ブラウザ制御エンジン」です。

👉 ここからツール(リポジトリ)へアクセスできます

このツールがあなたにもたらす「3つの絶対的な自由」

  1. 完全なインストールフリー & ホワイト仕様
    追加のEXEファイル(WebDriver)、PowerShellスクリプト、サードパーティ製アドイン(XLL/DLL)は1ミリも必要ありません。Excelファイル(xlsm)をメールやチャット、SharePointで1枚ぽんと送るだけで、どんな厳しいセキュリティPCでもそのままクリーンに、スマートに動き出します
  2. 自動アプデによる「突然死」からの解放
    ChromeやEdgeが勝手に自動更新されても、ドライバの入れ替えなどのメンテナンスは一切不要です。W3C標準のデバッグ規格を直接WinSockで叩いているため、10年後でも手元のExcelを開くだけでそのまま動き続けます
  3. 5つの画面が一斉にシンクロして動く「並行非同期処理」
    順番に1つずつポチポチ動く「反復横跳び」の自動化は終わりです。WithEventsを駆使した高度なイベント駆動モデルにより、5アカウント分、10アカウント分のブラウザを一斉に同期させて爆速でデータを回収できます

先人たちが開拓してくれたコンクリートの上に、現代のAIをフル動員して、1から執念で削り出した「魂のVBAコード」です。

ネットワークのパズルを解き明かし、VBA本来の「ByRef」や「ADODB.Stream」という野生のパワーを手なずけたからこそ実現できた、ExcelVBAにおけるひとつの到達点。

ぜひ、あなたの手元で、あの 「Excelから最新ブラウザがキビキビと直結して、自分の意のままに爆走する感動」 を、もう一度体感してみてください🦊

使ってみた感想や、こんな過酷な環境でも動いた!といったフィードバックも、Issueやコメントでお待ちしています!

5
5
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
5
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?