2
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?

Excel VBAでZIPを解凍する「真の最適解」:レガシーなShell.Applicationから最新のtar.exe、そして禁断のCDPハックまで

2
Posted at

業務効率化や自動化(RPA)を実装する際、避けては通れないのが 「ZIPファイルの圧縮・解凍」 です。

「VBAでZIPを解凍する」のようなのをネットで検索すると、数年前に書かれた古いコードばかりが上位を占領しています。しかし、現代のWindows 10/11環境においては、それらはすでに最適な選択肢ではありません。

image.png

今回は、お馴染みの王道ルートから、セキュリティソフトの網をすり抜ける最新のOS標準コマンド、そして裏技的な「純VBA」や「CDP(ブラウザ)ハック」まで、VBAにおけるZIP解凍のすべてを一挙に整理します。


1. 最もお手軽だけど「非推奨」:Shell.Application × CopyHere

おそらくネット検索で最も多く見かける、定番中の定番の戦法です。
外部ライブラリを必要とせず、Windows標準の Shell32.Folder を使ってVBA内で完結するため、一見するとベストに思えます。

' お馴染みのレガシーコード
With CreateObject("Shell.Application")
    .Namespace(destFolder).CopyHere .Namespace(zipFile).items
End With

⚠️ デメリットと隠された真実

  • 非同期の罠: CopyHere は、OSのバージョンによっては非同期で動く等、解凍が完了する前にVBAが次のコードに進んでしまい、ファイル参照エラーになります。そのため、Sleep などで「解凍が終わるまで待つ」泥臭いループ処理が必須になります🫠
  • 本質は「人間用の操作」: このメソッドはプログラム用の命令ではなく、エクスプローラー上での 「マウスのドラッグ&ドロップ」を偽装しているだけ にすぎない仕様です
  • Microsoftも非推奨: Microsoftの公式ドキュメント等でも、自動化スクリプトでのこの方法の使用は推奨されていません

image.png

オリジナルのリンクは404につき、WebArchiveリンクを使用してます

2. 機能はモダン、でもセキュリティソフトに殺される:PowerShell

WScript.Shell を利用し、PowerShellの Expand-Archive コマンドを呼び出して解凍する、近年増えている戦法です。
Google検索Topはまさにこれです。

Dim wsh As Object
Set wsh = CreateObject("WScript.Shell")
' PowerShellをバックグラウンドで実行
wsh.Run "powershell -Command ""Expand-Archive -Path '" & zipFile & "' -DestinationPath '" & destFolder & "' -Force""", 0, True

引数に 0(非表示)と True(同期実行)を渡せば、VBAは解凍が終わるまで自動で待機してくれるため、コードは非常にシンプルになります。

「誤検知」問題

これには 「アンチウイルスソフトによる誤検知」 という致命的な壁があります。
「Excel(VBA)が、裏側で見えないようにPowerShellを起動する」という挙動は、昨今のランサムウェアなどのマルウェアと全く同じ挙動 です。
そのため、特に市販のセキュリティソフト(ノートンなど)を導入している環境では、実行した瞬間に「危険なウイルス」と誤認され、 Excelプロセスごと強制終了 されます。

私がこの記事を書こうとしたのも、この処理自体には悪意がないのに市販のセキュリティソフトに即検知されてマクロが中断されるという理不尽な仕打ちを受けてしまったからです🥲

なお、標準の「Windows セキュリティ」なら、比較的起こりづらいので、その場合は有効かもしれません🤔


3. 高性能、でも環境依存:7-Zip などの外部ツール

最近の社内PCにプリインストールされていることが増えてきた 7-Zipによる外部解凍ソフト(.exe)を、VBAの Shell から叩く手法です。

Dim wsh As Object
Set wsh = CreateObject("WScript.Shell")
wsh.Run "C:\Program Files\7-Zip\7z.exe x -o " & 解凍先フォルダパス & " -y " & 圧縮ファイルパス, 0, True

パフォーマンスは最強で、パスワード保護や大容量ファイルも難なくこなします。

