【テクニカル・上級編】【実務中級者向け】FSOのファイル走査時に一時ファイル(.tmp / ~$)や不要ファイルをスキップするフィルタリング – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク
スポンサーリンク

走査の極致: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実装」こそが、保守性と信頼性のバランスが最も取れた最適解となる。

結論

プロフェッショナルな自動化エンジニアにとって、コードの美しさとは「動くこと」ではない。「予期せぬ環境下でも、静かに、かつ正確に、意図した通りに動き続けること」である。
一時ファイルを排除し、属性を読み解き、メモリを制御する。この積み重ねこそが、レガシーシステムを支える技術の真髄である。

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