【実務・中級編】【再帰的フォルダ探索】FileSystemObject を用いたサブフォルダ全検索と特定拡張子ファイルの一括処理ロジック – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【再帰的フォルダ探索】FileSystemObject を用いたサブフォルダ全検索と特定拡張子ファイルの一括処理ロジック

開発現場でよくある要求だ。
「指定したルートフォルダ以下にある、すべての階層のサブフォルダをくまなく走査し、特定の拡張子を持つファイルだけを特定のアクション(リネーム、移動、テキスト置換など)で一括処理したい」

この要件に対し、安易にネットの断片的なコードをコピペして実装すると、深い階層でスタックオーバーフローを起こしたり、アクセス権限のエラー(Permission Denied)でスクリプトが突然死したり、巨大なファイル群の処理でメモリリークを引き起こしたりする。

私はこれまで数多くのWindows環境における自動化基盤を構築してきたが、「止まらない、例外をハンドリングできる、そしてリソースを適切に解放する」再帰処理のパターンには、明確な定石がある。

今回は、VBScript(WSH)の `Scripting.FileSystemObject`(FSO)を極限までチューニングし、プロダクション環境にそのまま投入できる堅牢な再帰的フォルダ探索・一括処理ロジックを伝授しよう。

1. なぜ「深さ優先探索(DFS)」と「適切なエラーハンドリング」が必要なのか

フォルダ構造の探索には、大きく分けて「幅優先探索(BFS)」と「深さ優先探索(DFS)」がある。VBScriptでキュー構造を自前で実装するのはコードが冗長化するため、シンプルかつメモリ効率の良い再帰呼び出しによる深さ優先探索(DFS)を採用するのが定石だ。

しかし、VBScriptでの再帰には以下の罠が潜んでいる。

1. アクセス拒否(Error 70: Permission Denied)の頻発
System Volume Information や、権限のないシステムフォルダに突入した瞬間、スクリプトは容赦なくクラッシュする。
2. オブジェクトのライフサイクル管理の欠如
FSOやFolder、Fileコレクションをループ内で不適切に生成・破棄すると、COMコンポーネントのメモリリークを招き、長時間のバッチ処理でOSが重くなる。
3. 文字列結合によるパスの破綻
`FolderPath & “\” & SubFolderName` の単純結合は、スラッシュの重複や不正文字でバグの温床となる。

これらを完全に克服したプロダクションコードを以下に提示する。

2. 実装コード:堅牢な再帰的ファイル一括処理エンジン

以下のコードは、指定したルートフォルダを起点にサブフォルダを再帰的に走査し、指定した拡張子のファイルを安全に処理するテンプレートだ。今回は実務で最も需要が高い「対象ファイルのログ出力(またはパス取得)」を例にするが、ここをリネームや移動のロジックに差し替えるだけで、あらゆる自動化に応用できる。

‘ ==============================================================================
‘ Script Name: RecursiveFileProcessor.vbs
‘ Description: FileSystemObjectを用いた堅牢な再帰的フォルダ探索と一括処理
‘ Author: Chief Architect
‘ ==============================================================================
Option Explicit

‘ — 定数定義 —
Const TARGET_ROOT = “C:\Work\TargetFolder” ‘ 探索対象のルートフォルダ
Const TARGET_EXT = “.txt” ‘ 処理対象の拡張子(ドット含む・大文字小文字無視)
Const LOG_PATH = “C:\Work\process.log” ‘ 実行ログの出力先

‘ メイン処理の実行
Main

Sub Main()
Dim fso, tsLog
Dim startTime
startTime = Timer

‘ FSOのインスタンス生成(スクリプト全体で1つだけ生成し、使い回す)
Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ ログファイルの準備(Unicode対応、上書きモード)
Set tsLog = fso.CreateTextFile(LOG_PATH, True, True)
tsLog.WriteLine “=== 処理開始: ” & Now & ” ===”

‘ ルートフォルダの存在確認
If fso.FolderExists(TARGET_ROOT) Then
‘ 再帰処理の呼び出し
ProcessFolder fso, fso.GetFolder(TARGET_ROOT), tsLog
Else
WScript.Echo “エラー: 指定されたルートフォルダが存在しません。” & vbCrLf & TARGET_ROOT
tsLog.WriteLine “エラー: ルートフォルダが存在しません – ” & TARGET_ROOT
End If

tsLog.WriteLine “=== 処理終了 (経過時間: ” & FormatNumber(Timer – startTime, 2) & “秒) ===”
tsLog.Close

‘ オブジェクトの明示的な解放
Set tsLog = Nothing
Set fso = Nothing

WScript.Echo “一括処理が完了しました。”
End Sub

‘ ==============================================================================
‘ フォルダを再帰的に走査し、ファイルを処理するコアプロシージャ
‘ @param fso FileSystemObject
‘ @param folder 現在走査中のFolderオブジェクト
‘ @param tsLog ログ用TextStreamオブジェクト
‘ ==============================================================================
Sub ProcessFolder(fso, folder, tsLog)
On Error Resume Next ‘ 権限エラー等でスクリプトが停止するのを防ぐ防壁

Dim subFolder, file
Dim colFiles, colSubFolders

