和風スパゲティのレシピ

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

40本目:複数ブックの統合

Excel&VBA解説サイト「エクセルの神髄」様出題の問題集、
VBA100本ノック」に対する私の回答と解説のページです。

100本ノックの出題リストはこちらから
excel-ubara.com

出題:40本目:複数ブックの統合

#VBA100本ノック 40本目
「data」フォルダ内のExcelファイルについて、シート「2020年12月」のA1からの連続表範囲を集めます。※このシートが無いファイルもある。
自身の既存シート「2020年12月」に集めてください。
1行目は見出しなので2件目からは除く。
※ブック指定と「data」のパス位置は任意

取込結果サンプル

◇ 出題ページはこちら


なお、今回の処理はライブラリが威力を発揮する回で、
汎用関数を1つCallしただけでほぼ完成してしまいました。

それはそれでライブラリづくりの参考にしていただけそうですが、
ある程度しっかり記述したコードも読みたい方もいるかと思います。


よって本ページでは2つの解答を記載しておきますので、
お好きな方をご覧ください。

ソースコード(ライブラリ未使用版)

※ 本問を一発回答する万能型プロシージャを使用していないだけで、
「Get最終行」「指定行より下を削除する」「Isシートが存在」など、
頻出コードを汎用関数化したプロシージャは通常通り使用しています。

メインモジュール

Option Explicit

Private 対象年月 As String
Dim ws出力シート As Worksheet

' 100本ノック040:複数ブックの統合
Sub フォルダ内の対象年月シートを統合する()
    
    ' 対象月の指定
    対象年月 = "2020年12月"
    Set ws出力シート = ThisWorkbook.Worksheets(対象年月)
    
    ' 出力シートをクリア
    Call 指定行より下をすべて削除する(ws出力シート, 2)
    
    ' 対象フォルダを取得
    Dim 対象フォルダ As Folder
    Set 対象フォルダ = FSO.GetFolder("C:\Users\wfsp\Desktop\data")
    
    ' フォルダ内のExcelファイルをループ
    Dim ファイル As File
    For Each ファイル In 対象フォルダ.Files
        If FSO.GetExtensionName(ファイル.Name) Like "xls*" Then
        
            ' 読み取り専用で開く
            Dim wb取込ブック As Workbook
            Set wb取込ブック = Workbooks.Open(ファイル.Path, , True)
            
            ' ■ ブックごとの処理
            Call ブックごとの処理(wb取込ブック)
        
            ' 保存せず閉じる
            wb取込ブック.Close False
        
        End If
    Next
    
    Set ws出力シート = Nothing
End Sub

' ブックごとの処理
Private Sub ブックごとの処理(wb取込ブック As Workbook)
    
    ' 対象シートを取得 (なければExit)
    If Isシートが存在する(対象年月, wb取込ブック) = False Then Exit Sub
    Dim ws取込シート As Worksheet
    Set ws取込シート = wb取込ブック.Worksheets(対象年月)
    
    ' 出力シートの最終行の次行を取得
    Dim 貼付起点セル As Range
    Set 貼付起点セル = ws出力シート.Cells(Get最終行(ws出力シート) + 1, 1)
    
    ' 取込シートのUsedRangeから見出しを除去
    Dim コピーエリア As Range
    Set コピーエリア = GetUsedRange指定行以下(ws取込シート, 2)
    
    ' コピーを実行
    If Not コピーエリア Is Nothing Then
        コピーエリア.Copy 貼付起点セル
    End If
        
End Sub

汎用関数モジュール

Option Explicit

' FileSystemObjectの短縮取得
' 参考:https://www.limecode.jp/entry/utility/shortcall-filesystemobject
Public FSO As New FileSystemObject

' UsedRangeのカット
Function GetUsedRange指定行以下(対象シート As Worksheet, 指定行 As Long) As Range
    With 対象シート
        Set GetUsedRange指定行以下 = Intersect(.UsedRange, .Rows(指定行).Resize(.UsedRange.Rows.Count))
    End With
End Function

