散らかったディレクトリを「秩序」へと変える:FSOを用いた堅牢な自動アーカイブ・エンジンの設計思想
現場のエンジニア諸君、日々、増殖し続けるドキュメントや画像ファイルの管理に頭を抱えてはいないか?
「とりあえず、ファイルの日付を見てフォルダに分けるスクリプトを書いた」――もし君がそう答えるなら、残念ながら君はまだ「ツールを作った」だけで、「システムを設計した」わけではない。
実務における自動化ツールに求められるのは、単なる「動くコード」ではない。「例外的な状況(ファイルが使用中、同名ファイルが存在、アクセス権限不足など)に遭遇しても、システムを破壊せず、整合性を保ちながら完遂できる堅牢性」だ。
今回は、VBScriptのFileSystemObject (FSO) を極限まで使いこなし、大量のファイルを「最終更新月(YYYY-MM)」別に自動仕分けする、プロダクションレベルのアーカイブツールを構築する。単なるコピペで終わらせず、その背後にある「設計思想」を脳に刻み込んでほしい。
1. 初級者の罠:なぜ君のスクリプトは現場で「事故」を起こすのか
多くの初級者が書くコードには、共通の致命的な欠陥がある。
1. `On Error Resume Next` の乱用: エラーを無視して進むのは、設計放棄だ。どこで何が失敗したのかが分からず、データ喪失の温床となる。
2. オブジェクトの過剰な生成: ループの中で `CreateObject(“Scripting.FileSystemObject”)` を繰り返すような愚行は、リソースを浪費し、大量ファイル処理時のパフォーマンスを著しく低下させる。
3. 存在確認の欠如: 移動先に同名のファイルがある場合、あるいはフォルダ作成に失敗した場合のハンドリングが抜けている。
我々プロフェッショナルが目指すのは、「予見可能なエラーを事前に防ぎ、予見不可能なエラーを確実に捕捉する」設計である。
2. アーキテクチャ設計:堅牢な仕分けロジックの3原則
本ツールを設計するにあたり、以下の3層構造を定義する。
① Validation(検証層)
対象のファイルが「移動可能か」「日付情報が取得できるか」を事前にチェックする。
② Extraction & Transformation(抽出・変換層)
`DateLastModified` から `YYYY-MM` 形式の文字列を生成する。ここでは、月が1桁の場合に `01` とゼロ埋め(Padding)を行う処理が不可欠だ。これを行わないと、フォルダ名が `2023-1`, `2023-10` となり、ソート順が崩壊する。
③ Atomic Operation(実行層)
「フォルダの存在確認」→「必要なら作成」→「ファイルの移動」を、一つの論理的なステップとして実行する。
3. プロダクション・コード:年月別自動アーカイブエンジン
以下に、そのまま実務に投入可能なレベルのコードを示す。コメントを読み、ロジックの意図を理解せよ。
' ==============================================================================
' Script Name: Professional_File_Archiver.vbs
' Description: 最終更新月(YYYY-MM)に基づき、ファイルを自動仕分けする堅牢なツール
' Author: Chief Architect
' ==============================================================================
Option Explicit
' --- 設定領域 (Configuration) ---
Const SRC_DIR = "C:\Target\SourceFiles" ' 仕分け元ディレクトリ
Const DEST_ROOT = "C:\Target\Archive" ' 仕分け先ルートディレクトリ
Const LOG_FILE = "C:\Target\archive_log.txt" ' 実行ログ出力先
' --- メイン処理 ---
Main
Sub Main()
Dim fso, srcFolder, destRootFolder
Dim file, targetMonth, destFolderPath, targetFilePath
Dim errorCount, successCount
Set fso = CreateObject("Scripting.FileSystemObject")
' 1. 必須リソースの存在確認
If Not fso.FolderExists(SRC_DIR) Then
MsgBox "Error: ソースディレクトリが見つかりません: " & SRC_DIR, vbCritical
Exit Sub
End If
If Not fso.FolderExists(DEST_ROOT) Then
On Error Resume Next
fso.CreateFolder(DEST_ROOT)
If Err.Number <> 0 Then
MsgBox "Error: 仕分け先ルートを作成できませんでした: " & Err.Description, vbCritical
Exit Sub
End If
On Error GoTo 0
End If
Set srcFolder = fso.GetFolder(SRC_DIR)
Set destRootFolder = fso.GetFolder(DEST_ROOT)
successCount = 0
errorCount = 0
' ログの開始
WriteLog "--- アーカイブ処理開始: " & Now & " ---"
' 2. ファイルループ処理
For Each file In srcFolder.Files
' スクリプト自身やログファイルを除外するロジックをここに追加可能
On Error Resume Next
' A. 更新月情報の抽出 (YYYY-MM形式への整形)
' Month関数は1-12を返すため、Right関数でゼロ埋めを行う
targetMonth = Year(file.DateLastModified) & "-" & Right("0" & Month(file.DateLastModified), 2)
' B. 移動先パスの構築
destFolderPath = DEST_ROOT & "\" & targetMonth
targetFilePath = destFolderPath & "\" & file.Name
' C. フォルダの自動作成 (存在しない場合のみ)
If Not fso.FolderExists(destFolderPath) Then
fso.CreateFolder(destFolderPath)
If Err.Number <> 0 Then
WriteLog "ERROR: フォルダ作成失敗 [" & destFolderPath & "] - " & Err.Description
errorCount = errorCount + 1
Err.Clear
GoTo NextFile
End If
End If
' D. ファイル移動の実行 (同名ファイル衝突回避策)
If fso.FileExists(targetFilePath) Then
' 衝突時はファイル名にタイムスタンプを付与して回避する設計
targetFilePath = destFolderPath & "\" & _
fso.GetBaseName(file.Name) & "_" & _
Replace(Replace(Now, ":", ""), " ", "_") & "." & _
fso.GetExtensionName(file.Name)
End If
fso.MoveFile file.Path, targetFilePath
If Err.Number <> 0 Then
WriteLog "ERROR: 移動失敗 [" & file.Name & "] - " & Err.Description
errorCount = errorCount + 1
Err.Clear
Else
successCount = successCount + 1
End If
NextFile:
On Error GoTo 0
Next
' 3. 結果報告
WriteLog "--- 処理完了: 成功(" & successCount & "), 失敗(" & errorCount & ") ---"
MsgBox "処理が完了しました。" & vbCrLf & _
"成功: " & successCount & " 件" & vbCrLf & _
"失敗: " & errorCount & " 件" & vbCrLf & _
"詳細はログを確認してください。", vbInformation
' オブジェクト解放
Set fso = Nothing
End Sub
' --- ユーティリティ関数: ログ出力 ---
Sub WriteLog(message)
Dim fso, logFile
Set fso = CreateObject("Scripting.FileSystemObject")
' 8 = ForAppending (追記モード)
Set logFile = fso.OpenTextFile(LOG_FILE, 8, True)
logFile.WriteLine "[" & Now & "] " & message
logFile.Close
Set logFile = Nothing
Set fso = Nothing
End Sub
4. エンジニアとしての「こだわり」:コード解説
このコードが単なるスクリプトと一線を画すポイントを解説する。
ゼロ埋めによるソートの保証
`Right(“0” & Month(file.DateLastModified), 2)`
この一行が、データの整合性を守る。これがないと、Windowsエクスプローラーで見た際に「2023-10」が「2023-1」の次に並んでしまう。データ管理において、「並び順の予測可能性」は基本中の基本だ。
ファイル衝突(Collision)への防衛策
実務では、同じ名前のファイルが異なるタイミングで作成されることは珍しくない。`fso.FileExists(targetFilePath)` を用いて衝突を検知し、タイムスタンプを付与してリネーム移動するロジックを組み込んでいる。これにより、「移動失敗によるプロセス停止」を防ぎつつ、既存データを破壊しない。
エラーハンドリングとロギング
`On Error Resume Next` を「エラーを無視するため」ではなく、「エラーが発生した瞬間にそのファイルだけをスキップし、ログに記録して次のファイルへ進むため」に使用している。これにより、1,000個のファイルのうち1個が使用中でロックされていても、残りの999個の処理は確実に完遂される。
5. まとめ:保守性の高いツールを目指して
自動化ツールを作成する際、君は常に自分に問いかけなければならない。
「もし、このスクリプトが深夜に、誰も見ていない場所で、予期せぬファイルに遭遇して止まったら、誰がどうやって原因を特定できるか?」
ログを出力し、例外を制御し、データの整合性を担保する。この「守りの設計」こそが、プロフェッショナルなエンジニアと、単なるスクリプターを分かつ境界線である。
このコードをベースに、君の業務に合わせたカスタマイズ(例:特定の拡張子のみ対象にする、移動後に元のフォルダが空なら削除するなど)を加えていけ。
健闘を祈る。
