【入門編】VB.NETでのファイル一括処理と例外ハンドリング:数千ファイルの高速リネーム・移動ツールを作る – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

こんにちは!開発現場の裏側で、日々泥臭い自動化ツールを組み上げている先輩エンジニアです。

Excelのマクロ記録から一歩踏み出し、「そろ本格的なバッチ処理ツールを作りたい!」と思ったとき、最初に直面するのが「大量のファイル操作とエラーハンドリングの壁」です。

例えば、社内の共有サーバーにある数千枚の画像を整理しようとプログラムを動かした瞬間、誰かがファイルを開いていたために「アクセス拒否(UnauthorizedAccessException)」や「ファイルロック(IOException)」が発生し、プログラムがガツンと強制終了してしまった……なんて経験はありませんか?

今回は、`System.IO.DirectoryInfo` をフル活用し、「途中でエラーが起きても絶対に止まらない、数千ファイルの高速リネーム・移動ツール」の作り方を解説します。
ここをクリアすれば、あなたのVB.NETのスキルは間違いなく「実務レベル」に到達しますよ。さあ、一緒に本物の自動化の世界へ足を踏み入れましょう!

なぜ「古いやり方」や「単純なループ」ではダメなのか?

VB.NETでファイルを扱うとき、初心者がやりがちなのが `My.Computer.FileSystem` を使った簡易的な記述や、古い `Microsoft.VisualBasic.FileIO` の使用です。

しかし、数千・数万ファイル規模のバッチ処理において、これらは以下の致命的な弱点を抱えています。

1. メモリの非効率性: 一度にファイルリストをメモリ上に展開しすぎて、巨大なフォルダでメモリ不足(OutOfMemoryException)を引き起こす。
2. エラー耐性の欠如: 1つのファイルでアクセス権限エラーやロックが発生しただけで、その瞬間に処理全体の例外がスローされ、プログラムがクラッシュする。
3. 処理速度の限界: ドライブ全体のメタデータを効率よくスキャンできず、I/Oのボトルネックに直面する。

これらを解決するのが、.NET Framework / .NET Core の底力である `System.IO.DirectoryInfo` と、個別に例外をいなす「粒度の細かいトライ・キャッチ構造」です。

堅牢なファイル一括処理エンジンの全体像

それでは、実際に動くプロダクション品質のVB.NETコードをお見せします。
このコードは、指定したフォルダ内にある特定の拡張子のファイルを走査し、「安全にリネームしながら別フォルダへ移動する」バッチ処理のサンプルです。

Imports System.IO

Module FileBatchProcessor

Sub Main()
‘ — 設定エリア —
Dim sourceDirectory As String = “C:\Data\BeforeProcessing”
Dim targetDirectory As String = “C:\Data\AfterProcessing”
Dim targetExtension As String = “.txt” ‘ 対象とする拡張子

Console.WriteLine(“=== ファイル一括処理バッチを開始します ===”)

‘ 移動先フォルダが存在しない場合は自動生成
If Not Directory.Exists(targetDirectory) Then
Directory.CreateDirectory(targetDirectory)
End If

‘ DirectoryInfoオブジェクトの生成(遅延評価の恩恵を受ける基盤)
Dim dirInfo As New DirectoryInfo(sourceDirectory)

If Not dirInfo.Exists Then
Console.WriteLine($”[エラー] 指定された元フォルダが存在しません: {sourceDirectory}”)
Return
End If

Dim successCount As Integer = 0
Dim errorCount As Integer = 0

‘ — 高速&安全なファイル列挙とループ処理 —
‘ EnumerateFilesを使うことで、全リストをメモリに一括展開せず、ストリーム的に1件ずつ処理します
Try
For Each fileInfo As FileInfo In dirInfo.EnumerateFiles(“” & targetExtension, SearchOption.AllDirectories)

‘ 1ファイルごとの処理(ここでエラーが起きても全体を止めない)
Try
‘ 1. 新しいファイル名の生成(例: タイムスタンプを付与してユニークにする)
Dim timestamp As String = DateTime.Now.ToString(“yyyyMMdd_HHmmss_fff”)
Dim newFileName As String = $”{timestamp}_{fileInfo.Name}”
Dim destFilePath As PathCombine = Path.Combine(targetDirectory, newFileName)

‘ 2. ファイルの移動(リネーム同時実行)
‘ ※ MoveToメソッドは同一ボリューム内なら高速、別ボリュームならコピー&削除を実行
fileInfo.MoveTo(destFilePath)

Console.WriteLine($”[成功] 処理完了: {fileInfo.Name} -> {newFileName}”)
successCount += 1

Catch ex As UnauthorizedAccessException
‘ アクセス権限がないファイル(管理者権限が必要など)
Console.WriteLine($”[警告] アクセスが拒否されました: {fileInfo.FullName} 理由: {ex.Message}”)
errorCount += 1

Catch ex As IOException
‘ 他のアプリでファイルが開かれている(ロックされている)場合など
Console.WriteLine($”[警告] ファイルがロックされています(スキップします): {fileInfo.FullName}”)
errorCount += 1

