【VB.NET極限知見】数千ファイルの爆速リネーム・移動!堅牢なバッチ処理アーキテクチャの構築法
開発現場でよくある要求:「数千、数万単位のファイルを一括でリネームし、別ディレクトリへ安全に移動させたい」。
これを素朴に `Directory.GetFiles` と `File.Move` で実装していませんか?
もしそうなら、実運用で確実に「アクセス拒否 (UnauthorizedAccessException)」や「ファイルロック (IOException)」に直面し、処理が途中でクラッシュする悪夢を見るでしょう。数千ファイルの処理において、「1つのファイルの例外で全体のバッチを止めない」ことは、業務自動化ツールにおける絶対の鉄則です。
今回は、VB.NET(.NET Core / .NET 6+対応)の性能を極限まで引き出し、堅牢性とスピードを両立したファイル一括処理エンジンの設計思想と実装を、チーフアーキテクトの私から直伝します。
—
1. なぜ「普通の書き方」では破綻するのか?
多くのプログラマが陥るアンチパターンを見てみましょう。
- `Directory.GetFiles()` の全件配列化
- 数万ファイルのディレクトリでこれを呼ぶと、メモリを無駄に消費し、ファイル列挙が終わるまでスレッドがブロックされます。
- 例外処理の欠落(または雑なCatch)
- 別のプロセスが開いているファイルを掴んでいるだけで `IOException` が発生し、処理が中断します。リトライ機構やログ記録なしでは、業務ツールとして使い物になりません。
- 古いCOMオブジェクト(FileSystemObject)の利用
- VB6時代の遺物を.NETで使うのはパフォーマンス的にも例外ハンドリングの観点でも百害あって一利なしです。`System.IO` Namespaceを使い倒すべきです。
—
2. 堅牢なファイルバッチエンジンの設計方針
プロフェッショナルなツールに必要な要件は以下の3点です。
1. ストリーミング列挙によるメモリ効率の最大化
- 配列ではなく `IEnumerable
` をベースにし、遅延評価(Lazy Evaluation)でメモリ消費を抑えます。
2. 多層防御の例外ハンドリングとリトライ戦略
- ファイルロックは一瞬のタイミングで解消されることが多いため、短時間のバックオフ(リトライ待ち)を挟むことで成功率が跳ね上がります。
3. トランザクション的な思考(ロールバックは難しいが、失敗箇所の完全な隔離とログ出力)
- 失敗したファイルはスキップし、成功・失敗の集計を正確に出力します。
—
3. 実装コード:プロダクション品質のファイル一括処理ツール
以下のコードは、Visual Basic (VB.NET) で記述された、数千ファイルの高速リネーム・移動を行うモジュールです。そのままコピー&ペーストしてプロジェクトに組み込むことができます。
Imports System.IO
Imports System.Collections.Generic
Namespace EnterpriseTools
Public Class FileBatchEngine
‘ 処理結果を格納する構造体
Public Structure BatchResult
Public TotalProcessed As Integer
Public SuccessCount As Integer
Public FailureCount As Integer
Public ErrorLog As List(Of String)
End Structure
”’
”’
”’ 元ディレクトリ
”’ 先ディレクトリ
”’ 検索パターン (例: “.txt”)
”’
Public Function ExecuteBatchProcess(sourceDir As String, destDir As String, searchPattern As String) As BatchResult
Dim result As New BatchResult With {
.ErrorLog = New List(Of String)()
}
Dim sourceInfo As New DirectoryInfo(sourceDir)
Dim destInfo As New DirectoryInfo(destDir)
‘ 移動先が存在しない場合は作成
If Not destInfo.Exists Then
destInfo.Create()
End If
‘ 【重要】GetFiles()ではなくEnumerateFiles()を使い、メモリ効率と速度を最適化
Dim files As IEnumerable(Of FileInfo)
Try
files = sourceInfo.EnumerateFiles(searchPattern, SearchOption.TopDirectoryOnly)
Catch ex As Exception
result.ErrorLog.Add($”[致命的エラー] ディレクトリの列挙に失敗しました: {ex.Message}”)
Return result
End Try
‘ メインループ
For Each fileInfo As FileInfo In files
result.TotalProcessed += 1
Try
‘ 1. 新しいファイル名の生成ロジック(例:プレフィックス付与 + タイムスタンプ)
Dim newFileName As String = GenerateNewFileName(fileInfo)
Dim destFilePath As PathString = Path.Combine(destInfo.FullName, newFileName)
‘ 2. リネームと移動を同時に実行 (Moveメソッド)
‘ ※同一ボリューム内であれば極めて高速に処理されます
SafeMoveWithRetry(fileInfo.FullName, destFilePath, maxRetries:=3)
result.SuccessCount += 1
Catch ex As Exception
‘ 個別ファイルの失敗は全体を止めず、ログに記録して次へ進む
result.FailureCount += 1
result.ErrorLog.Add($”[ファイル処理失敗] {fileInfo.Name} -> 理由: {ex.Message}”)
End Try
Next
Return result
End Function
”’
”’
Private Function GenerateNewFileName(fileInfo As FileInfo) As String
‘ 例: “Processed_” + タイムスタンプ(yyyyMMdd_HHmmss) + 元の拡張子
‘ 業務要件に合わせてここを書き換えてください
Dim timeStamp As String = DateTime.Now.ToString(“yyyyMMdd_HHmmss_fff”)
Dim extension As String = fileInfo.Extension
Dim baseName As String = Path.GetFileNameWithoutExtension(fileInfo.Name)
Return $”{baseName}_{timeStamp}{extension}”
End Function
”’
”’
Private Sub SafeMoveWithRetry(sourcePath As String, destPath As String, maxRetries As Integer)
Dim currentRetry As Integer = 0
While True
Try
‘ 既に同名ファイルが存在する場合は上書き
File.Move(sourcePath, destPath, overwrite:=True)
Exit While ‘ 成功したらループを抜ける
Catch ex As IOException When currentRetry < maxRetries
' ファイルが別プロセスに掴まれている可能性が高い場合、待機してリトライ
currentRetry += 1
System.Threading.Thread.Sleep(200 currentRetry) ' バックオフ待機 (200ms, 400ms, 600ms...)
Catch
' IOException以外、またはリトライ回数を超えた場合はそのままスロー
Throw
End Try
End While
End Sub
End Class
End Namespace
---
4. コードの解説とアーキテクトの視点
① `EnumerateFiles` の採用
配列を返す `GetFiles()` は、数万件のファイルがある場合に一括してメモリ上にファイル情報のインスタンス配列を構築するため、GC(ガベージコレクション)に多大な負荷をかけます。`EnumerateFiles()` は `IEnumerable
② `SafeMoveWithRetry` による耐障害性
業務システムにおいて、ファイルがアンチウイルスソフトスキャン中であったり、別システムが書き込み中であるために一時的な `IOException`(ファイルが使用中です)が発生することは日常茶飯事です。
このコードでは、LINQや単純なTry-Catchではなく、段階的なウェイト(バックオフ)を伴うリトライループを実装しています。これにより、人間の手動介入を必要としない「止まらないバッチ」を実現しています。
③ 同一ボリューム間の移動の特性
`File.Move` は、移動元と移動先が同一ドライブ(ボリューム)内である場合、物理的なデータのコピーを行わず、ファイルシステム上のMFT(マスターファイルテーブル)のポインタを書き換えるだけで完了します。そのため、数千ファイルあっても一瞬で処理が終わります。別ドライブへの移動の場合は物理コピーが発生するため処理時間が変わる点も、設計時に考慮すべき重要知見です。
—
5. まとめ:現場で成果を出すために
業務自動化ツールを作る際、動くだけのコードを書くのはプログラマであれば誰でもできます。しかし、「例外が起きる前提で設計し、システムが破綻しないように守りを固めること」こそが、エンジニアの腕の見せ所です。
今回紹介した設計パターンをあなたのVB.NETプロジェクトに組み込めば、現場からの「またバッチが止まったんだけど」というクレームを永遠に根絶することができるはずです。
さあ、レガシーな発想を捨て、堅牢でモダンなVB.NET開発を始めましょう。
