ねらい:VBAで「高速ファイル走査」を実現し、数十万件でも固まらず完走する
大量ファイルの走査は、再帰やループの書き方次第で極端に遅くなります。Excelでは「Dirの連続呼び出し+配列バッファ+一括書き出し」が最速の基本形です。FileSystemObject(FSO)は書きやすい反面遅い場面があり、純VBAのDirを軸に、進捗の間引き・チャンク出力・フィルタを組み合わせると、数十万件規模でも安定して終わります。初心者向けに貼って動くテンプレートと、実務で効く深掘りを示します。
重要ポイントの深掘り
- 走査結果は「配列に貯めて一括で書く」。セルへ都度書くと致命的に遅くなります。
- 再帰はスタックを使うため、極端に深い階層で不利。キュー(スタック)にフォルダを入れて非再帰にする方が安全です。
- 絞り込み(拡張子・サイズ閾値・更新時刻)を早い段階で行い、無駄な情報をそもそも貯めないことが速度に直結します。
最速の軸:Dirによる非再帰走査+配列バッファ
非再帰でフォルダをキューに入れて広げる
' ModFastScan_Dir.bas
Option Explicit
Public Sub Run_FastScan_Dir(ByVal root As String, Optional ByVal extsCsv As String = "*", Optional ByVal maxRows As Long = 200000)
On Error GoTo EH
AppEnter "FastScan"
Dim ws As Worksheet
Set ws = PrepareOutputSheet("Scan")
Dim allowedExts() As String
allowedExts = Split(UCase$(extsCsv), ",")
Dim buf() As Variant: ReDim buf(1 To 5000, 1 To 5) ' チャンク書き出し用バッファ
Dim w As Long: w = 0
Dim total As Long: total = 0
Dim q() As String: ReDim q(1 To 1)
q(1) = root
Dim head As Long: head = 1
Dim tail As Long: tail = 1
Dim rowOut As Long: rowOut = 2
ws.Range("A1:E1").Value = Array("Path", "Name", "Ext", "Size(bytes)", "LastWriteTime")
Do While head <= tail
Dim folder As String: folder = q(head): head = head + 1
Dim f As String
f = Dir(folder & "\*", vbDirectory)
Do While Len(f) > 0
If f <> "." And f <> ".." Then
Dim full As String: full = folder & "\" & f
If (GetAttr(full) And vbDirectory) = vbDirectory Then
tail = tail + 1
ReDim Preserve q(1 To tail)
q(tail) = full
Else
If PassExtFilter(full, allowedExts) Then
w = w + 1
buf(w, 1) = full
buf(w, 2) = f
buf(w, 3) = GetExt(full)
buf(w, 4) = FileLen(full)
buf(w, 5) = FileDateTime(full)
total = total + 1
If w = UBound(buf, 1) Then
ws.Range("A" & rowOut).Resize(w, 5).Value = buf
rowOut = rowOut + w
w = 0
Application.StatusBar = "Found " & total & " files..."
If total >= maxRows Then GoTo Done
End If
End If
End If
End If
f = Dir
Loop
Loop
Done:
If w > 0 Then
ws.Range("A" & rowOut).Resize(w, 5).Value = buf
End If
ws.Columns.AutoFit
AppLeave
MsgBox "走査完了: " & total & " 件", vbInformation
Exit Sub
EH:
AppLeave
MsgBox "失敗: " & Err.Description, vbExclamation
End Sub
Private Function PassExtFilter(ByVal path As String, ByVal allowedExts() As String) As Boolean
If UBound(allowedExts) = 0 And allowedExts(0) = "*" Then PassExtFilter = True: Exit Function
Dim e As String: e = UCase$(GetExt(path))
Dim i As Long
For i = LBound(allowedExts) To UBound(allowedExts)
If e = Trim$(allowedExts(i)) Then PassExtFilter = True: Exit Function
Next
End Function
Private Function GetExt(ByVal path As String) As String
Dim p As Long: p = InStrRev(path, ".")
If p > 0 Then GetExt = Mid$(path, p + 1) Else GetExt = ""
End Function
Private Function PrepareOutputSheet(ByVal name As String) As Worksheet
Dim ws As Worksheet
On Error Resume Next
Set ws = ThisWorkbook.Worksheets(name)
On Error GoTo 0
If ws Is Nothing Then
Set ws = ThisWorkbook.Worksheets.Add
ws.Name = name
End If
ws.Cells.Clear
Set PrepareOutputSheet = ws
End Function
VB重要ポイントの深掘り
- Dirは「同一スレッド内で状態を持つ」ため、同時に複数Dirループを走らせないようにします。非再帰でフォルダ名をキューへ追加し、1本のDirで広げる設計が速くて安全です。
- バッファは2,000〜10,000行程度がバランス。行ごとの書き込みは禁物です。
- サイズ・更新時刻などの情報は標準関数(FileLen, FileDateTime)で高速取得できます。
柔軟性重視:FileSystemObject(FSO)で読みやすく書く
FSOで拡張子・サイズ・時刻を取る(速度より可読性)
' ModScan_FSO.bas
Option Explicit
Public Sub Run_Scan_FSO(ByVal root As String, Optional ByVal minSizeBytes As Double = 0)
On Error GoTo EH
AppEnter "FSO Scan"
Dim ws As Worksheet: Set ws = PrepareOutputSheet("ScanFSO")
ws.Range("A1:F1").Value = Array("Path", "Name", "Ext", "Size(bytes)", "LastWriteTime", "Parent")
Dim fso As Object: Set fso = CreateObject("Scripting.FileSystemObject")
Dim buf() As Variant: ReDim buf(1 To 3000, 1 To 6)
Dim w As Long: w = 0, rowOut As Long: rowOut = 2
Dim stack As Collection: Set stack = New Collection
stack.Add fso.GetFolder(root)
Do While stack.Count > 0
Dim fol As Object: Set fol = stack(1): stack.Remove 1
Dim subF As Object
For Each subF In fol.SubFolders
stack.Add subF
Next
Dim fil As Object
For Each fil In fol.Files
If fil.Size >= minSizeBytes Then
w = w + 1
buf(w, 1) = fil.Path
buf(w, 2) = fil.Name
buf(w, 3) = fso.GetExtensionName(fil.Path)
buf(w, 4) = fil.Size
buf(w, 5) = fil.DateLastModified
buf(w, 6) = fil.ParentFolder.Name
If w = UBound(buf, 1) Then
ws.Range("A" & rowOut).Resize(w, 6).Value = buf
rowOut = rowOut + w
w = 0
Application.StatusBar = "FSO Found..."
End If
End If
Next
Loop
If w > 0 Then ws.Range("A" & rowOut).Resize(w, 6).Value = buf
ws.Columns.AutoFit
AppLeave
MsgBox "FSO走査完了", vbInformation
Exit Sub
EH:
AppLeave
MsgBox "失敗: " & Err.Description, vbExclamation
End Sub
VB重要ポイントの深掘り
- FSOはオブジェクト生成と列挙が多く、Dirより遅くなりがちですが、拡張情報(親フォルダ名など)が簡単に取れる利点があります。
- 深いフォルダ構造でも非再帰(コレクションのスタック/キュー)なら安全です。
- サイズフィルタなど「入口で早く捨てる」設計にすると、総処理量が減り体感速度が上がります。
フィルタと並べ替え:走査中に軽く、書き出し後に重く
走査中は最小限の判定だけに絞る
- 拡張子一致、サイズ閾値、更新時刻(After/Before)の3点に留める。複雑な正規表現や文字列操作は後工程(シート上の関数やPower Query)に回す方が速いです。
書き出し後の並べ替え・重フィルタはExcel機能で
- 走査結果を書き出したら、テーブル化(Ctrl+T)して列名でフィルタ・並べ替え。開発不要で高速です。
- 必要なら列関数(LEFT/RIGHT/SEARCH)で軽整形し、ピボットで集約すれば「何が多いか」がすぐ見えます。
超大規模向け:チャンク書き出し+疑似並列でUIを固めない
OnTimeで段階的に回すテンプレ
' ModScan_Chunked.bas
Option Explicit
Private gQueue() As String, gHead As Long, gTail As Long
Private gBuf() As Variant, gW As Long, gWs As Worksheet, gRowOut As Long, gStop As Boolean
Public Sub StartScanChunked(ByVal root As String)
gStop = False
Set gWs = PrepareOutputSheet("ScanChunk")
gWs.Range("A1:E1").Value = Array("Path", "Name", "Ext", "Size(bytes)", "LastWriteTime")
gRowOut = 2
ReDim gBuf(1 To 5000, 1 To 5): gW = 0
ReDim gQueue(1 To 1): gQueue(1) = root: gHead = 1: gTail = 1
Application.OnTime Now, "'" & ThisWorkbook.Name & "'!TickScanChunked", , True
End Sub
Public Sub StopScanChunked(): gStop = True: Application.StatusBar = False: End Sub
Public Sub TickScanChunked()
If gStop Then Exit Sub
Dim startTick As Double: startTick = Timer
Do While gHead <= gTail
Dim folder As String: folder = gQueue(gHead): gHead = gHead + 1
Dim f As String: f = Dir(folder & "\*", vbDirectory)
Do While Len(f) > 0
If f <> "." And f <> ".." Then
Dim full As String: full = folder & "\" & f
If (GetAttr(full) And vbDirectory) = vbDirectory Then
gTail = gTail + 1: ReDim Preserve gQueue(1 To gTail)
gQueue(gTail) = full
Else
gW = gW + 1
gBuf(gW, 1) = full
gBuf(gW, 2) = f
gBuf(gW, 3) = GetExt(full)
gBuf(gW, 4) = FileLen(full)
gBuf(gW, 5) = FileDateTime(full)
If gW = UBound(gBuf, 1) Then
gWs.Range("A" & gRowOut).Resize(gW, 5).Value = gBuf
gRowOut = gRowOut + gW: gW = 0
Application.StatusBar = "Chunk write..."
End If
End If
End If
f = Dir
Loop
If Timer - startTick > 0.2 Then Exit Do ' 約200msで区切る
Loop
If gHead > gTail Then
If gW > 0 Then gWs.Range("A" & gRowOut).Resize(gW, 5).Value = gBuf
Application.StatusBar = "完了"
Else
Application.OnTime Now + 0.5 / 86400#, "'" & ThisWorkbook.Name & "'!TickScanChunked", , True
End If
End Sub
VB重要ポイントの深掘り
- 「200msで区切って再予約」はUI応答を保ちながら進める定番。大量フォルダでも固まりません。
- バッファ書き出しでディスクI/Oをまとめ、行ごと書くコストを消します。
- キャンセル(Stop)で次チャンク開始前に抜けるようにして、いつでも安全に止められるようにします。
補助ツール連携:PowerShellで一覧生成→VBAで読み込み
外部で高速一覧、VBAでは表示と加工に専念する
- PowerShell例(Get-ChildItemで再帰、CSV出力)
Get-ChildItem -Path "C:\Data" -Recurse -File |
Select-Object FullName, Name, Extension, Length, LastWriteTime |
Export-Csv -Path "C:\Data\list.csv" -NoTypeInformation -Encoding UTF8
- VBAでCSV読み込み(UTF-8)
' ModCsvLoad.bas
Option Explicit
Public Function LoadCsvToArray(ByVal path As String) As Variant
Dim st As Object: Set st = CreateObject("ADODB.Stream")
st.Type = 2: st.Charset = "UTF-8": st.Open: st.LoadFromFile path
Dim text As String: text = st.ReadText: st.Close
Dim lines() As String: lines = Split(text, vbCrLf)
Dim head() As String: head = Split(lines(0), ",")
Dim rows As Long: rows = UBound(lines)
Dim cols As Long: cols = UBound(head) + 1
Dim a() As Variant: ReDim a(1 To rows, 1 To cols)
Dim r As Long, c As Long
For c = 1 To cols: a(1, c) = head(c - 1): Next
For r = 2 To rows
If Len(lines(r - 1)) = 0 Then Exit For
Dim rec() As String: rec = Split(lines(r - 1), ",")
For c = 1 To cols: a(r, c) = IIf(c - 1 <= UBound(rec), rec(c - 1), ""): Next
Next
LoadCsvToArray = a
End Function
Public Sub ShowCsv(ByVal path As String)
Dim ws As Worksheet: Set ws = PrepareOutputSheet("ScanCsv")
Dim a As Variant: a = LoadCsvToArray(path)
ws.Range("A1").Resize(UBound(a, 1), UBound(a, 2)).Value = a
ws.Columns.AutoFit
End Sub
VB重要ポイントの深掘り
- 外部に任せると「一覧生成のスピード」はさらに出ます。VBAは表示・フィルタ・集計に専念すると、全体の設計がシンプルで壊れにくくなります。
- CSVはUTF-8で固定。Excel側で文字化けしないように、Streamの文字コードを揃えます。
共通枠:必ず復帰し、UIを固めない基本テンプレ
開始・終了枠
' ModApp.bas
Option Explicit
Public Sub AppEnter(Optional ByVal status As String = "")
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.DisplayAlerts = False
If Len(status) > 0 Then Application.StatusBar = status
End Sub
Public Sub AppLeave()
Application.StatusBar = False
Application.DisplayAlerts = True
Application.EnableEvents = True
Application.ScreenUpdating = True
End Sub
VB重要ポイントの深掘り
- 走査は時間がかかります。描画・イベントを止めて速度を確保し、終了時は必ず元に戻す。
- StatusBarは進捗や件数の可視化に有効ですが、毎行更新は禁物。チャンク終端だけ更新します。
例題の通し方と検証ポイント
手順
- Run_FastScan_Dir “C:\Data”, “CSV,TXT,XLSX” を実行して、拡張子フィルタが効いた一覧が出るか確認します。
- Run_Scan_FSO “C:\Data”, 100000 でサイズ100KB以上に絞って動作を比較します。
- StartScanChunked “C:\Data” でUIが固まらず進行するか、StopScanChunked で安全に止まるかを確認します。
- ShowCsv “C:\Data\list.csv” で外部生成CSVの読込が正しく表示されるか確認します。
確認ポイント
- 書き出しはチャンク単位で行われているか(行単位でなく)。
- 大量階層でもエラーなく完了するか。権限のないフォルダでDir/GetAttrが失敗する場合はOn Errorの局所ガードを追加。
- 進捗更新が間引かれており、体感が滑らかか。
まとめ:Dir+非再帰+配列バッファが「速さの核」。必要に応じてFSOや外部連携を併用する
- 最速はDirで非再帰走査、結果は配列バッファへ貯めて一括書き込み。
- 可読性・拡張情報が欲しければFSOで、ただしバッファとチャンク出力は維持。
- 超大規模はOnTimeでチャンク化し、UI応答と完走性を両立。
- さらに大規模・厳密要件はPowerShellで一覧生成→VBAで表示に分担。