Catch ex As Exception
‘ 予期せぬその他のエラー
Console.WriteLine($”[エラー] 予期せぬエラー [{fileInfo.FullName}]: {ex.Message}”)
errorCount += 1
End Try

Next

Catch ex As Exception
‘ フォルダ自体にアクセスできない等の致命的エラー
Console.WriteLine($”[致命的エラー] ディレクトリの走査に失敗しました: {ex.Message}”)
End Try

‘ — 処理結果サマリーの出力 —
Console.WriteLine(“========================================”)
Console.WriteLine($” 処理完了サマリー”)
Console.WriteLine($” 成功件数: {successCount} 件”)
Console.WriteLine($” スキップ・エラー件数: {errorCount} 件”)
Console.WriteLine(“========================================”)
Console.WriteLine(“何かキーを押すと終了します…”)
Console.ReadKey()
End Sub

End Module

コードの核心を読み解く:プロがこだわる3つのポイント

上記のコードには、単に動くだけではなく「現場で耐え抜くため」の高度なエンジニアリングが詰まっています。初心者から一歩抜け出すためのポイントを解説します。

1. `EnumerateFiles` によるメモリ爆発の回避

`Directory.GetFiles()` というメソッドを聞いたことがあるかもしれません。これはフォルダ内のファイルパスを一気にString配列としてすべてメモリに読み込みます。もし対象が10万ファイルあったら、それだけでメモリを大きく消費します。
一方、ここで使っている `dirInfo.EnumerateFiles()` は、イテレータ(遅延評価)パターンを使用しています。「必要な瞬間に、1ファイルずつメモリにロードして処理する」ため、数万〜数百万ファイルの巨大なツリー構造であっても、メモリ使用量を極限まで低く抑えた高速処理が可能です。

2. 例外処理(Try-Catch)の「粒度(スコープ)」の美学

プログラミング初心者がやりがちな最大のミスが、`For Each` ループの外側に `Try-Catch` を置いてしまうことです。
外側に置いてしまうと、3000番目のファイルでロックエラーが起きた瞬間、ループ全体がそこで強制終了してしまい、残りの2000ファイルが処理されなくなってしまいます。

今回のコードでは、「ループの内側(1ファイルごとの処理単位)」に `Try-Catch` を配置しています。これにより、エラーが発生したファイルはその場で例外をキャッチしてログ(コンソール出力)に残し、「何事もなかったかのように次のファイルの処理へ進む(フォールトトレランス)」という堅牢性を実現しています。

3. 具体的な例外のキャッチ(型指定の重要性)

`Catch ex As Exception` と大雑把に書くのは簡単ですが、プロのコードではエラーの原因ごとに処理を分けるのが鉄則です。

  • `UnauthorizedAccessException`: 権限不足。セキュリティ設定を見直すヒントになる。
  • `IOException`: ExcelやWordなどで誰かがファイルを開いている。これは業務中によくある「仕様上のエラー」なので、処理を止めずにスキップするのが正解。

このように例外を細かくハンドリングすることで、バッチ処理のログを見ただけで「なぜそのファイルが処理されなかったのか」が一目で分かるようになります。

初学者がハマりやすい「落とし穴」と対策

最後に、このツールを実務で動かす際につまずきやすいポイントを先回りして共有しておきます。

  • 落とし穴1: パスの文字数制限(MAX_PATH問題)

Windowsの古い仕様では、ファイルパスの合計が260文字を超えるとエラーになります。もし深い階層のフォルダを扱う場合は、プロジェクトの `app.manifest` や .NET の設定でロングパス(Long Path)を有効にするか、極力浅い階層で作業するようにしましょう。

  • 落とし穴2: `MoveTo` の挙動の違い

`fileInfo.MoveTo()` は、移動先のドライブが元と同じ(例: CドライブからCドライブ)であれば、ファイルの実データをコピーせず「場所の看板を書き換えるだけ」なので一瞬で終わります。しかし、別ドライブ(例: CドライブからDドライブやネットワーク共有)へ移動する場合は、裏側で「コピー + 元のファイルの削除」が行われるため時間がかかります。 大容量ファイルを扱う際は注意してください。

まとめ

今回は、`System.IO.DirectoryInfo` を駆使した、堅牢で高速なファイル一括処理バッチの構築法を解説しました。

  • `EnumerateFiles` でメモリを節約しながら安全に高速スキャンする
  • ループの内側で例外を細かくキャッチし、エラーでプログラムを止めない
  • 発生しうるエラー(権限・ロック)を想定したコードを書く

ここをクリアできれば、単なる「マクロの延長」ではなく、プロのエンジニアが書く堅牢なバックエンド自動化ツールの領域に足を踏み入れたことになります。

日々の退屈なファイル整理作業はプログラムに任せて、あなたしかできないクリエイティブな開発に時間を使いましょう。それでは、また次の知見でお会いしましょう!

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