マクロでファイルのメタ情報を取得

以下のマクロを実行すると、ユーザーが選択したフォルダ内のJPEGファイルのメタデータ(ファイル情報およびExplorer列の詳細情報)を収集し、その結果を構造体配列 imgList に格納し、シート「メタデータ」に出力します。

Option Explicit

' ******************************************************
' ************** ユーザー定義型(構造体) **************
' ******************************************************

' 画像のメタ情報(Explorerで取得される詳細情報)を格納するための構造体を定義
Public Type ImageInfo
    name As String              ' ファイル名(例: "IMG_1234.jpg")
    fullPath As String          ' ファイルの絶対パス(例: "C:\Temp\IMG_1234.jpg")
    TypeText As String          ' ファイルの種類(例: "JPEG 画像")
    Size As Double              ' ファイルサイズ(バイト単位)
    DateCreated As Date         ' ファイルの作成日時(ファイルシステム情報)
    DateModified As Date        ' ファイルの最終更新日時(ファイルシステム情報)
    BestDate As Date            ' 最適な日時情報(Explorerの「日付」列、撮影日時、作成日時の優先順で設定)
    DateTaken As Date           ' 撮影日時(EXIF情報。Explorer列から取得)
    Tags As String              ' タグ、キーワード(Explorer列から取得)
    Width As Long               ' 画像の幅(ピクセル)
    Height As Long              ' 画像の高さ(ピクセル)
    Rating As Long              ' 評価(0..5、★の数)
End Type

' ******************************************************
' ************** グローバル変数 **************
' ******************************************************

Private imgFolder As String     ' ユーザーが選択したフォルダのパスを格納
Private imgList() As ImageInfo  ' 抽出したメタデータを格納する配列(ImageInfo構造体の動的配列)

' ******************************************************
' ************** メインプロシージャ **************
' ******************************************************

