和風スパゲティのレシピ

日本語でコーディングするExcelVBA

FileSystemObject入門-フォルダ取得と再帰処理

FileSystemObject(以下FSOと表記します)において、
フォルダ内のサブフォルダ一覧を取得する方法を解説します。

この処理はDir関数が苦手としているため、FSOに頼らざるを得ません。


そしてその先、「子フォルダ ⇒ 孫フォルダ…」と全階層を取得するには、
再帰関数」と呼ばれる関数を作る必要があります。


なかなか難しいコードになってしまうのですが、
これさえマスターすればFSOを習得したといっていい処理です。


こいつがFileSystemObjectのラスボスですので、心してかかってください。

◇ FileSystemObject入門シリーズ
【1】FileSystemObjectとは - Dir関数群との違い
【2】参照設定と変数宣言
【3】ファイル一覧の取得
【4】すべての下層フォルダの取得(本記事)
【5】FileSystemObjectって何が便利なの?

フォルダ内のすべてのサブフォルダを取得する

まずはフォルダ内のすべてのサブフォルダの取得してみましょう。

これは「フォルダ内のすべてのファイルを取得」したのと、
まったく同じ構造のコードで実行できます。

ソースコード

Public FSO As New FileSystemObject

Sub フォルダ内のすべてのサブフォルダを取得する()
    
    Dim 親フォルダ As Folder
    Set 親フォルダ = FSO.GetFolder("C:\Users\○○\Desktop\テスト")
    
    Dim 子フォルダ As Folder
    For Each 子フォルダ In 親フォルダ.SubFolders
        
        Debug.Print 子フォルダ.Name

    Next
    
End Sub

実行結果

以下のフォルダで実行した場合
フォルダサンプル

イミディエイトウィンドウの出力

いちご
みかん
りんご

 

解説

ファイル取得のコードを以下のように書き換えただけのコードになります。

For Each ファイル In 親フォルダ.Files
' ↓ 書き換え
For Each 子フォルダ In 親フォルダ.SubFolders

 
とても素直な記述のコードですね。


FSOはとてもキレイなオブジェクトの階層構造なので、
ファイルの操作を覚えてしまえば、フォルダ操作もできたも同然です。

フォルダ内の下層フォルダをすべて取得

さてここからが本題です。

フォルダ内の下層フォルダをすべて取得してみましょう。


今回はサンプルとして、

取得したいフォルダ階層

このA~Iのフォルダをすべて取得していくとします。


ひとまず階層が3つとわかっていれば、

Dim 親フォルダ As Folder
Set 親フォルダ = FSO.GetFolder("C:\Users\~~\A")

Dim 子フォルダ As Folder
For Each 子フォルダ In 親フォルダ.SubFolders
    
    Dim 孫フォルダ As Folder
    For Each 孫フォルダ In 子フォルダ.SubFolders

    Next

Next

これでいいわけですが、問題は階層がいくつかわからない点です。


こういったネスト(≒インデント)の深さを動的にしなければいけない処理は、
再帰呼出という手法を使って書く必要があります。


まずは答えから書きますので眺めてみてください。

ソースコード

Public FSO As New FileSystemObject

Sub 実行プロシージャ()
    
    Call 下層フォルダをすべて取得する("C:\Users\○○\Desktop\テスト\A")
    
End Sub

Sub 下層フォルダをすべて取得する(対象フォルダパス As String)
    
    Dim 親フォルダ As Folder
    Set 親フォルダ = FSO.GetFolder(対象フォルダパス)
    
    ' まずは親自身を取得
    Debug.Print 親フォルダ.Path
    
    ' すべてのサブフォルダをループ処理
    Dim 子フォルダ As Folder
    For Each 子フォルダ In 親フォルダ.SubFolders
        
        ' 各サブフォルダに対してこの関数を再度呼出
        Call 下層フォルダをすべて取得する(子フォルダ.Path)
        
    Next
    
End Sub

実行結果

