走査の極致:FSOにおける「ノイズ」の排除と、堅牢なファイルシステム・アーキテクチャの構築
ファイルシステムの走査(Traversal)を、単なる「フォルダ内のファイルを列挙する作業」と考えているエンジニアがいるならば、その認識は今すぐ捨ててもらいたい。
実務、とりわけ大規模なレガシー環境や、多人数が共有するネットワークドライブ上で動作する自動化スクリプトにおいて、ファイル走査は「地雷原の歩行」に等しい。Office製品が生成する一時ファイル(`~$`で始まるオーナーファイル)、システムが生成する隠しファイル、そしてアクセス権限のないディレクトリ。これらを適切にフィルタリングできなければ、スクリプトは無慈悲に `Permission Denied (800A0046)` を吐いて停止し、それまでの処理時間を無に帰すことになる。
本稿では、`FileSystemObject` (FSO) を単なる道具としてではなく、プロフェッショナルな「フレームワーク」として扱うための、フィルタリング設計とメモリ管理、そしてエラー回避の極限知見を伝授する。
1. なぜ「単純なループ」は実務で通用しないのか
多くの入門書では、以下のようなコードが紹介される。
' 典型的な「素人」のコード
For Each objFile In objFolder.Files
' 何のフィルタリングもなく、全ファイルを処理しようとする
ProcessFile objFile.Path
Next
このコードは、開発環境のクリーンなフォルダでは完璧に動作する。しかし、運用環境に投入した瞬間、以下の事象に直面する。
1. Officeの一時ファイルによる衝突: ExcelやWordが開かれている際、同フォルダに `~$example.xlsx` のような一時ファイルが生成される。これらは属性が特殊であったり、ファイルロックがかかっていたりするため、読み取りや移動の操作を行うと即座に実行時エラーを引き起こす。
2. システム・隠しファイルの混入: `desktop.ini` や `$RECYCLE.BIN` 内のファイルなど、処理対象外のファイルが混入し、ロジックを汚染する。
3. 権限エラーによる全停止: 走査中に、現在のユーザーにアクセス権のないサブフォルダに遭遇した瞬間、スクリプト全体がクラッシュする。
シニアエンジニアに求められるのは、「正常系を回すこと」ではなく、「異常系をいかに静かに、かつ確実に無視するか」である。
2. 防御的フィルタリング・フレームワークの設計
堅牢な走査を実現するには、以下の3層のフィルタリング・レイヤーを実装すべきである。
Layer 1: 拡張子・プレフィックス・フィルタ
ファイル名そのものに基づくフィルタリング。`~$`(Office一時ファイル)や `.tmp`、`.log` などの不要な拡張子を、処理の入り口で遮断する。
Layer 2: ファイル属性(Attributes)フィルタ
Windowsのファイル属性(Hidden, System, ReadOnly)に基づくフィルタリング。`FSO.GetFile(path).Attributes` を参照し、システムファイルや隠しファイルを排除する。
Layer 3: 例外ハンドリング・レイヤー
`On Error Resume Next` を多用するのは素人のすることだ。エラーが発生した際に「何が起きたか」を捕捉し、特定の権限エラー(アクセス拒否)のみをスキップして、致命的なエラー(メモリ不足やディスク故障)は適切にハンドリングする設計が求められる。
3. 実装:プロフェッショナル・レジリエント・トラバーサー
以下に、実務に耐えうる高度な再帰的走査関数を示す。このコードは、メモリリークを防ぐためのオブジェクト解放と、徹底したフィルタリングを組み込んでいる。
' ==========================================================================
' Professional File Traversal Framework
' Author: Legendary Chief Architect
' Description: 高度なフィルタリングとメモリ管理を備えた再帰的ファイル走査
' ==========================================================================
Option Explicit
Dim fso, targetPath
Set fso = CreateObject("Scripting.FileSystemObject")
' ターゲットパスを指定(実環境に合わせて変更)
targetPath = "C:\Your\Target\Directory"
If fso.FolderExists(targetPath) Then
RecursiveTraverse fso.GetFolder(targetPath)
Else
WScript.Echo "Error: Target directory does not exist."
End If
' オブジェクトの明示的解放
Set fso = Nothing
' --------------------------------------------------------------------------
' Sub: RecursiveTraverse
' 目的: フォルダを再帰的に走査し、条件に合致するファイルのみを処理する
' --------------------------------------------------------------------------
Sub RecursiveTraverse(objCurrentFolder)
Dim objSubFolder, objFile
Dim strName, strExt, nAttr
' --- Layer 1 & 2: サブフォルダの走査 ---
On Error Resume Next ' フォルダへのアクセス権限エラー対策
For Each objSubFolder In objCurrentFolder.SubFolders
If Err.Number = 0 Then
' 隠しフォルダやシステムフォルダをスキップするロジック
nAttr = objSubFolder.Attributes
If (nAttr And 2) = 0 And (nAttr And 4) = 0 Then ' 2=Hidden, 4=System
RecursiveTraverse objSubFolder
End If
Else
' アクセス拒否等のエラーはログに記録して継続(無視して進むのがプロの判断)
' WScript.Echo "Skip Folder: " & objSubFolder.Path & " (Error: " & Err.Description & ")"
Err.Clear
End If
' 重要: サブフォルダオブジェクトの参照を明示的にクリアし、メモリリークを防ぐ
Set objSubFolder = Nothing
Next
On Error GoTo 0
' --- Layer 1, 2 & 3: ファイルの走査 ---
On Error Resume Next
For Each objFile In objCurrentFolder.Files
If Err.Number = 0 Then
strName = objFile.Name
strExt = fso.GetExtensionName(strName)
nAttr = objFile.Attributes
' フィルタリング・ロジックの集約
If IsValidFile(strName, strExt, nAttr) Then
ProcessFile objFile
End If
Else
Err.Clear
End If
' 重要: ファイルオブジェクトの参照を明示的にクリア
Set objFile = Nothing
Next
On Error GoTo 0
End Sub
' --------------------------------------------------------------------------
' Function: IsValidFile
' 目的: ファイルが処理対象として適切かどうかを判定する
' --------------------------------------------------------------------------
Function IsValidFile(strName, strExt, nAttr)
Dim isTemp, isHidden, isSystem
' 1. プレフィックス・フィルタ (Officeの一時ファイル ~$ を排除)
isTemp = (Left(strName, 2) = "~$")
' 2. 拡張子フィルタ (.tmp, .log などを排除)
' 必要に応じて条件を追加
If strExt = "tmp" Or strExt = "log" Then isTemp = True
' 3. 属性フィルタ (Hidden=2, System=4)
isHidden = (nAttr And 2) <> 0
isSystem = (nAttr And 4) <> 0
' すべての条件をクリアした場合のみ True
If Not isTemp And Not isHidden And Not isSystem Then
IsValidFile = True
Else
IsValidFile = False
End If
End Function
' --------------------------------------------------------------------------
' Sub: ProcessFile
' 目的: フィルタリングを通過したファイルに対する実際の業務ロジック
' --------------------------------------------------------------------------
Sub ProcessFile(objFile)
' ここに実際の処理(コピー、移動、データ抽出など)を記述する
' 例: WScript.Echo "Processing: " & objFile.Path
End Sub
4. 伝説のアーキテクトが教える「極限の最適化」
このコードをさらに高みへと引き上げるための、設計思想を補足する。
オブジェクト・ライフサイクルの管理
VBScriptはCOM(Component Object Model)上で動作している。`For Each` ループの中で `objFile` や `objSubFolder` を生成し続けると、ガベージコレクションが追いつく前にメモリ消費が膨れ上がり、特に数万件のファイルを扱う場合にプロセスが不安定になる。
ループの終端で `Set objFile = Nothing` を明示的に行うことは、単なる作法ではなく、リソース管理における責務である。
Windows APIとの境界線
FSOは非常に便利だが、内部的には高度なラッパーであり、大量のファイル属性を取得する際にオーバーヘッドが発生する。もし、ミリ秒単位のパフォーマンスが要求される超大規模走査が必要であれば、VBScriptの限界を認め、C++やC#によるネイティブ実装、あるいは `FindFirstFile` / `FindNextFile` APIを直接叩く設計へとシフトすべきだ。しかし、Windowsの標準的な管理業務においては、上記のような「堅牢なFSO実装」こそが、保守性と信頼性のバランスが最も取れた最適解となる。
結論
プロフェッショナルな自動化エンジニアにとって、コードの美しさとは「動くこと」ではない。「予期せぬ環境下でも、静かに、かつ正確に、意図した通りに動き続けること」である。
一時ファイルを排除し、属性を読み解き、メモリを制御する。この積み重ねこそが、レガシーシステムを支える技術の真髄である。