'ファイルのメタ情報取得
Sub GetFileMetadata()
    
    ' オブジェクトの宣言
    Dim fso As Object, folder As Object, file As Object  ' FileSystemObject関連
    Dim sh As Object, ns As Object, it As Object        ' Shell.Application関連 (sh: Shell, ns: Namespace/Folder, it: Item)
    
    ' 変数の宣言
    Dim ext As String           ' ファイルの拡張子
    Dim cnt As Long, i As Long  ' cnt: 対象ファイル数, i: 配列インデックス
    Dim dimText As String, w As Long, h As Long ' 寸法情報(文字列、幅、高さ)

    ' 1. フォルダの選択と初期化
    
    ' Shell Namespace(Explorerの列情報取得に使用するShellオブジェクトを作成)
    Set sh = CreateObject("Shell.Application")
    
    ' ユーザーにフォルダ選択ダイアログを表示(初期パスをC:\Tempに設定)
    ' ns には選択されたフォルダの Shell.Folder オブジェクトが格納される
    Set ns = sh.BrowseForFolder(0, "対象フォルダを選択してください", 0, "C:\Temp")

    ' ユーザーがキャンセルした場合のチェック
    If ns Is Nothing Then Exit Sub

    ' 選択されたフォルダのパスをフルパスで取得
    imgFolder = ns.Self.Path

    ' 2. FileSystemObject (FSO) でファイル走査の準備
    
    Set fso = CreateObject("Scripting.FileSystemObject")
    ' FSOでフォルダオブジェクトを取得
    Set folder = fso.GetFolder(imgFolder)

    ' 3. 対象ファイル(jpg/jpeg)のカウント
    
    ' フォルダ内のファイルを走査し、対象のJPEGファイル数 (cnt) を数える
    cnt = 0
    For Each file In folder.Files
        ext = LCase$(fso.GetExtensionName(file.name)) ' 拡張子を小文字化
        If ext = "jpg" Or ext = "jpeg" Then cnt = cnt + 1
    Next
    
    ' 対象ファイルが0の場合の処理
    If cnt = 0 Then
        MsgBox "画像が見つかりません。", vbExclamation
        Exit Sub
    End If

    ' 4. 配列確保
    
    ' カウントした数に合わせて、ImageInfo配列のサイズを確保 (1ベース)
    ReDim imgList(1 To cnt)

    ' 5. メタ情報の収集と配列への格納
    
    i = 0 ' 配列インデックスをリセット
    For Each file In folder.Files
        ext = LCase$(fso.GetExtensionName(file.name))
        
        ' JPEGファイルのみを対象にする
        If ext = "jpg" Or ext = "jpeg" Then
            i = i + 1 ' 配列インデックスをインクリメント
            
            ' 1) 基本情報(ファイルシステムから取得)
            With imgList(i)
                .name = file.name
                .fullPath = file.Path
                .Size = CDbl(file.Size) ' Double型に変換
                .DateCreated = file.DateCreated
                .DateModified = file.DateLastModified
                .TypeText = "JPEG 画像" ' Explorerで情報が取得できない場合の初期値
            End With

            ' 2) Explorer列からメタデータ取得(Shell.Application機能)
            If Not ns Is Nothing Then
                ' ファイル名から Shell.FolderItem オブジェクト(it)を取得
                Set it = ns.ParseName(file.name)
                
                If Not it Is Nothing Then
                    With imgList(i)
                        
                        ' ファイルの種類(TypeText)をExplorer情報で上書き
                        ' GetDetailByHeader関数で「種類」「Item type」「Type」を優先順に探す
                        .TypeText = GetDetailByHeader(ns, it, "種類", "Item type", "Type")
                        
                        ' BestDate(最適な日付)の取得と補完
                        ' Explorerの「日付」列(あれば)→「撮影日時」→ファイル作成日時の順に試す
                        .BestDate = ParseDateLoose(GetDetailByHeader(ns, it, "日付", "日付時刻", "Date"))
                        If .BestDate = 0 Then .BestDate = ParseDateLoose(GetDetailByHeader(ns, it, "撮影日時", "Date taken"))
                        If .BestDate = 0 Then .BestDate = .DateCreated ' 最終的にファイル作成日時で補完
                        
                        ' 撮影日時(DateTaken)を個別に取得
                        .DateTaken = ParseDateLoose(GetDetailByHeader(ns, it, "撮影日時", "Date taken"))
                        
                        ' タグ/キーワードを取得
                        .Tags = GetDetailByHeader(ns, it, "タグ", "Tags", "Keywords")

                        ' 幅と高さの取得(「幅」「高さ」列から)
                        ' 取得した文字列から数字のみを抽出し、CLngでLong型に変換
                        w = CLng(ExtractOnlyNumbers(GetDetailByHeader(ns, it, "幅", "Width")))
                        h = CLng(ExtractOnlyNumbers(GetDetailByHeader(ns, it, "高さ", "Height")))
                        If .Width = 0 Then .Width = w ' 既にWidth/Heightが設定されていなければ適用
                        If .Height = 0 Then .Height = h
                        
                        ' 寸法情報の取得(「大きさ」列から)
                        ' "4000 x 3000" のような文字列をパースし、幅と高さに設定
                        dimText = GetDetailByHeader(ns, it, "大きさ", "寸法", "Dimensions")
                        ParseDimensions dimText, w, h ' w, h にパース結果がByRefで返る
                        If .Width = 0 Then .Width = w   ' 幅/高さが未設定の場合に、パース結果を適用
                        If .Height = 0 Then .Height = h

                        ' 評価(Rating)の取得(TryGetShellRating関数による優先的な取得)
                        Dim r As Long
                        ' ExtendedProperty経由でRatingを取得を試みる
                        If TryGetShellRating(ns, it, r) Then
                            .Rating = r
                        Else
                            ' 失敗した場合、Explorer列の文字列から評価をパース
                            .Rating = ParseRatingString(GetDetailByHeader(ns, it, "評価", "Rating"))
                        End If
                    End With
                End If
            End If


            ' 3) BestDateの最終的なフォールバック(二重チェック)
            With imgList(i)
                If .BestDate = 0 Then ' BestDateがまだ未設定(0)の場合
                    If .DateTaken <> 0 Then
                        .BestDate = .DateTaken
                    Else
                        .BestDate = .DateCreated ' 最終手段としてファイル作成日時を設定
                    End If
                End If
            End With
        End If
    Next ' For Each file

    ' 6. オブジェクトの解放(ガベージコレクション)
    Set it = Nothing
    Set ns = Nothing
    Set sh = Nothing
    Set folder = Nothing
    Set fso = Nothing
    
    ' ここに、imgList配列を使った後続処理(例: Excelシートへの書き出し)を記述
    Call WriteMetadataToSheet(imgList)