C:\Users\○○\Desktop\テスト\A
C:\Users\○○\Desktop\テスト\A\B
C:\Users\○○\Desktop\テスト\A\B\D
C:\Users\○○\Desktop\テスト\A\B\E
C:\Users\○○\Desktop\テスト\A\B\F
C:\Users\○○\Desktop\テスト\A\C
C:\Users\○○\Desktop\テスト\A\C\G
C:\Users\○○\Desktop\テスト\A\C\H
C:\Users\○○\Desktop\テスト\A\C\I

解説

このコードで最も重要なポイントが、

  • 「Sub 下層フォルダをすべて取得する」という関数の中で、
  • 「Call 下層フォルダをすべて取得する」と、自分自身を呼んでいる

という点です。


このように自分自身をCallすることを「再帰呼出」と言い、
自分自身をCallしている関数を「再帰関数」と言います。


再帰呼出は最初はかなり混乱すると思いますので、
実際に実行される順番にコードを展開してみましょう。


まずは第1回目のCallを簡略化するとこんな感じです。
※ 実際に動くコードではないです。

Sub 下層フォルダをすべて取得する("A")
    Debug.Print "A"
    
    Dim 子フォルダ As Folder
    For Each 子フォルダ In "A".SubFolders
        Call 下層フォルダをすべて取得する(子フォルダ.Path)
    Next
End Sub

 
For Eachも展開しちゃいましょう。

Sub 下層フォルダをすべて取得する("A")
    Debug.Print "A"

    Call 下層フォルダをすべて取得する("B")
    Call 下層フォルダをすべて取得する("C")
End Sub

だいぶ頭を整理して読める感じになりましたね。


さてこのCall部分を無理やりコードに展開してみます。

Sub 下層フォルダをすべて取得する("A")
    Debug.Print "A"

    Sub 下層フォルダをすべて取得する("B")
        Debug.Print "B"

        For Each 子フォルダ In "B".SubFolders
            Call 下層フォルダをすべて取得する(子フォルダ.Path)
        Next
    End Sub

    Sub 下層フォルダをすべて取得する("C")
        Debug.Print "C"
        
        For Each 子フォルダ In "C".SubFolders
            Call 下層フォルダをすべて取得する(子フォルダ.Path)
        Next
    End Sub

End Sub

これでだいぶわかりやすくなりました。


さらにBとCのForEach部分のCallも展開してみましょう。

Sub 下層フォルダをすべて取得する("A")
    Debug.Print "A"

    Sub 下層フォルダをすべて取得する("B")
        Debug.Print "B"

        Call 下層フォルダをすべて取得する("D")
        Call 下層フォルダをすべて取得する("E")
        Call 下層フォルダをすべて取得する("F")
    End Sub

    Sub 下層フォルダをすべて取得する("C")
        Debug.Print "C"
        
        Call 下層フォルダをすべて取得する("G")
        Call 下層フォルダをすべて取得する("H")
        Call 下層フォルダをすべて取得する("I")
    End Sub

End Sub

となります。なんとなくわかってきましたね。


さて最後にまたCallを展開しますが、例えばCall(D)の中身は、

Sub 下層フォルダをすべて取得する("D")
    Debug.Print "D"

    For Each In "D".SubFolders
End Sub

こうなっています。

DにSubFolderはありませんので、このForEachステートメントは実行されません。


これを踏まえて、Callを展開したコードはこんな感じです。

Sub 下層フォルダをすべて取得する("A")
    Debug.Print "A"

    Sub 下層フォルダをすべて取得する("B")
        Debug.Print "B"

        Sub 下層フォルダをすべて取得する("D")
            Debug.Print "D"
            空のForEach
        End Sub
        Sub 下層フォルダをすべて取得する("E")
            Debug.Print "E"
            空のForEach
        End Sub
        Sub 下層フォルダをすべて取得する("F")
            Debug.Print "F"
            空のForEach
        End Sub
    End Sub

    Sub 下層フォルダをすべて取得する("C")
        Debug.Print "C"

        Sub 下層フォルダをすべて取得する("G")
            Debug.Print "G"
            空のForEach
        End Sub
        Sub 下層フォルダをすべて取得する("H")
            Debug.Print "H"
            空のForEach
        End Sub
        Sub 下層フォルダをすべて取得する("I")
            Debug.Print "I"
            空のForEach
        End Sub
    End Sub