‘ 1. 現在のフォルダ内のファイルを処理
Set colFiles = folder.Files
For Each file in colFiles
‘ 拡張子の判定(小文字に統一して比較)
If LCase(fso.GetExtensionName(file.Name)) = LCase(Mid(TARGET_EXT, 2)) Then
‘ ——————————————————————
‘ 【ここに実処理を記述】
‘ 例: ファイルのリネーム、移動、内容の書き換えなど
‘ ——————————————————————
ExecuteFileProcess file, tsLog
End If
Next

‘ 2. サブフォルダを再帰的に探索
Set colSubFolders = folder.SubFolders
For Each subFolder in colSubFolders
‘ エラーが発生した場合(アクセス権限がない等)はスキップして続行
If Err.Number = 0 Then
‘ 再帰呼び出し
ProcessFolder fso, subFolder, tsLog
Else
‘ 権限エラーなどをログに記録してクリア
tsLog.WriteLine “[WARNING] アクセススキップ: ” & subFolder.Path & ” (Error: ” & Err.Description & “)”
Err.Clear
End If
Next

On Error GoTo 0 ‘ エラー監視をデフォルトに戻す
End Sub

‘ ==============================================================================
‘ 個別ファイルのビジネスロジック実行部
‘ @param file 処理対象のFileオブジェクト
‘ @param tsLog ログ用TextStreamオブジェクト
‘ ==============================================================================
Sub ExecuteFileProcess(file, tsLog)
‘ エラーハンドリングを個別に挟むことで、1ファイルの破損が全体に波及しないようにする
On Error Resume Next

‘ 実処理のサンプル:ファイルのフルパスをログに書き出す
tsLog.WriteLine “処理実行: ” & file.Path & ” (サイズ: ” & file.Size & ” bytes)”

‘ — 拡張実装のヒント —
‘ ファイル名のリネーム例:
‘ file.Move file.ParentFolder.Path & “\processed_” & file.Name

‘ テキストファイルの内容置換例:
‘ Dim ts
‘ Set ts = file.OpenAsTextStream(1, -2) ‘ 読み取り専用・システムデフォルト
‘ Dim content: content = ts.ReadAll
‘ ts.Close
‘ content = Replace(content, “旧文字列”, “新文字列”)
‘ Set ts = file.OpenAsTextStream(2, -2) ‘ 書き込み専用
‘ ts.Write content
‘ ts.Close

If Err.Number <> 0 Then
tsLog.WriteLine “[ERROR] ファイル処理失敗: ” & file.Path & ” – ” & Err.Description
Err.Clear
End If

On Error GoTo 0
End Sub

3. チーフアーキテクトが教える、コードの設計思想と勘所

上記のコードが「なぜプロの現場で耐えうるのか」、その設計思想を解説しよう。

① `On Error Resume Next` の局所的かつ厳格な運用

初心者向けのコードでは、スクリプトの先頭に `On Error Resume Next` をベタッと貼り付け、エラーを完全に隠蔽してしまう悪癖が見られる。これはバグの温床だ。
本コードでは、「アクセス権限がないフォルダ」や「使用中のファイル」に遭遇した際に対象を安全にスキップするため、あえてエラーハンドリングを有効にしつつ、`Err.Number = 0` のチェックや `Err.Clear` を挟むことで、予期せぬサイレントバグを防いでいる。

② メモリとパフォーマンスへの配慮

FSOのインスタンス (`CreateObject`) は、ループの内部ではなく、メイン処理で一度だけ生成し、引数として各プロシージャに参照渡し(ByRef)している。VBScriptにおいてCOMオブジェクトの生成・破棄コストは意外とバカにならない。この徹底が、数万ファイルを扱うバッチ処理でのメモリリークを防ぐ。

③ 拡張子の確実な判定

`fso.GetExtensionName(file.Name)` を利用することで、ファイル名に複数のドットが含まれる複雑なケース(例: `archive.tar.gz`)であっても、正確に末尾の拡張子を切り出すことができる。また、`LCase` で大文字小文字の揺らぎ(`.TXT` と `.txt`)を完全に吸収している。

4. 実運用における注意点(インフラ・セキュリティ観点)

  • ネットワークドライブ(UNCパス)の罠

ローカル環境ではなく `\\server\share\folder` のようなネットワークパスに対して本スクリプトを実行する場合、レイテンシやパケットロスによって一時的な切断が発生することがある。可能であれば、処理対象のフォルダをローカル(または同一セグメントの高速ストレージ)に一度同期(RoboCopy等)してからスクリプトを走査させる設計を推奨する。

  • 実行権限の担保

スクリプトを実行するWindowsユーザーアカウントが、対象フォルダツリーのすべてに対して適切な読み取り(および書き込み)権限を持っていることを確認すること。権限不足によるスキップが多発すると、処理漏れが発生してもログに警告が埋もれて気づきにくくなる。

総括

VBScriptはレガシーな言語と揶揄されることもあるが、Windows環境において追加のランタイムインストールなしで動作し、OSの奥深くを直接叩ける極めて強力なツールだ。
今回解説した「堅牢な再帰探索のテンプレート」をマスターすれば、日々の単調なファイル整理やデータ移行、ログ集計などの業務自動化において、二度と「途中で止まる」「バグが追えない」という悪夢に悩まされることはなくなるはずだ。現場の信頼を勝ち取る堅牢な自動化基盤を、ぜひあなたの環境でも構築してほしい。

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