End Sub


' ******************************************************
' ************** 後続処理用プロシージャ **************
' ******************************************************

' --------------------------------------------------------------------------------
' imgList配列の内容をシート「メタデータ」に書き出す
' --------------------------------------------------------------------------------
Public Sub WriteMetadataToSheet(ByRef dataList() As ImageInfo)
    
    Dim ws As Worksheet
    Dim i As Long
    Dim lastRow As Long
    
    ' 対象シート名の設定
    Const SHEET_NAME As String = "メタデータ"
    
    ' -----------------------------------
    ' 1. シートの準備
    ' -----------------------------------
    
    ' シート「メタデータ」の存在を確認し、なければ作成
    On Error Resume Next
    Set ws = ThisWorkbook.Sheets(SHEET_NAME)
    If ws Is Nothing Then
        Set ws = ThisWorkbook.Sheets.Add(After:=ThisWorkbook.Sheets(ThisWorkbook.Sheets.Count))
        ws.name = SHEET_NAME
        
        ' ヘッダーが未記入の場合に備えて(もし未記入ならここで書き込む)
        ' ws.Cells(1, 1).Value = "名前"
        ' ...
    End If
    On Error GoTo 0
    
    ' -----------------------------------
    ' 2. 既存データのクリア(2行目以降)
    ' -----------------------------------
    
    ' 1行目はヘッダーなので残し、2行目以降の既存データをクリアする
    lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
    If lastRow >= 2 Then
        ' データが存在する場合、2行目から最終行までをクリア
        ws.Range("A2:J" & lastRow).ClearContents
    End If
    
    ' -----------------------------------
    ' 3. 配列のデータをシートに書き出し
    ' -----------------------------------
    
    ' imgList配列が空の場合のチェック (LBound > UBound)
    If UBound(dataList) < LBound(dataList) Then Exit Sub
    
    For i = LBound(dataList) To UBound(dataList)
        ' 書き出し対象の行番号 (ヘッダー(1行目) + インデックス(i))
        ' LBoundが1だとすると、i=1で2行目、i=2で3行目
        Dim currentRow As Long
        currentRow = i + 1
        
        With dataList(i)
            ws.Cells(currentRow, "A").Value = .name
            ' B列: 日付時刻 (BestDate)
            ws.Cells(currentRow, "B").Value = .BestDate
            ws.Cells(currentRow, "B").NumberFormat = "yyyy/mm/dd hh:mm:ss" ' 日付時刻形式を適用
            
            ws.Cells(currentRow, "C").Value = .TypeText
            ' D列: サイズ
            ws.Cells(currentRow, "D").Value = .Size
            ws.Cells(currentRow, "D").NumberFormat = "#,##0" ' 数値形式を適用
            
            ws.Cells(currentRow, "E").Value = .Tags
            ws.Cells(currentRow, "F").Value = .DateCreated
            ws.Cells(currentRow, "F").NumberFormat = "yyyy/mm/dd hh:mm:ss"
            
            ws.Cells(currentRow, "G").Value = .DateModified
            ws.Cells(currentRow, "G").NumberFormat = "yyyy/mm/dd hh:mm:ss"
            
            ws.Cells(currentRow, "H").Value = .DateTaken
            ws.Cells(currentRow, "H").NumberFormat = "yyyy/mm/dd hh:mm:ss"
            
            ' I列: 大きさ (Width x Height) の形式で結合
            ws.Cells(currentRow, "I").Value = .Width & " x " & .Height
            
            ws.Cells(currentRow, "J").Value = .Rating
        End With
    Next i
    
    ' -----------------------------------
    ' 4. 表示の調整
    ' -----------------------------------
    ws.Columns.AutoFit
    