End Sub

綺麗に「A→B→D→E→F→C→G→H→I」の順番にDebug.Printされていますね。


図にするとこの順番で実行されたことになります。

取得されたフォルダの順番

何となくわかりましたでしょうか?

これが再帰呼出を使ったすべての階層のフォルダを取得するコードになります。



この再帰処理というのは、初めて読むときには間違いなく混乱します。


もし混乱せず理解できたとしたら、
あなたにはアルゴリズムの才能があります。


はじめはなかなか処理順がつかめないかもしれませんが、
なんとなく理解するだけでも、コードを書き換えて使う分には十分だと思います。

完全に理解しなくても大丈夫ですので、
なんとなくわかった気になって読み進めてください。

処理の順番の変え方4パターン

この章は混乱しそうなら読み飛ばしていいです。

もし各フォルダを取得する順番にこだわる場合は、
以下の4つの実行タイミングでパターン分けすることができます。

Sub 下層フォルダをすべて取得する(対象フォルダパス As String)
    
    Dim 親フォルダ As Folder
    Set 親フォルダ = FSO.GetFolder(対象フォルダパス)
    
    ' 処理タイミングの候補①:Callされた直後
    Debug.Print 親フォルダ.Path

    Dim 子フォルダ As Folder
    For Each 子フォルダ In 親フォルダ.SubFolders
        
        ' 処理タイミングの候補②:For Eachの中で、再帰の前
        Debug.Print 子フォルダ.Path
        
        Call 下層フォルダをすべて取得する(子フォルダ.Path) ' 再帰はここ
        
        ' 処理タイミングの候補③:For Eachの中で、再帰の後
        Debug.Print 子フォルダ.Path
        
    Next
    
    ' 処理タイミングの候補④:すべての再帰処理が終わった後
    Debug.Print 親フォルダ.Path

End Sub

 

ただし、4パターンには同じコードを書けばいいわけではなく、

  • ①と④は「Subごとに取得」なので親フォルダへの処理
  • ②と③は「ForEachで取得」なので子フォルダへの処理

を書く必要がある点に注意してください。


パターン別の処理順は以下の結果になります。

  • ①ForEachが始まる前 :「A→B→D→E→F→C→G→H→I」
  • ②ForEach内で再起の前:「   B→D→E→F→C→G→H→I」
  • ③ForEach内で再起の後:「D→E→F→B→G→H→I→C   」
  • ④ForEachが終わった後:「D→E→F→B→G→H→I→C→A」

取得したいフォルダ階層

気になる方はじっくりコードを追ってみてください。


両者の違いをまとめてみると、

まずは「ForEachの外①④ / ForEachの中②③」に書く違いで、
最上位であるAフォルダに処理されるかどうかが変わります。

  • ①と④は「Subごとに取得」
  • ②と③は「ForEachで取得」

でしたからね。
ForEachの親としてしか登場しないAは、②③では処理されません。


続いて「再帰の前①② / 再帰の後③④」に書く違いで、
自分と子供達のどちらを先に処理するかが変わります。

③④は子供達がすべて終わってから、ようやく自分をDebug.Printしていますね。



複雑なアルゴリズムを再帰呼出で行う場合は、
この4パターンのうちどの処理順にするかが重要になることがあります。


しかし、なんでもいいから全部のフォルダを処理できればOKということであれば、
別に気にする必要はない部分です。


最低限、

  • For Eachの外には「自分を処理するコード」を書く
  • For Eachの中には「子供を処理するコード」を書く
  • For Eachの中に書いたら最上位フォルダには実行されない