' 最終行の取得
' 参考:https://www.limecode.jp/entry/library/get-lastrow-lastcolumn
Function Get最終行(指定オブジェクト As Variant, Optional ByVal C As Long = -1) As Long

    ' 渡されたオブジェクトからセル範囲を取得
    Dim 対象セル範囲 As Range
    Select Case TypeName(指定オブジェクト)
    Case "Range"
        If 指定オブジェクト.Cells.CountLarge = 1 Then ' 単独セルにはCurrentRegionを取る
            Set 対象セル範囲 = 指定オブジェクト.CurrentRegion
        Else
            Set 対象セル範囲 = 指定オブジェクト
        End If
    Case "Worksheet"
        Set 対象セル範囲 = 指定オブジェクト.UsedRange
    Case "AutoFilter", "ListObject"
        Set 対象セル範囲 = 指定オブジェクト.Range
    Case Else
        Err.Raise 1000, , "対象外のオブジェクト「" & TypeName(指定オブジェクト) & "」が指定されました。"
    End Select

    ' エリアの最終行を取得
    Get最終行 = 対象セル範囲.Rows.Count + 対象セル範囲.Row - 1

    ' 列が指定されていればその列の入力最終行を取得
    If C <> -1 Then
        Do While 対象セル範囲.Worksheet.Cells(Get最終行, C) = ""
            Get最終行 = Get最終行 - 1
            If Get最終行 < 対象セル範囲.Row Then
                Get最終行 = 0
                Exit Function
            End If
        Loop
    End If

End Function

' シートの存在判定
' 参考:https://www.limecode.jp/entry/utility/existsworksheet
Function Isシートが存在する(判定シート名 As String, 指定ブック As Workbook) As Boolean

    ' ブック内の全シートを走査
    Dim ws As Worksheet
    For Each ws In 指定ブック.Worksheets

        ' シート名が一致したらTrueを返してExit
        If ws.Name = 判定シート名 Then
            Isシートが存在する = True
            Exit Function
        End If

    Next

End Function

' 指定行より下の削除
' 参考:https://www.limecode.jp/entry/utility/delete-rows-below-to-last
Sub 指定行より下をすべて削除する(対象シート As Worksheet, 指定行 As Long)
    With 対象シート
    
        Call フィルターをクリアする(対象シート)
        
        .Range(.Rows(指定行), .Rows(.Rows.Count)).Delete
        
    End With
End Sub

' フィルターのクリア
' ◆ ブログ未掲載
Sub フィルターをクリアする(対象シート As Worksheet)
    If 対象シート.AutoFilterMode = True Then
        If 対象シート.AutoFilter.FilterMode = True Then
            対象シート.AutoFilter.ShowAllData
        End If
    End If
End Sub

解説

FileSystemObjectを利用したストレートなフォルダ内のファイルループと、
シート・データエリアの取得 ⇒ セル範囲のコピーという、
基本的なコードを組み合わせたコードです。


メインプロシージャ・サブプロシージャ・汎用関数プロシージャと、
ちょうどいいプロシージャ分割の復習問題と思いました。


頻出コードを汎用関数化1行にすることでコードを見やすくしつつ、
ループの中身を丸ごとプロシージャ化して外に出し、
For Each 文の全体像が見やすくなるように整理してみました。


プロシージャ分割の参考にしてみてください。

ソースコード(ライブラリ使用版)

メインモジュール

' 100本ノック040:複数ブックの統合
Sub フォルダ内の対象年月シートを統合する_()
    
    ' 対象月の指定
    Dim 対象年月 As String: 対象年月 = "2020年12月"
    Set ws出力シート = ThisWorkbook.Worksheets(対象年月)
    
    ' 出力シートをクリア
    Call 指定行より下をすべて削除する(ws出力シート, 2)
    
    ' ■ フォルダ内の全xlsxファイルの対象名シートを結合
    Dim ws統合シート As Worksheet
    Set ws統合シート = 指定フォルダ内のすべてのブックを結合する( _
        "C:\Users\wfsp\Desktop\data", 1, , "xlsx", 対象年月)
    
    ' 本ブックの対象シートへコピーして新規シートは閉じる
    ' ※ ライブラリが新規ブック作成仕様のため
    ws統合シート.UsedRange.Copy ws出力シート.Range("A1")
    ws統合シート.Parent.Close False
    