End Sub




' ******************************************************
' ************** ユーティリティ関数 **************
' ******************************************************

' --------------------------------------------------------------------------------
' Explorer列から日本語/英語ヘッダ名で値を取得
' (複数の候補ヘッダ(ParamArray headers)を順に探し、最初に見つかった値を返す)
' --------------------------------------------------------------------------------
Private Function GetDetailByHeader(ns As Object, item As Object, ParamArray headers()) As String
    Dim i As Long, h As Variant, name As String, val As String
    ' エラーが発生しても処理を中断しない(存在しない列を参照した場合などに備える)
    On Error Resume Next
    
    For Each h In headers ' 候補のヘッダー名(例: "日付", "Date", "日付時刻")を順に試す
        For i = 0 To 400 ' 0から最大400番目までのExplorer列を走査
            ' 列ヘッダー名を取得
            name = ns.GetDetailsOf(Nothing, i)
            
            If Len(name) > 0 Then
                ' 列名が候補名と一致するかをテキスト比較(大文字小文字無視)でチェック
                If StrComp(name, CStr(h), vbTextCompare) = 0 Then
                    ' ヘッダー名が一致したら、その列インデックス(i)を使って項目の値を取得
                    val = ns.GetDetailsOf(item, i)
                    If Len(val) > 0 Then
                        GetDetailByHeader = val ' 値が取得できたらそれを返して関数を終了
                        Exit Function
                    End If
                End If
            End If
        Next i
    Next h
    
    On Error GoTo 0 ' エラーハンドラをリセット
End Function


' --------------------------------------------------------------------------------
' 日付文字列の緩やかなパース
' ・EXIF形式 "YYYY:MM:DD HH:MM:SS" に対応(コロン区切り)
' ・通常のロケール日付(スラッシュ区切りなど)にも対応
' --------------------------------------------------------------------------------
Private Function ParseDateLoose(ByVal s As String) As Date
    On Error Resume Next
    If Len(s) = 0 Then Exit Function

    ' 1. EXIF形式のパース処理
    ' EXIFの撮影日時は "YYYY:MM:DD HH:MM:SS" の形式になっていることが多い
    If InStr(s, ":") >= 5 And InStr(s, " ") > 0 Then ' 年の部分にコロンとスペースがあるかチェック
        Dim parts() As String, dpart As String, tpart As String, d() As String, tt() As String
        parts = Split(s, " ")
        dpart = parts(0): tpart = parts(1) ' 日付部分と時刻部分に分割
        d = Split(dpart, ":"): tt = Split(tpart, ":") ' 日付と時刻をコロンで分割
        
        If UBound(d) = 2 And UBound(tt) >= 1 Then ' 日付部分が(年,月,日)と時刻部分(時,分,秒)に分割できているか
            ' DateSerialとTimeSerialを使ってDate型に結合
            ParseDateLoose = DateSerial(CInt(d(0)), CInt(d(1)), CInt(d(2))) + _
                             TimeSerial(CInt(tt(0)), CInt(tt(1)), IIf(UBound(tt) >= 2, CInt(tt(2)), 0)) ' 秒がなければ0を設定
            Exit Function
        End If
    End If

    ' 2. 通常ロケール解釈のための前処理
    ' 文字化けや余計な文字を削除し、数字と日付/時刻の区切り文字のみを抽出
    s = ExtractAllowedChars(s) ' ExtractAllowedCharsで"0-9/: "のみを抽出

    ' 3. 抽出後の文字列をCDateで解釈
    If IsDate(s) Then
        ParseDateLoose = CDate(s)
    End If

    On Error GoTo 0
End Function