当然ですが...
「配布先PCに7-Zipがインストールされていること」 が大前提となります。
「マクロブックを1個渡せば、誰のPCでも動く」というVBA最大の強みが死んでしまうため、社内配布用ツールとしては採用しにくいのが現状です。
個人用としては1つの選択肢でもあるかも?


4. 【本命】現代のWindowsにおける「真の最適解」:tar.exe

「事前準備は不要」「セキュリティソフトにも怒られない」「1行で同期処理ができる」という、現代のWindows 10/11におけるベストアンサーがこれです。

実は、Windows 10 (Build 17063 / 2018年春) 以降、C:\Windows\System32 には tar.exe が標準搭載されています。

「tar」という名前ですが、中身は世界標準の強力な解凍ライブラリ(bsdtar)が使われているため、.zip ファイルもネイティブで高速解凍できます。

' ★ 現代のVBAにおける、最も美しく安全な解凍コード
Public Sub SmartUnzip(ByVal zipFile As String, ByVal destFolder As String)
    Dim wsh As Object
    Set wsh = CreateObject("WScript.Shell")
    
    ' tar.exeを使って、同期・非表示でZIPを解凍する
    ' -xf : アーカイブを展開する
    ' -C  : 展開先のフォルダを指定する
    Dim cmd As String
    cmd = "tar -xf """ & zipFile & """ -C """ & destFolder & """"
    
    ' 「0」(非表示)「True」(同期実行:終わるまでVBAを待機させる)
    wsh.Run cmd, 0, True
End Sub

' 呼び出し例
Private Sub Test()
    Call SmartUnzip(Environ("UserProfile") & "\Downloads\DB.Browser.for.SQLite-v3.13.1-win64.zip", Environ("UserProfile") & "\Downloads\tarexeからの結果")
End Sub

✨ なぜこれが最強なのか?

  1. OS標準搭載: Windows 10/11なら、追加インストールなしで100%動きます
  2. 完璧な同期処理: True を指定するだけで、VBAは解凍が終わるまで自動で待機します。泥臭い Sleep ループは一切不要です
  3. セキュリティソフトに怒られない: tar.exe はただの圧縮解凍ツール(非スクリプトエンジン)であるため、セキュリティソフトにハッカー扱いされてExcelごと殺されるリスクがほぼありません

ここからはロマン溢れる技術の深淵(アドバンスド領域)です。
実務では先ほどのtar.exeを推奨しますが、気になる方は是非読み進めてみてください


5. 【ロマン枠】純VBAだけで解凍エンジンを自作:Excel-ZipTools

外部の .exe を1ミリも起動させたくない、あるいはMac版のExcelでも動かしたい場合の究極の選択肢です。

💡 仕組み

VBAのコードだけで、ZIPのデータ構造(ヘッダー)を解析し、「DEFLATE / Inflate圧縮アルゴリズム(ハフマン符号の復号)」を自力で計算して解凍するという、オーパーツのようなライブラリです。

' 呼び出し例
Private Sub ExcelZipToolsDemo()
    Dim zip As New ExcelZIP: zip.Init Environ("UserProfile") & "\Downloads\twinBASIC_IDE_BETA_983.zip"
    Dim Z() As Byte
    Dim s As String

    Debug.Print "Archive has " & zip.Count & " files"
    Dim List
    List = zip.GetFileNames         'ファイルリストが配列で見れる
    zip.ReadData "twinBASIC.exe", Z '第一引数にZIP内相対パスを指定してあげれば、第二引数に解凍済みバイト配列を得れる。これをファイル保存すればOK!
End Sub

image.png

メリット
.exe を一切起動しないため、鉄壁のセキュリティ環境でも100%安全です。なお、厳密にはDeclareは使用してます。
また、VBAのコードだけで完結しているため、Mac版のExcelでもそのまま動きます。 さらに、ファイルとして書き出さず、 メモリ(Byte配列)の中で直接データを解凍するといった、超スマートな処理も可能です

デメリット
純VBAでビット演算のループを回すため、数ギガバイトあるような極めて巨大なファイルの解凍には、ネイティブC++の tar.exe と比べて時間がかかりますが、ほとんどの実務ケースでは軽量ZIPがほとんどなので、影響は低いでしょう

6. 【一石二鳥】Webスクレイピングのついでにブラウザに解凍させる:zip.js × CDP

近年、ChromeやEdgeを直接コントロールする CDP(Chrome DevTools Protocol) による自動化が盛んです。
VBAからCDP制御できるツールもいくつか登場してます。

もしあなたがCDPを使って自動化システムを作っているなら、 「ブラウザのJavaScriptエンジン(V8)を、VBAの解凍用コプロセッサとして逆利用する」 という、最高にクリエイティブな裏技が使えます。

今回のDemoに使うZIP解凍のJavaScriptは下記を用います。

💡 仕組み

VBA側で読み込んだZIPファイルを、CDP経由でEdge/Chromeのメモリ上に流し込み、ブラウザ内で zip.js(またはブラウザ標準の DecompressionStream)を動かして超高速で解凍。その結果(Base64等)をVBAに送り返す手法です。

Demoコード

Sub Webブラウザ操作でZIPテスト()
    '設定シートに基づくブラウザ立ち上げ
    Dim ZIPテスト As CDPContext: Set ZIPテスト = 設定シートからのCDP起動ForTab


    ' 1. zip.js (UMD版) を動的にロードするJSを実行
    Dim injectCode As String
    injectCode = "var script = document.createElement('script');" & _
                 "script.src = 'https://cdn.jsdelivr.net/npm/@zip.js/zip.js@2.7.34/dist/zip-no-worker.min.js';" & _
                 "document.head.appendChild(script);"

    
    ZIPテスト.jsEval injectCode

    ' 2. ライブラリの読み込み完了を待機
    Dim isLoaded
    Do
        isLoaded = ZIPテスト.jsEval("typeof zip !== 'undefined'")
        If Not IsError(isLoaded) Then Exit Do
        Application.wait (Now + TimeValue("0:00:01"))
    Loop

    ' 3. ローカルのZIPをBase64化する
    Dim b64ZipData As String, ZipDataBin() As Byte
    Dim CharConv As New CharacterCodeConversion
    ZipDataBin = CharConv.BytesFromSavedFile(Environ("UserProfile") & "\Downloads", "twinBASIC_IDE_BETA_983.zip")
    b64ZipData = WebCrypto.Encode(ZipDataBin, edfBase64, efNoFolding)

    ' 4. 即時実行関数 (IIFE) のJSコードを組み立て
    Dim jsCode As String
    jsCode = _
        "(async () => {" & _
        "  const zipBytes = Uint8Array.from(atob('" & b64ZipData & "'), c => c.charCodeAt(0));" & _
        "  const reader = new zip.ZipReader(new zip.Uint8ArrayReader(zipBytes));" & _
        "  const entries = await reader.getEntries();" & _
        "  const results = [];" & _
        "  for (const entry of entries) {" & _
        "    if (!entry.directory) {" & _
        "      const fileBytes = await entry.getData(new zip.Uint8ArrayWriter());" & _
        "      let binary = '';" & _
        "      const len = fileBytes.byteLength;" & _
        "      for (let i = 0; i < len; i++) {" & _
        "        binary += String.fromCharCode(fileBytes[i]);" & _
        "      }" & _
        "      results.push({" & _
        "        filename: entry.filename," & _
        "        base64: btoa(binary)" & _
        "      });" & _
        "    }" & _
        "  }" & _
        "  await reader.close();" & _
        "  return results;" & _
        "})()"

    ' 5. CDP経由で実行し、解凍された全データを一発で受け取る!
    ' ※ awaitPromise:=True でPromiseの解決を待ち、
    '   returnByValue:=True で中身のデータを直接取得します。
    Dim resCDP As Collection
    Set resCDP = ZIPテスト.jsEval(strString:=jsCode, awaitPromise:=True, returnByValue:=True)
    ZIPテスト.InheritanceCDPBrowser.quit

    '6.展開
    Dim ベース展開先 As String
    ベース展開先 = Environ("UserProfile") & "\Downloads\JavaScriptからの結果"
    
    Dim tmp, i As Long
    Debug.Print resCDP.Count; "個のファイルを展開します..."
    For Each tmp In resCDP
        '保存先用意
        Dim 相対パス As String, ファイル名 As String, 保存先フルフォルダパス As String
        相対パス = "\" & tmp("filename")                    'JavaScript結果から、相対パスを取って...
        ファイル名 = ファイルとフォルダパス分離(相対パス)   'うまいぐあいに、ファイル名とフォルダパスを分離させて...
        保存先フルフォルダパス = ベース展開先 & 相対パス    '保存先フォルダの絶対パスを作って...
        If Len(Dir(保存先フルフォルダパス, vbDirectory)) = 0 Then
            Dim ResultCode As Long
            ResultCode = SHCreateDirectoryEx(0&, 保存先フルフォルダパス, 0&)    'その保存先がなければ作る

            '失敗時は終了
            If ResultCode Then
                MsgBox 保存先フルフォルダパス & vbCrLf & "上記のフォルダ作成にて、エラーが発生しました。" & vbCrLf & "> " & WinApiError.GetMessage(ResultCode), vbCritical, "ErrorCode: " & ResultCode
                Exit Sub
            End If
        End If

        '実際に保存
        Dim 展開後B64 As String, 展開後Bin() As Byte
        展開後B64 = tmp("base64")
        
        '空文字=0バイト判定
        If LenB(展開後B64) > 0 Then 展開後Bin = WebCrypto.Decode(展開後B64, edfBase64) Else 展開後Bin = vbNullString
        CharConv.BytesToSaveFile 展開後Bin, 保存先フルフォルダパス, ファイル名

        DoEvents
        i = i + 1
        Debug.Print i; "つめのファイルを展開完了: "; ファイル名
    Next

End Sub

Function ファイルとフォルダパス分離(ByRef Path As String) As String
    '1. 最も右にある「\」を取得
    Dim EndPos As Long
    EndPos = InStrRev(Path, "/")

    '2. 存在有無に応じた判定
    If EndPos = 0 Then
        'ない場合は、引数をファイル名として返す
        ファイルとフォルダパス分離 = Path

        'クリア
        Path = vbNullString
    Else
        ファイルとフォルダパス分離 = Mid(Path, EndPos + 1, Len(Path) - EndPos)
        Path = Replace(Mid(Path, 1, EndPos - 1), "/", "\")
    End If
End Function

さぁ、一気になげぇ~コードが来ました🫠🫠大丈夫です。1つ1つ解説します🤠

🛠️ 処理フロー解説

Step 1:ブラウザの初期化と「zip.js」の動的注入

  • CDP経由でChrome/Edgeを起動し、CDNから zip.js の軽量版(WebWorkerを使用しない zip-no-worker.min.js)を動的に <script> タグとしてDOMに注入します
  • zip-no-worker版を採用することで、ブラウザのスレッド制限を気にせず、シングルタブ内でシンプルかつ高速に動作させることができます

Step 2:ライブラリ読み込み完了の確実な待機

  • JavaScriptを注入した直後は、まだライブラリのロードが完了していません
  • そこで、VBA側から typeof zip !== 'undefined' という判定用JSを定期実行(ポーリング)し、JavaScript側で zip オブジェクトが使える状態になるのを安全に待機します

Step 3:ローカルZIPファイルの「Base64テキスト化」

  • 解凍したいローカルの .zip ファイルを、VBA側でバイナリ(Byte配列)として読み込みます
  • それを、CDPで送信(通信)できるように、VBAのヘルパー関数(WebCrypto.Encode)を使ってBase64テキストへと変換します

Step 4:ブラウザで実行する非同期JavaScript(IIFE)の組み立て

  • ブラウザ側(V8エンジン)で超高速解凍を行うための、 即時実行関数(IIFE:Immediately Invoked Function Expression) をVBAの文字列結合で組み立てます
  • このJavaScriptコードは、以下の仕事をブラウザ内で完結させます
    1. VBAから送られてきたBase64データを Uint8Array(バイナリ)に復元
    2. zip.jsZipReader)を使い、メモリ上でZIPファイルを解凍
    3. 各ファイルの「相対パス(フォルダ階層)」と「ファイルデータ(Base64)」のペアのリスト(JSON配列)を作成して返す

Step 5:CDPからJSを実行し、解凍結果を一発で回収

  • 組み立てたJSコードを jsEval で実行します
  • ここで awaitPromise:=True(非同期Promiseの解決を待つ)と returnByValue:=True(JSON構造をそのままVBAに返す)の2つのパラメータを指定することで、ブラウザ側の解凍処理の完了を完全に同期(待機)させ、解凍された全ファイルのデータをVBAの Collection / Dictionary として一発で受け取ります。
  • (※受け取りが完了した時点で、役目を終えたブラウザは安全に閉じられます:quit

Step 6:Windows APIを駆使した「フォルダ階層の復元」と保存

  • 受け取ったファイル情報のコレクションをVBAのループで処理します
  • ZIP内のフォルダ階層(/folder/sub/ など)を再現するため、VBAの MkDir ではなく、Windows APIの SHCreateDirectoryEx を使用し、深い階層のフォルダを1発で自動生成します
  • 最後に、ファイルごとのBase64データをデコードして元のバイナリに戻し、ローカルに書き出して展開完了です

ZIPDemo.gif

メリット

  • 100%プロセスセーフ: 外部の解凍用 .exetarなど)を一切起動しないため、アンチウイルスの誤検知(クラッシュ)を完全に回避できます。
  • インメモリ処理の美しさ: ファイルの解凍はすべて「ブラウザのメモリ(RAM)の中」で行われます。余計な一時ファイルを作成してハードディスクを汚すことがありません。
  • VBAでのJSON直受け: returnByValue:=True によって、JS側のObject配列が、VBAの Collection / Dictionary としてそのまま返ってくるため、面倒な正規表現や外部のJSONパース処理が不要です。

デメリット
前述のExcel-ZipToolsと重なりますが、Jsonパース処理は純VBAで回すため、数ギガバイトあるような極めて巨大なファイルの解凍には、ネイティブC++の tar.exe と比べて時間がかかりますが、ほとんどの実務ケースでは軽量ZIPがほとんどなので、影響は低いでしょう

📊 最終比較:私たちはどのルートを選ぶべきか?

手法 難易度 同期(待機) セキュリティ耐性 総合評価
Shell.Application 初級 ✕ (ループが必要) 🐢 引退を推奨
PowerShell 初級 ✕ (誤検知リスク大) ⚠️ 社内配布に不向き
7-Zip 中級 🛠️ 環境が整っていればアリ
tar.exe (OS標準) 初級 🏆 現代のベストアンサー
Excel-ZipTools 上級 🍏 Mac対応・ロマン枠
zip.js (CDP連携) 最上級 🌐 スクレイピング連携の極致

📝 まとめ

長年、VBA開発者を苦しめてきた「ZIP解凍時の非同期フリーズ問題」や「セキュリティソフトの誤検知問題」。

もし、あなたの開発・実行環境が Windows 10 / 11 であるなら、今すぐ古い Shell.Application をゴミ箱に捨てて、tar.exe を使った1行の同期解凍コードに書き換えましょう。それだけで、システムの安定性は劇的に向上します。

また、Mac環境やインメモリでのハックを追求するなら Excel-ZipTools、ブラウザ自動化のついでなら zip.js を使ったCDPハックなど、現代のVBAには、先人たちが開拓した素晴らしい選択肢が用意されています。

道具の仕組みと歴史を理解し、あなたのシステムに最適な「解凍ルート」を選択してみてください!🦊

2
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
2
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?