End Sub

汎用関数モジュール

Option Explicit

' FileSystemObjectの短縮取得
' 参考:https://www.limecode.jp/entry/utility/shortcall-filesystemobject
Public FSO As New FileSystemObject

' フォルダ内ファイルの結合
' 参考:https://www.limecode.jp/entry/tools/merge-all-workbooks-in-folder
Function 指定フォルダ内のすべてのブックを結合する _
    (Path指定フォルダ As String, R_見出し行 As Long _
    , Optional ByVal ファイルLike条件 As String = "*", Optional 対象拡張子 As String = "xlsx" _
    , Optional ByVal シート名Like条件 As String = "*" _
    , Optional ファイル名出力始点セル As Range, Optional 読込結果出力始点セル As Range) As Worksheet
    
    ' 対象フォルダを取得
    If FSO.FolderExists(Path指定フォルダ) = False Then Exit Function
    Dim 対象フォルダ As Folder
    Set 対象フォルダ = FSO.GetFolder(Path指定フォルダ)
    If 対象フォルダ.Files.Count = 0 Then Exit Function
        
    ' 出力するシートは第1ブックの第1シートを利用
    Dim ws出力シート As Worksheet
    
    ' Like条件テキストを生成
    ファイルLike条件 = ファイルLike条件 & "." & 対象拡張子

    ' ファイルの走査ログを出力する各セルをSet
    Dim ファイル名出力セル As Range: Set ファイル名出力セル = ファイル名出力始点セル
    Dim 読込結果出力セル As Range: Set 読込結果出力セル = 読込結果出力始点セル

    ' 指定フォルダ内のすべてのファイルを走査
    Dim 読取結果テキスト As String
    Dim ファイル As File
    For Each ファイル In 対象フォルダ.Files
        読取結果テキスト = ""
        
        ' 条件を満たすファイルを処理
        If ファイル.Name Like ファイルLike条件 Then
            Call 対象ブック内のすべてのシートを取り込む(ファイル.Path, ws出力シート _
                , R_見出し行, シート名Like条件, 読取結果テキスト)
        Else
            読取結果テキスト = "ファイル名条件に合致せず"
        End If
        
        ' 読取ログの出力
        If Not ファイル名出力セル Is Nothing Then
            ファイル名出力セル.Value = ファイル.Name
            Set ファイル名出力セル = ファイル名出力セル.Offset(1)
        End If
        If Not 読込結果出力セル Is Nothing Then
            読込結果出力セル.Value = 読取結果テキスト
            Set 読込結果出力セル = 読込結果出力セル.Offset(1)
        End If

    Next ' 指定フォルダ内のすべてのファイルを走査
    
    Set 指定フォルダ内のすべてのブックを結合する = ws出力シート

End Function

' ブックごとの取込処理
Private Sub 対象ブック内のすべてのシートを取り込む(ファイルパス As String, ws出力シート As Worksheet _
    , R_見出し行 As Long, シート名Like条件 As String, ByRef 読取結果テキスト As String)

    ' ファイルを開く
    Dim wb読取ブック As Workbook: Set wb読取ブック = Nothing
    On Error Resume Next
    Set wb読取ブック = Workbooks.Open(ファイルパス)
    On Error GoTo 0

    If wb読取ブック Is Nothing Then
        読取結果テキスト = "ブックがエラーで開けませんでした。"
        Exit Sub
    End If
    
    ' 開いたブック内の条件合致シートをループ
    Dim cnt読取シート数 As Long: cnt読取シート数 = 0
    Dim ws読取シート As Worksheet
    For Each ws読取シート In wb読取ブック.Worksheets
        If ws読取シート.Name Like シート名Like条件 Then
    
            ' 最初に読み取ったシートをコピーして出力シートとして使う
            If ws出力シート Is Nothing Then
                ws読取シート.Copy
                Set ws出力シート = ActiveSheet
                cnt読取シート数 = cnt読取シート数 + 1
            
            Else
                
                ' UsedRangeのうち見出し行より下を取得
                Dim コピーエリア As Range
                Set コピーエリア = GetUsedRange指定行以下(ws読取シート, R_見出し行 + 1)
                
                ' コピーを実行
                If Not コピーエリア Is Nothing Then
                    コピーエリア.Copy ws出力シート.Cells(Get最終行(ws出力シート) + 1, コピーエリア.Column)
                    cnt読取シート数 = cnt読取シート数 + 1
                End If
            
            End If
    
        End If
    Next ' 開いたブック内の条件合致シートをループ
        
    読取結果テキスト = cnt読取シート数 & "枚のシートを取込"
    wb読取ブック.Close False
            