これを抑えておくだけでも、十分に使いこなせると思います。

下層フォルダも含めたすべてのファイルを取得

では最後に「下層フォルダにあるすべてのファイル」を取得します。

すべてのExcelファイルをシートに書き出してみましょう。

ソースコード

Public FSO As New FileSystemObject

Sub 実行プロシージャ()
    
    ' ファイル一覧シートを初期化(2行目から最終行までを削除)
    With Worksheets("ファイル一覧")
        .Range(.Rows(2), .Rows(.Rows.Count)).Delete
    End With
    
    ' すべてのファイルパスを取得
    Call フォルダ下にあるすべてのxlsxファイルを取得する("C:\Users\wfsp\Desktop\A")
    
End Sub

' すべてのファイルパスを取得
Sub フォルダ下にあるすべてのxlsxファイルを取得する(対象フォルダパス As String)
    
    Dim 親フォルダ As Folder
    Set 親フォルダ = FSO.GetFolder(対象フォルダパス)
    
    ' まず自分の中にあるファイルを処理
    Call ファイル情報をシートに書き出す(親フォルダ)
       
    ' 続いてすべての子フォルダに対して再帰呼出
    Dim 子フォルダ As Folder
    For Each 子フォルダ In 親フォルダ.SubFolders
        
        ' 再帰部分
        Call フォルダ下にあるすべてのxlsxファイルを取得する(子フォルダ.Path)
        
    Next
    
End Sub

' フォルダ内のファイルへの処理
Sub ファイル情報をシートに書き出す(フォルダ As Folder)
    Dim ファイル As File
    For Each ファイル In フォルダ.Files
        If FSO.GetExtensionName(ファイル.Path) = "xlsx" Then
    
            With Worksheets("ファイル一覧")
                Dim R As Long
                R = .UsedRange.Rows.Count + 1
                
                .Cells(R, 1) = ファイル.Name ' ファイル名
                .Cells(R, 2) = ファイル.DateLastModified ' 更新日時
                .Cells(R, 3) = ファイル.ParentFolder.Path ' フォルダパス
                .Cells(R, 4) = ファイル.Path ' フルパス
    
            End With

        End If
    Next
End Sub

実行結果

各フォルダに1つずつExcelファイルを入れて実行した結果がこちらです。

全階層のxlsxファイル取得結果

解説

少し長いですが、Debug.Printしていた前項のコードとの違いは単純です。

前半部分は第2Subを以下の通り書き換えただけです。

Debug.Print 親フォルダ.Path
' ↓ 書き換え
Call ファイル情報をシートに書き出す(親フォルダ)

 

そして実際にxlsxファイルを取得するFor Each文は、
Callしている第3Subの中に書きました。


このように、実際の処理をプロシージャに分けることで、
一番複雑なメインSubの改修をCall1行に抑えることができています。

最初は処理を追うのが少し難しいかもしれませんが、じっくり読んでみてください。


再帰のような難しいプロシージャ内に別の処理を書いてしまうと、
高確率で脳みそのメモリが足りなくなります。

そんな状態で再帰をいじろうものなら、どの順番で実行されるのかがわからない、
俗にいうスパゲティコードを生んでしまうかもしれません。


しかし逆にプロシージャにさえ分けてしまえば、
それぞれの内部はただの教本コードで済んだりします。


複雑な構造のオブジェクトを処理する場合は、
対象のオブジェクトごとに丁寧にプロシージャを分ける癖をつけましょう。

実際のブックへの処理を追加する

さて最後に、各ブックを実際に処理するコードを書きましょう。

  1. ブックを開く
  2. 第1シートのA1セルに1を入力
  3. ブックを保存する
  4. ブックを閉じる

この処理を追加します。


さてこの処理をどこに書くかという問題ですが、
このコードはFSOの処理が全部終わった後に書くのがおすすめです。


