【テクニカル・上級編】VBAの「再帰呼び出し」でフォルダ階層を全探索する:ファイル操作の自動化 – Excel VBA解析バイブル

スポンサーリンク

再帰呼び出しとFileSystemObjectによるフォルダ階層の全探索:VBAの限界領域を突破する極限アプローチ

Excel VBAにおけるファイルシステム操作において、開発者が最も直面する壁の一つが「ネストが未知数であるサブフォルダの網羅的走査」である。

浅い階層であれば `Dir` 関数をループさせることで糊塗できるが、業務システムが生成するデータレイクや、エンドユーザーが勝手に肥大化させた共有フォルダの構造は、容赦なく10段、20段の深さに達する。`Dir` 関数は内部で単一の検索コンテキストしか保持できないため、ネストしたループの中で呼び出すと状態が破壊され、破綻する。

この限界を突破する唯一にして最善の解が、`Scripting.FileSystemObject` (FSO) と「再帰呼び出し(Recursive Call)」の組み合わせだ。

今回は、単に動くだけのコードではない。メモリの枯渇を防ぎ、VBAの脆弱なオブジェクトモデルを完全に統御するための「極限の知見」を共有する。

—

1. 再帰処理の本質とVBAにおける致命的リスク

再帰呼び出しとは、関数が自分自身を呼び出すことで複雑な階層構造を突破するアルゴリズムである。フォルダ探索におけるロジックは極めてシンプルだ。

1. 指定されたフォルダ内の全ファイルに対して処理を実行する。
2. 指定されたフォルダ内の全サブフォルダを取得し、それぞれのサブフォルダに対して自分自身(関数)を再度呼び出す。

しかし、シニアエンジニアが警戒すべきは「コールスタックの肥大化」と「COMオブジェクトのメモリリーク」である。

VBAは、C++やC#のようにモダンなガベージコレクションを持たない。再帰のたびに `New` や `CreateObject` でCOMオブジェクトを生成し、それを適切に解放(`Set … = Nothing`)し忘れると、数千フォルダを走査した瞬間にメモリを食い潰し、Excelごとクラッシュする。あるいは、Windowsの最大パス長(MAX_PATH: 260文字)や、深すぎるネストによる「スタックオーバーフロー」を引き起こす。

このリスクを完全にコントロールした実用コードを以下に示す。

—

2. 実装:メモリ最適化・エラーハンドリング完備の全探索エンジン

以下のコードは、単にファイルを列挙するだけでなく、長パスカバーの概念、進捗のイミディエイト出力、そして厳格なオブジェクト解放を実装したプロダクションレベルのアーキテクチャである。

Option Explicit

‘ 処理したファイルの総数をカウントするグローバル(またはモジュールレベル)変数
Private m_FileCount As Long

Public Sub RunFolderTraversal()
Dim fso As Object
Dim targetPath As String
Dim startTime As Double

startTime = Timer
m_FileCount = 0

‘ 探索の起点となるパス(環境に合わせて変更すること)
targetPath = “C:\DataStorage”

‘ パスが存在するかどうかの事前検証
Set fso = CreateObject(“Scripting.FileSystemObject”)
If Not fso.FolderExists(targetPath) Then
MsgBox “指定されたパスが存在しません: ” & targetPath, vbCritical
Set fso = Nothing
Exit Sub
End If

‘ 画面描画とイベントを停止し、処理速度を極限まで引き上げる
With Application
.ScreenUpdating = False
.Calculation = xlCalculationManual
.EnableEvents = False
End With

On Error GoTo ErrorHandler

Debug.Print “— 探索開始: ” & Now & ” —”

‘ 再帰プロシージャの呼び出し
Call TraverseFolders(fso, fso.GetFolder(targetPath))

Debug.Print “— 探索終了: ” & Now & ” —”
Debug.Print “総処理ファイル数: ” & m_FileCount & ” / 処理時間: ” & Format(Timer – startTime, “0.00秒”)

MsgBox “処理が完了しました。\n総ファイル数: ” & m_FileCount, vbInformation

CleanUp:
‘ アプリケーション設定の復元
With Application
.ScreenUpdating = True
.Calculation = xlCalculationAutomatic
.EnableEvents = True
End With
Set fso = Nothing
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub

Private Sub TraverseFolders(ByRef fso As Object, ByRef currentFolder As Object)
Dim subFolder As Object
Dim fileItem As Object
Dim colFiles As Object
Dim colSubFolders As Object

On Error GoTo FolderError

‘ 【重要】コレクションを一度変数に受けてループを回す
‘ FSOのプロパティ(Files, SubFolders)を直接For Eachの評価式に置くと、
‘ COMのラップ層でメモリリークや参照ズレを引き起こすリスクがある。
Set colFiles = currentFolder.Files