End Sub

' UsedRangeのカット
Function GetUsedRange指定行以下(対象シート As Worksheet, 指定行 As Long) As Range
    With 対象シート
        Set GetUsedRange指定行以下 = Intersect(.UsedRange, .Rows(指定行).Resize(.UsedRange.Rows.Count))
    End With
End Function

' 最終行の取得
' 参考:https://www.limecode.jp/entry/library/get-lastrow-lastcolumn
Function Get最終行(指定オブジェクト As Variant, Optional ByVal C As Long = -1) As Long

    ' 渡されたオブジェクトからセル範囲を取得
    Dim 対象セル範囲 As Range
    Select Case TypeName(指定オブジェクト)
    Case "Range"
        If 指定オブジェクト.Cells.CountLarge = 1 Then ' 単独セルにはCurrentRegionを取る
            Set 対象セル範囲 = 指定オブジェクト.CurrentRegion
        Else
            Set 対象セル範囲 = 指定オブジェクト
        End If
    Case "Worksheet"
        Set 対象セル範囲 = 指定オブジェクト.UsedRange
    Case "AutoFilter", "ListObject"
        Set 対象セル範囲 = 指定オブジェクト.Range
    Case Else
        Err.Raise 1000, , "対象外のオブジェクト「" & TypeName(指定オブジェクト) & "」が指定されました。"
    End Select

    ' エリアの最終行を取得
    Get最終行 = 対象セル範囲.Rows.Count + 対象セル範囲.Row - 1

    ' 列が指定されていればその列の入力最終行を取得
    If C <> -1 Then
        Do While 対象セル範囲.Worksheet.Cells(Get最終行, C) = ""
            Get最終行 = Get最終行 - 1
            If Get最終行 < 対象セル範囲.Row Then
                Get最終行 = 0
                Exit Function
            End If
        Loop
    End If

End Function

' 指定行より下の削除
' 参考:https://www.limecode.jp/entry/utility/delete-rows-below-to-last
Sub 指定行より下をすべて削除する(対象シート As Worksheet, 指定行 As Long)
    With 対象シート
    
        Call フィルターをクリアする(対象シート)
        
        .Range(.Rows(指定行), .Rows(.Rows.Count)).Delete
        
    End With
End Sub

' フィルターのクリア
' ◆ ブログ未掲載
Sub フィルターをクリアする(対象シート As Worksheet)
    If 対象シート.AutoFilterMode = True Then
        If 対象シート.AutoFilter.FilterMode = True Then
            対象シート.AutoFilter.ShowAllData
        End If
    End If
End Sub

解説

「Function 指定フォルダ内のすべてのブックを結合する」という、
万能型の比較的規模が大きい汎用関数を活用したコードです。

メインコードはほぼこの関数のCallで処理が終わっていますね。


この関数は「フォルダパス」「ファイル名条件」「シート名条件」を受け取ってくれるため、「dataフォルダ」「.xls*」「2020年12月」を渡すだけで取込が完了します。

このライブラリは取込ログを出力する機能も持っていますが、
今回は不要なため使用しておりません。


こういったよくある処理を万能型の汎用関数にしておけば、
最初に作るときはかなり時間を割きますが、それに見合う効力があります。

フォルダ内のファイル走査をよく行う方は、
是非ともライブラリに加えてあげてください。