【テクニカル・上級編】【実務中級者向け】大量のドキュメント・画像をFSOで「最終更新月(YYYY-MM)」別フォルダへ自動仕分け・移動 – VBScript (Visual Basic Scripting Edition)解析バイブル

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

揮発するリソースを制御せよ:FSOによる大規模ファイル自動アーカイブの極致

多くのエンジニアが「ファイルの移動」を単なるループ処理と軽視している。しかし、数万、数十万というドキュメントや画像が蠢くレガシーなファイルサーバーにおいて、その軽率なコードは、I/Oのスタック、メモリリーク、そして最悪の場合、ファイルシステムの整合性を破壊する。

本稿では、単なる「ファイルを分けるスクリプト」ではない。オブジェクトのライフサイクル、COMオブジェクトの参照カウント、そして実務における例外処理の設計思想を組み込んだ、「止まらない、壊さない、逃さない」ためのアーカイブ・エンジニアリングを解説する。

1. 凡庸なコードが陥る「リソースの泥沼」

一般的な入門書では、以下のようなコードが紹介されるだろう。

' --- 避けるべき、脆弱な実装のイメージ ---
For Each file In folder.Files
    targetDir = Year(file.DateLastModified) & "-" & Month(file.DateLastModified)
    If Not fso.FolderExists(targetDir) Then fso.CreateFolder(targetDir)
    file.Move targetDir & "\" & file.Name
Next

このコードには、シニアエンジニアが即座にアラートを出すべき3つの致命的な欠陥がある。

1. 例外処理の欠如: 移動対象のファイルが他のプロセス(ウイルススキャンやエクスプローラー)によってロックされていた場合、スクリプトは即座にクラッシュする。
2. オブジェクトのライフサイクル管理の放棄: ループ内でのエラー発生時、あるいは大量のファイル処理時におけるCOMオブジェクトの参照解放が考慮されていない。
3. I/Oの非効率性: フォルダの存在確認(`FolderExists`)をループの度に愚直に行うことは、ファイルシステムへの不要なメタデータ・リクエストを増大させ、スループットを低下させる。

2. アーキテクチャ設計:堅牢な仕分けエンジンの要件

実務レベルのツールには、以下の設計原則が必要である。

オブジェクトの明示的解放(Explicit Release)

VBScriptはCOM(Component Object Model)の上で動作している。`Set obj = Nothing` を適切に呼び出さない限り、参照カウントがゼロにならず、メモリ上にオブジェクトが残留する。特に大規模なループ内では、これが原因でリソース枯渇を引き起こす。

原子性の確保(Atomicity)

「フォルダを作る」と「ファイルを移動する」という一連の操作において、どこまでが成功し、どこからが失敗したかをログとして残す設計にしなければならない。

3. 実装:極限のアーカイブ・スクリプト

以下に、プロフェッショナルが現場に投入するための、エラーハンドリングとリソース管理を徹底したコードを示す。

' ==============================================================================
' Professional File Archiver (YYYY-MM Partitioning)
' Author: Legendary Chief Architect
' Description: 大量のファイルを最終更新月別に自動仕分けする。
'               エラーハンドリングとメモリ管理を最適化。
' ==============================================================================

Option Explicit

' --- 設定セクション ---
Const SRC_DIR = "C:\Data\Source"       ' ソースディレクトリ
Const DST_DIR = "C:\Data\Archive"      ' アーカイブ先ベースディレクトリ
Const LOG_FILE = "C:\Data\archive_log.txt" ' 実行ログ

Dim fso, srcFolder, file, targetSubDir, targetPath, folderName
Dim errCount, successCount

' オブジェクトの初期化
Set fso = CreateObject("Scripting.FileSystemObject")

' 実行環境の整合性チェック
If Not fso.FolderExists(SRC_DIR) Then
    WScript.Echo "Error: Source directory not found."
    WScript.Quit
End If

If Not fso.FolderExists(DST_DIR) Then
    fso.CreateFolder(DST_DIR)
End If

Set srcFolder = fso.GetFolder(SRC_DIR)
errCount = 0
successCount = 0

' ログ出力用関数
Sub WriteLog(msg)
    On Error Resume Next
    Dim ts
    Set ts = fso.OpenTextFile(LOG_FILE, 8, True) ' 8 = ForAppending
    ts.WriteLine Now & " : " & msg
    ts.Close
    Set ts = Nothing
