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ファイルを入れて実行した結果がこちらです。
解説
少し長いですが、Debug.Printしていた前項のコードとの違いは単純です。
前半部分は第2Subを以下の通り書き換えただけです。
Debug.Print 親フォルダ.Path ' ↓ 書き換え Call ファイル情報をシートに書き出す(親フォルダ)
そして実際にxlsxファイルを取得するFor Each文は、
Callしている第3Subの中に書きました。
このように、実際の処理をプロシージャに分けることで、
一番複雑なメインSubの改修をCall1行に抑えることができています。
最初は処理を追うのが少し難しいかもしれませんが、じっくり読んでみてください。
再帰のような難しいプロシージャ内に別の処理を書いてしまうと、
高確率で脳みそのメモリが足りなくなります。
そんな状態で再帰をいじろうものなら、どの順番で実行されるのかがわからない、
俗にいうスパゲティコードを生んでしまうかもしれません。
しかし逆にプロシージャにさえ分けてしまえば、
それぞれの内部はただの教本コードで済んだりします。
複雑な構造のオブジェクトを処理する場合は、
対象のオブジェクトごとに丁寧にプロシージャを分ける癖をつけましょう。
実際のブックへの処理を追加する
さて最後に、各ブックを実際に処理するコードを書きましょう。
- ブックを開く
- 第1シートのA1セルに1を入力
- ブックを保存する
- ブックを閉じる
この処理を追加します。
さてこの処理をどこに書くかという問題ですが、
このコードは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だけになって美しくなります。
流石にコメントもいらなくなりますしね。
シートの初期化はよく使いまわすため、プロシージャにしておくと便利です。
ついでにこの手法も持って帰ってください。