' ----------------------------------------------------------------------
' 目的: 文字列から数字、スラッシュ、コロン、半角スペースのみを抽出する。
' (ParseDateLoose関数内で、CDateに渡す前のクリーニングに使用)
' ----------------------------------------------------------------------
Private Function ExtractAllowedChars(ByVal originalString As String) As String
    Dim i As Long, char As String, resultString As String
    Const ALLOWED_CHARS As String = "0123456789/: " ' 許可文字リスト
    resultString = ""
    
    For i = 1 To Len(originalString)
        char = Mid(originalString, i, 1)
        If InStr(ALLOWED_CHARS, char) > 0 Then
            resultString = resultString & char
        End If
    Next i
    
    ExtractAllowedChars = resultString
End Function


' ----------------------------------------------------------------------
' 目的: 文字列から数字(0~9)のみを抽出する。
' (幅や高さの文字列から数値部分を抽出するために使用)
' ----------------------------------------------------------------------
Private Function ExtractOnlyNumbers(ByVal originalString As String) As String
    Dim i As Long, char As String, resultString As String
    resultString = ""
    
    For i = 1 To Len(originalString)
        char = Mid(originalString, i, 1)
        ' Charが "0"~"9" の範囲にあるかを確認
        If char >= "0" And char <= "9" Then
            resultString = resultString & char
        End If
    Next i
    
    ExtractOnlyNumbers = resultString
End Function


' ----------------------------------------------------------------------
' 「大きさ」列の文字列 "4000 x 3000" など → 幅・高さにパース
' (×/x、ピクセル/pixels などの単位文字を正規化して対応)
' ----------------------------------------------------------------------
Private Sub ParseDimensions(ByVal s As String, ByRef w As Long, ByRef h As Long)
    Dim parts() As String
    
    ' 全角「×」や単位文字を半角「x」や空文字に置換して正規化
    s = Replace(s, "×", "x")
    s = Replace(s, "ピクセル", "")
    s = Replace(s, "pixels", "")
    s = Trim$(s)
    
    If InStr(s, "x") = 0 Then Exit Sub ' 区切り文字 'x' がなければ終了
    
    parts = Split(s, "x") ' "x"で分割
    
    If UBound(parts) = 1 Then ' 分割結果が2要素(幅と高さ)の場合
        ' それぞれの要素から数字のみを抽出し、val関数で数値化して幅と高さに設定
        w = val(ExtractOnlyNumbers(Trim$(parts(0))))
        h = val(ExtractOnlyNumbers(Trim$(parts(1))))
    End If
End Sub

' ----------------------------------------------------------------------
' ShellのExtendedPropertyを使って評価(Rating)を確実に取得を試みる
' ----------------------------------------------------------------------
Private Function TryGetShellRating(ns As Object, it As Object, ByRef rating05 As Long) As Boolean
    Dim v As Variant, s As String

    ' 1. System.SimpleRating (0..5の数値) を直接取得
    On Error Resume Next
    v = it.ExtendedProperty("System.SimpleRating") ' 0..5
    On Error GoTo 0
    
    If IsNumeric(v) Then
        rating05 = CLng(v)
        TryGetShellRating = True
        Exit Function
    End If

    ' 2. System.RatingText ("3 個の星" などの文字列) を取得しパース
    On Error Resume Next
    v = it.ExtendedProperty("System.RatingText") ' "未評価" / "3 個の星" 等
    On Error GoTo 0
    s = ""
    If Not IsEmpty(v) And Not IsNull(v) Then s = CStr(v)
    
    If Len(s) > 0 Then
        rating05 = ParseRatingString(s)
        TryGetShellRating = True
        Exit Function
    End If

    ' 3. 列名("評価", "Rating")経由で取得しパース
    s = GetDetailByHeader(ns, it, "評価", "Rating")
    If Len(s) > 0 Then
        rating05 = ParseRatingString(s)
        TryGetShellRating = True
        Exit Function
    End If
    
    TryGetShellRating = False ' どこからも取得できなかった場合
End Function