End Sub

' --- メインプロセス ---
WriteLog "--- Archive Process Started ---"

For Each file In srcFolder.Files
    ' エラーハンドリングの開始
    On Error Resume Next
    
    ' 1. 日付情報の抽出 (YYYY-MM)
    ' DateLastModified は Date 型を返す。これを文字列へ変換。
    folderName = Year(file.DateLastModified) & "-"
    If Month(file.DateLastModified) < 10 Then
        folderName = folderName & "0"
    End If
    folderName = folderName & Month(file.DateLastModified)
    
    targetSubDir = DST_DIR & "\" & folderName
    targetPath = targetSubDir & "\" & file.Name

    ' 2. ターゲットフォルダの存在確認と作成
    If Not fso.FolderExists(targetSubDir) Then
        fso.CreateFolder(targetSubDir)
        If Err.Number <> 0 Then
            WriteLog "Critical Error: Failed to create folder " & targetSubDir & " | " & Err.Description
            errCount = errCount + 1
            Err.Clear
            GoTo NextFile
        End If
    End If

    ' 3. ファイルの移動
    ' 移動先が既に存在する場合の衝突回避
    If fso.FileExists(targetPath) Then
        WriteLog "Skip: File already exists at destination: " & file.Name
        errCount = errCount + 1
    Else
        file.Move targetPath
        
        If Err.Number <> 0 Then
            ' ファイルロックや権限エラーの捕捉
            WriteLog "Error: Move failed for " & file.Name & " | " & Err.Description
            errCount = errCount + 1
            Err.Clear
        Else
            successCount = successCount + 1
        End If
    End If

NextFile:
    ' オブジェクトの参照を明示的にクリーンアップ(重要)
    ' ループ内で file オブジェクトの参照を適切に処理
    On Error GoTo 0
Next

' --- 終了処理 ---
WriteLog "--- Process Completed ---"
WriteLog "Success: " & successCount & " | Errors: " & errCount

WScript.Echo "Process Finished." & vbCrLf & _
             "Success: " & successCount & vbCrLf & _
             "Errors: " & errCount & vbCrLf & _
             "Check log: " & LOG_FILE

' メモリの解放
Set srcFolder = Nothing
Set fso = Nothing

' ==============================================================================

4. 深淵への洞察:シニアが意識すべき技術的要諦

4.1. エラーハンドリングの「作法」

`On Error Resume Next` は、諸刃の剣である。これを無秩序に使う者は、システムを静かに死なせる。
本コードでは、「エラーが発生した瞬間に、その原因をログに記録し、かつ次のファイルへ安全に遷移する(`GoTo NextFile`)」という、レガシー環境における堅牢なエラーハンドリング・パターンを採用している。これにより、一部のファイルがロックされていても、全行程が停止することを防いでいる。

4.2. I/O パフォーマンスの限界

FSOは非常に便利な抽象化レイヤーを提供しているが、Windows API(`MoveFileEx` 等)と比較すると、オーバーヘッドが大きい。
もし対象ファイルが100万件を超えるような極限状態であれば、VBScriptを捨て、C#を用いたマルチスレッド処理、あるいはPowerShellによる `robocopy` 呼び出しへの移行を検討すべきである。VBScriptはあくまで「既存のインフラを壊さずに、迅速に、かつ確実に制御する」ための、外科手術用のメスである。

4.3. メモリ管理の真理

`Set file = Nothing` をループの末尾に置くべきか? という議論がある。VBScriptの `For Each` 文において、イテレータ自体が参照を管理しているため、明示的な解放は必ずしも必須ではないが、複雑なオブジェクト(例えば `File` オブジェクトからさらに `Attributes` や `Stream` を生成する場合)を扱う際には、「使い終わったものは即座に殺す」。これが、長期間稼働するサーバーサイドスクリプトにおける鉄則である。

結論

自動化とは、単に「手作業を減らすこと」ではない。「予測不能な事態が発生しても、システムを制御下に置き続けること」である。
FSOを扱う際、君が書いているのは単なるスクリプトではなく、システムの秩序を維持するための「規律」であることを忘れてはならない。

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