実際に追加したコードを見てみましょう。

長いのでプロシージャのCall部分だけ流し読みしてみてください。

Public FSO As New FileSystemObject

Sub 実行プロシージャ()
    
    ' ファイル一覧シートを初期化(2行目から最終行までを削除)
    With Worksheets("ファイル一覧")
        .Range(.Rows(2), .Rows(.Rows.Count)).Delete
    End With
    
    ' すべてのファイルパスを取得
    Call フォルダ下にあるすべてのxlsxファイルを一覧シートに書き出す("C:\Users\○○\Desktop\A")
    
    ' 各ファイルへの処理
    Call 一覧シートに記載されたすべてのファイルを処理する
    
End Sub

' すべてのファイルパスを取得
Sub フォルダ下にあるすべてのxlsxファイルを一覧シートに書き出す(対象フォルダパス As String)
    
    Dim 親フォルダ As Folder
    Set 親フォルダ = FSO.GetFolder(対象フォルダパス)
    
    ' まず自分の中にあるファイルを処理
    Call ファイル情報をシートに書き出す(親フォルダ)
       
    ' 続いてすべての子フォルダに対して再帰呼出
    Dim 子フォルダ As Folder
    For Each 子フォルダ In 親フォルダ.SubFolders
        
        ' 再帰部分
        Call フォルダ下にあるすべてのxlsxファイルを一覧シートに書き出す(子フォルダ.Path)
        
    Next
    
End Sub

' フォルダ内のファイルへの処理
Sub ファイル情報をシートに書き出す(フォルダ As Folder)
    Dim ファイル As File
    For Each ファイル In フォルダ.Files
        If FSO.GetExtensionName(ファイル.Path) = "xlsx" Then
    
            With Worksheets("ファイル一覧")
                Dim R As Long
                R = .UsedRange.Rows.Count + 1
                
                .Cells(R, 1) = ファイル.Name ' ファイル名
                .Cells(R, 2) = ファイル.DateLastModified ' 更新日時
                .Cells(R, 3) = ファイル.ParentFolder.Path ' フォルダパス
                .Cells(R, 4) = ファイル.Path ' フルパス
    
            End With

        End If
    Next
End Sub


' ///// ここから先が実際にExcelファイルを処理する部分

' ファイル一覧シートのループ部分
Sub 一覧シートに記載されたすべてのファイルを処理する()
    With Worksheets("ファイル一覧")

        ' ファイル一覧のすべての行をループ
        Dim R As Long
        For R = 2 To .UsedRange.Rows.Count

            ' ブックを開く
            Dim ブック As Workbook
            Set ブック = Workbooks.Open(.Cells(R, 4))

            ' 各ブックへの処理はまた別のプロシージャに
            Call ブックごとの処理(ブック)
            
            ' 保存して閉じる
            ブック.Save
            ブック.Close

        Next

    End With
End Sub

' ブックごとの処理
Sub ブックごとの処理(処理ブック As Workbook)
    
    処理ブック.Worksheets(1).Range("A1").Value = 1
    
End Sub

 
だいぶ長くなりましたが、仕組みは単純です。

コードをよく見ると、

' ///// ここから先が実際にExcelファイルを処理する部分

ここまでのコードは前回のコードとまったく同じになっています。
(わかりやすいようにプロシージャ名だけちょっと変えましたが)


つまり実際にファイルを開くのは、前回のコードが終わったあとでやっています。


① まずはこれを作る
ファイル取得の結果

② ↑のリストをループしてExcelを開く

この流れで処理を行っているということですね。


このメリットは、何といっても後半をFSOが全く登場しないコードにできることです。

実際に「Excelを開いて処理」するSubプロシージャを見てみてください。