For Each fileItem In colFiles
‘ ————————————————–
‘ ここに実際のファイル処理ロジックを記述する
‘ 例: 拡張子が “.xlsx” のファイルのみを対象にする等
‘ ————————————————–
If LCase(fso.GetExtensionName(fileItem.Name)) = “xlsx” Then
m_FileCount = m_FileCount + 1
‘ デバッグ出力(実運用ではシートへの書き込みや別処理に置き換え)
‘ Debug.Print fileItem.Path
End If
Next fileItem

‘ サブフォルダの走査(再帰呼び出し)
Set colSubFolders = currentFolder.SubFolders

For Each subFolder In colSubFolders
‘ 再帰的に自分自身を呼び出す
Call TraverseFolders(fso, subFolder)
Next subFolder

FolderError:
‘ 権限エラー(アクセス拒否)等が発生した階層はスキップして継続する耐障害設計
If Err.Number <> 0 Then
Debug.Print “[警告] アクセススキップまたはエラー: ” & currentFolder.Path & ” (” & Err.Description & “)”
Err.Clear
End If

‘ 明示的なオブジェクト解放(VBAのガベージコレクションの弱点を補う)
Set fileItem = Nothing
Set subFolder = Nothing
Set colFiles = Nothing
Set colSubFolders = Nothing
End Sub

—

3. チーフアーキテクトが解説する「コードの急所」

上記のコードには、レガシーかつ脆弱なVBA環境を生き抜くための実践的な設計思想が凝縮されている。

① コレクションのローカル変数化と明示的解放

`For Each fileItem In currentFolder.Files` と直接書くことは、一見するとスマートに見える。しかし、背後でCOMが動的に生成するIEnumVARIANTインターフェースの参照カウントがVBAのランタイムによって正確にデクリメントされないケースが、長時間のループにおいて発生する。
これを防ぐため、一度 `colFiles` というオブジェクト変数に受けてからイテレートし、プロシージャの抜け際(またはエラー時)に必ず `Set … = Nothing` で参照を切断している。この一手間が、数万ファイルを処理する際のメモリリークを完全に防ぐ。

② 権限エラー(Access Denied)への耐性

ネットワーク共有フォルダやシステムフォルダ(`System Volume Information` など)を巡回する際、VBAは容赦なく「実行時エラー ’70’: 書き込み権限がありません」あるいは「アクセスが拒否されました」でクラッシュする。
本アーキテクチャでは、プロシージャ内に `On Error GoTo FolderError` を配置し、権限を持たないフォルダに遭遇した場合はログ(イミディエイトウインドウ)に退避しつつ `Err.Clear` で処理を続行する「フェイルセーフ(Fail-Safe)設計」を取り入れている。全自動バッチにおいて、たった一つのアクセス権のないフォルダで全体の処理が停止することは絶対に許されない。

③ パフォーマンスの極限チューニング

`Application.ScreenUpdating` や `Calculation` を手動(`Manual`)に切り替えるのは基本中の基本だが、ファイルシステムにアクセスするループ内では、Excelの再計算や画面描画のオーバーヘッドが致命傷になる。バックグラウンドで純粋なファイルパスの解析・処理に専念させ、最後に一括してUIを復元することで、処理速度を最大数倍へと跳ね上げることができる。

—

4. さらなる高みへ:VBAの限界を超える選択肢

もし、探索するファイル数が「10万件」を超え、かつネットワーク越しの遅延がボトルネックになるような極限の環境であるならば、VBA単体でのループ処理には物理的な限界が訪れる。

その場合、VBAはあくまで「UIとオーケストレーション(司令塔)」に徹し、実際のファイル検索ロジックを PowerShellスクリプト や C#製COMコンポーネント (DLL) に委譲するというシステム間連携のアーキテクチャを採用すべきだ。

例えば、VBAからWScript.Shell経由で高速なPowerShellの `Get-ChildItem` を非同期実行させ、その結果のCSVをExcelにインポートする手法の方が、純粋なVBAの再帰よりも圧倒的に速いケースが存在する。

しかし、社内のセキュリティポリシーで外部スクリプトの実行が厳禁とされているレガシー環境や、単一の `.xlsm` ファイルだけで完結させなければならない保守性の制約がある現場において、今回紹介した 「FSO × 再帰処理 × 厳格なメモリ管理」 のパターンは、今なお最強のカードである。

VBAの特性を熟知し、そのリソース管理の癖を掌握した者だけが、巨大なファイル群を従わせる真の自動化を成し遂げることができる。

タイトルとURLをコピーしました