' ----------------------------------------------------------------------
' 評価の文字列→0..5(★×5)に変換
' ・"未評価"/"unrated"/"3 個の星"/数値(0..5)などに対応
' ----------------------------------------------------------------------
Public Function ParseRatingString(ByVal s As String) As Long
    Dim t As String, stars As Long, n As Double
    t = Trim$(s)
    If Len(t) = 0 Then Exit Function

    ' 1. "未評価" / "unrated" のチェック
    If InStr(1, t, "未評価", vbTextCompare) > 0 Or InStr(1, t, "unrated", vbTextCompare) > 0 Then
        ParseRatingString = 0
        Exit Function
    End If

    ' 2. ★の個数を数える
    stars = Len(t) - Len(Replace(t, "★", ""))
    If stars > 0 Then
        If stars > 5 Then stars = 5 ' 5を超える場合は5に制限
        ParseRatingString = stars
        Exit Function
    End If

    ' 3. 数値解釈(例: "3", "3.0", "80" (パーセント評価の可能性))
    n = val(t)
    If n <= 0 Then
        ParseRatingString = 0
    Else
        ' nが1?5ならそのまま★の数として、それ以外(例: 80%)はそのまま数値を返している
        ' ここでは、ExtendedPropertyで取得できなかった場合のフォールバックとしてCLng(n)を返す
        ParseRatingString = CLng(n)
    End If
End Function

詳細な説明

このマクロは、Windowsの機能(Shell.Application)とファイルシステム機能(FileSystemObject)を組み合わせて利用し、JPEG画像ファイルの詳細なメタデータを自動的に収集し、整形してExcelシートに書き出す一連の処理を実行します。

1. フォルダの選択と準備

  • フォルダ選択: Shell.Application.BrowseForFolder メソッドを使って、ユーザーに処理対象のフォルダを選択させます。
  • 初期化: 選択されたフォルダのパスを imgFolder 変数に格納し、後の処理で使用する FSO (FileSystemObject) オブジェクトと Shell Namespace (ns) オブジェクトを準備します。

2. ファイルの走査と配列の準備

  • カウント: imgFolder 内の全ファイルを走査し、拡張子が "jpg" または "jpeg" であるファイルの総数をカウントします。
  • 配列確保: カウントした数に基づき、メタデータを格納するための構造体配列 imgList() のサイズを動的に確保します。

3. メタデータの収集と格納

  • FSO情報の取得: 各JPEGファイルに対し、まず FileSystemObject を使ってファイルシステムレベルの基本情報(ファイル名、フルパス、サイズ、作成日時、更新日時など)を取得し、配列に格納します。
  • Explorer列情報の取得: Shell.Application の機能を用いて、ファイルシステム情報だけでは得られない詳細なメタデータ(EXIF由来の撮影日時、タグ、幅、高さ、評価など)を取得します。
    • ns.ParseName(ファイル名) でファイルアイテムオブジェクト (it) を取得します。
    • GetDetailByHeader 関数(ヘルパー関数)を使用し、日本語・英語の複数の列名候補(例:「日付」「Date taken」など)を検索して、OSの言語環境に依存せずロバストに値を取得します。
  • 日付の補完: 最適な日時(BestDate)を決定するため、Explorerの「日付」列、EXIFの「撮影日時」、ファイルシステム上の「作成日時」の順に値を試し、有効な日付で補完します。
  • 寸法の解析: 「大きさ/Dimensions」列の文字列(例: “4000 x 3000 ピクセル”)を ParseDimensions 関数(ヘルパー関数)で解析し、数値の幅と高さとして抽出します。

4. Excelシートへの書き出し

  • 後続処理の呼び出し: 最後に、収集・整形された全データを含む配列 imgList を引数として WriteMetadataToSheet プロシージャを呼び出します。
  • 出力: このプロシージャは、シート「メタデータ」の2行目以降に、imgList の全データを書き出します。I列の「大きさ」は、Width x Height の形式に整形されます。

実行結果の「メタデータ」シート

エクスプローラでの表示

ご覧のように、エクスプローラでの情報を、Excelに取得でしました。

GetDetailByHeader関数の説明

Follow me!