' ファイル一覧シートのループ部分
Sub 一覧シートに記載されたすべてのファイルを処理する()
    With Worksheets("ファイル一覧")

        ' ファイル一覧のすべての行をループ
        Dim R As Long
        For R = 2 To .UsedRange.Rows.Count

            ' ブックを開く
            Dim ブック As Workbook
            Set ブック = Workbooks.Open(.Cells(R, 4))

            ' 各ブックへの処理はまた別のプロシージャに
            Call ブックごとの処理(ブック)
            
            ' 保存して閉じる
            ブック.Save
            ブック.Close

        Next

    End With
End Sub

ものすごく見慣れたコードになりましたよね。

シートに書いているファイルパスをただループしたコードです。



このようにファイルの「取得」と「処理」を完全に分断することで、
コード流れを整理しやすく、またテストも格段にやりやすくなります。


まずはこれ↓だけを実行すれば、ファイル取得がうまくいっているかテストできます。

Call フォルダ下にあるすべてのxlsxファイルを取得する("C:\Users\wfsp\Desktop\A")

 

それが問題なさそうなら、今度は適当に1ブックを手で開き、

Call ブックごとの処理(ActiveWorkbook)

これでExcel処理が正常かをテストできるということです。 


これなら脳のメモリを温存しながら検証ができますね。



「親フォルダ⇒子フォルダ⇒ブック⇒シート⇒セル」という多階層を扱うと、
For文だけで最低5つ、しかも内1つは再帰呼出という大変な状況になります。


もしそれをプロシージャ分割なしで組んでしまうと、

                            End If
                        Next
                    Next
                End If
            Next
        End If
    Next
Next

こういう地獄絵図を目にすることになります。



FSOによる「フォルダ階層」を扱う場合は、

  • フォルダ取得とファイル取得はしっかり分ける
  • 特に「再帰呼出」部分にCall以外のコードを書かない
  • 実際の処理はファイルの取得がすべて終わった後でやる

ことをしっかり意識しましょう。

とにかく自分の脳をいたわって、丁寧にプロシージャを分けてあげてください。



以上でFSOによる「すべての下層フォルダの取得」の解説を終わります。

かなり難しい内容だったと思いますが、
これを理解できれば階層化されたオブジェクトの扱いが相当上達すると思います。


前述の通りこれがFileSystemObjectのラスボスですので、
マスター目指して頑張って習得してください。


長文読了、お疲れさまでした!

◇ FileSystemObject入門シリーズ
【1】FileSystemObjectとは - Dir関数群との違い
【2】参照設定と変数宣言
【3】ファイル一覧の取得
【4】すべての下層フォルダの取得(本記事)
【5】FileSystemObjectって何が便利なの?

おまけ:ついでにもうひとつプロシージャ分割

本題と関係ないため割愛しましたが、
せっかくプロシージャを綺麗に分割していましたので、

Sub 実行プロシージャ()
    
    ' ファイル一覧シートを初期化(2行目から最終行までを削除)
    With Worksheets("ファイル一覧")
        .Range(.Rows(2), .Rows(.Rows.Count)).Delete
    End With
    
    ' すべてのファイルパスを取得
    Call フォルダ下にあるすべてのxlsxファイルを一覧シートに書き出す("C:\Users\○○\Desktop\A")
    
    ' 各ファイルへの処理
    Call 一覧シートに記載されたすべてのファイルを処理する
    
End Sub

↓書き換え

Sub 実行プロシージャ()
    
    Call ファイル一覧シートを初期化する
    Call フォルダ下にあるすべてのxlsxファイルを一覧シートに書き出す("C:\Users\○○\Desktop\A")
    Call  一覧シートに記載されたすべてのファイルを処理する
    
End Sub

Sub ファイル一覧シートを初期化する()
    With Worksheets("ファイル一覧")
        .Range(.Rows(2), .Rows(.Rows.Count)).Delete
    End With
End Sub

これもついでに分割しちゃってください。
実行プロシージャがCallだけになって美しくなります。


流石にコメントもいらなくなりますしね。


シートの初期化はよく使いまわすため、プロシージャにしておくと便利です。

ついでにこの手法も持って帰ってください。