たのしいみかんゼリーのブログ

VBAのTipsを発信しています。

ファイルやフォルダのパス情報取得について(例題:拡張子一括変更マクロ)

導入

先日、iPhoneで撮った写真(拡張子:.heic)をアルバム作成サービスにアップロードしようとしたところ、拡張子が対応しておらず困ったという事がありました。
色々と試した結果、写真の拡張子を".jpg"に変更することで画像ファイルとしての取扱いにも差し障りなく正常にアップロードできることがわかったので、拡張子一括変更マクロを考えてみました。
その際、ファイルやフォルダのパス情報を取得・編集する必要があったので、備忘のために各メソッド等で取得できるパス情報の具体例をまとめておこうと思います。

ファイルやフォルダのパス情報取得の具体例まとめ

以下の通りです。

オブジェクト/VBA関数 プロパティ/メソッド 具体例
Scripting.Folder Name "testFolder"
Scripting.Folder Path "C:\test\testFolder"
Scripting.File Name "TestFile.jpg"
Scripting.File Path "C:\test\testFolder\TestFile.jpg"
Scripting.FileSystemObject GetAbsolutePathName(パス) "C:\test\testFolder\TestFile.jpg"
Scripting.FileSystemObject GetBaseName(パス) "TestFile"
Scripting.FileSystemObject GetDriveName(パス) "C:"
Scripting.FileSystemObject GetExtensionName(パス) "jpg"
Scripting.FileSystemObject GetFileName(パス) "TestFile.jpg"
Scripting.FileSystemObject GetParentFolderName(パス) "C:\test\testFolder"
VBA関数 Dir(パス) "TestFile.jpg"
パスが存在しない場合は空文字
VBA関数 CurDir() "C:\test\testFolder"
Workbook Name "拡張子一括変更.xlsm"
Workbook FullName "C:\test\拡張子一括変更.xlsm"
Workbook Path "C:\test"

例題:拡張子一括変更マクロ(設計)

せっかくなので、UMLを使って設計図を描いてみます。

要件定義(やりたいことの文章化)

拡張子一括変更マクロでやりたいことを文章で定義すると、以下の様になります。

対象フォルダに存在する全てのファイルの拡張子を所定の拡張子に変更すること。
なお、対象フォルダのサブフォルダ配下の全ファイルを処理の対象とする。

設計(クラス図)

要件定義から考えて、フォルダクラスとファイルクラスを以下の様にモデル化してみました。

f:id:e0m0jelly:20210429210116p:plain
図01_【設計】クラス図

設計(シーケンス図)

クラス図で洗い出した操作をどの様なフローで呼び出されるべきかを考えると、サブフォルダを全て処理するためには拡張子一括変更処理を再帰呼び出しする必要がありました。
シーケンス図を以下の様に描いてみます。

f:id:e0m0jelly:20210429222819p:plain
図02_【設計】シーケンス図

設計(クラス図(再考))

シーケンス図から、「対象フォルダパス」と「変更後拡張子」の情報がクラス図に無いことに気付きました。
拡張子一括変更処理を呼び出す側がその情報を持っていることを、クラス図に追記してみます。

f:id:e0m0jelly:20210429210722p:plain
図03_【設計】クラス図(シーケンス図による気付きを修正)

こんな感じでしょうか?
議論の余地はかなりあると思いますが。。。

例題:拡張子一括変更マクロ(FileSystemObjectを使った実装)

皆さんはFileSystemObjectをよく使うでしょうか?
大抵のプロパティやメソッドが揃っているので、僕はフォルダやファイルを扱うときは真っ先に飛びつきがちです。
今回もまずはFileSystemObjectを使って実装してみました。以下の通りです。

'拡張子一括変更処理(FileSystemObject使うver)
Private Function changeExtensionByFso(folderPath As String, afterExtension As String)
    'ローカル変数
    Dim fso As FileSystemObject
    Dim targetFolder As Folder
    Dim tempFolder As Folder
    Dim tempFile As File
    
    'ローカル変数初期化
    Set fso = New FileSystemObject
    Set targetFolder = fso.GetFolder(folderPath)
    
    'サブフォルダの取得 & サブフォルダの数だけループし、拡張子一括変更処理を再帰呼び出し。
    For Each tempFolder In targetFolder.SubFolders
        Call changeExtensionByFso(tempFolder.Path, afterExtension)
    Next
    
    'ファイルの取得 & ファイルの数だけループし、拡張子の変更を実施。
    For Each tempFile In targetFolder.Files
        fso.MoveFile _
            Source:=tempFile.Path, _
            Destination:=folderPath & "\" & fso.GetBaseName(tempFile.Path) & afterExtension
    Next
    
End Function

例題:拡張子一括変更マクロ(FileSystemObjectを使わない実装)

今回の記事を書くにあたって色々調べていくと、Dir関数とNameステートメントを使えばFileSystemObjectを使わなくても実装できそうだという事がわかりました。
FileSystemObjectとは違い、細かいところを気にかける必要がありましたが何とか以下の様に実装できました。

'拡張子一括変更処理(FileSystemObject使わないver)
Private Function changeExtensionByRegularLibrary(folderPath As String, afterExtension As String)
    'ローカル変数
    Dim dirResult As String
    Dim subFolderPath() As String
    Dim filePath() As String
    Dim i As Integer
    
    'ローカル変数初期化
    ChDir folderPath
    dirResult = Dir("*", vbDirectory)
    ReDim subFolderPath(0)
    ReDim filePath(0)
    
    'サブフォルダの取得 & ファイルの取得
    Do Until dirResult = ""
        If dirResult <> "." And dirResult <> ".." Then
            If (GetAttr(dirResult) And vbDirectory) = vbDirectory Then
                subFolderPath(UBound(subFolderPath)) = folderPath & "\" & dirResult
                ReDim Preserve subFolderPath(UBound(subFolderPath) + 1)
            Else
                filePath(UBound(filePath)) = folderPath & "\" & dirResult
                ReDim Preserve filePath(UBound(filePath) + 1)
            End If
        End If
        dirResult = Dir()
    Loop
    
    'サブフォルダの数だけループし、拡張子一括変更処理を再帰呼び出し。
    For i = LBound(subFolderPath) To UBound(subFolderPath) - 1
        Call changeExtensionByRegularLibrary(subFolderPath(i), afterExtension)
    Next
    
    'ファイルの数だけループし、拡張子の変更を実施。
    For i = LBound(filePath) To UBound(filePath) - 1
        Name filePath(i) As Left(filePath(i), InStrRev(filePath(i), ".") - 1) & afterExtension
    Next
    
End Function

Dir関数を使うと一つのループ処理でフォルダとファイル両方の名前を取得できたので、ファイルの取得処理のタイミングがシーケンス図とは乖離した状態にしました。
趣味でやっていることなので、まぁ良しとしましょう。

補足(例外について)

上記の実装では、例外を考慮していません。
より安全にするのであれば、例えば次の様な例外の処理を実装しておく必要があります。

拡張子変更後のファイルが存在している場合

例えば「photo1.heic」と「photo1.jpg」がある場合に、拡張子を".jpg"に変更するように上記のマクロを実行すると実行時エラーとなります。

f:id:e0m0jelly:20210429214819p:plain
図04_拡張子変更後ファイルが存在する場合のエラー

拡張子変更後のファイルの存在チェックやOn Errorステートメントを利用して処理が止まらないようにすることと、何もしないのかそれとも例外情報を通知するのかといった例外の取扱い方針の決定および実装が必要になります。

拡張子無しのファイルの扱い

実際に試してみたところFileSystemObjectを使う場合は考慮する必要が無く、例えば「photo1」というファイルを「photo1.jpg」に変更することができていました。(さすが!)
FileSystemObjectを使わない場合では、"."という文字列の検索により拡張子を識別しています。該当のコードは以下の通りです。

Name filePath(i) As Left(filePath(i), InStrRev(filePath(i), ".") - 1) & afterExtension

そのため、拡張子が無い場合は以下の2通りのバグが発生することになります。

  • ファイルパスに1つも"."が含まれない場合
    InStrRev関数の戻り値が0となるため、Left関数の引数lengthに-1を渡してしまい「引数が不正です」という実行時エラーが発生します。

  • ファイルパスに"."が含まれる場合(いずれかの親フォルダの名前に"."が含まれる場合)
    例えばファイルパスが"C:\test\folderNo.1\photo1"の場合は、ファイルパス"C:\test\folderNo.jpg"に移動されてしまいます。

対処としては、上記のコードで拡張子が無い場合の条件分岐を設けて正しい移動先ファイルパスを指定する必要があります。

おまけ(plantUMLのソース)

UMLを描くにあたっては、plantUMLを利用しました。
おまけとしてそのソースを記載しておきます。

  • 01_【設計】クラス図
@startuml
    skinparam defaultFontName MS ゴシック

    class "フォルダ" as folder{
        フォルダパス
        --
        サブフォルダの取得()
        ファイルの取得()
    }
    class "ファイル" as file{
        ファイルパス
        --
        拡張子の変更(変更後拡張子)
    }

    hide circle

    folder "格納先フォルダ" o-- "サブフォルダ" folder
    folder o-- file
@enduml
  • 図02_【設計】シーケンス図
@startuml
    skinparam defaultFontName MS ゴシック
    
    title 拡張子一括変更処理(フォルダパス,変更後拡張子)
    
    actor "VBA" as vba
    participant "フォルダ" as folder
    participant "ファイル" as file
    
    vba -> folder : <<create>>\nnew(フォルダパス)
    vba -> folder : サブフォルダの取得()
    loop サブフォルダの数だけ
        |||
        ref over vba,folder,file : 拡張子一括変更処理(サブフォルダパス,変更後拡張子)
        |||
    end
    vba -> folder : ファイルの取得()
    loop ファイルの数だけ
        vba -> file : 拡張子の変更(変更後拡張子)
    end
@enduml
  • 図03_【設計】クラス図(シーケンス図による気付きを修正)
@startuml
    skinparam defaultFontName MS ゴシック

    class "VBA" as vba{
        対象フォルダパス
        変更後拡張子
        --
        拡張子一括変更処理(フォルダパス,変更後拡張子)
    }
    together {
        class "フォルダ" as folder{
            フォルダパス
            --
            サブフォルダの取得()
            ファイルの取得()
        }
        class "ファイル" as file{
            ファイルパス
            --
            拡張子の変更(変更後拡張子)
        }
    }

    hide circle

    vba x.right.> folder
    vba x.right.> file
    folder "格納先フォルダ" o-- "サブフォルダ" folder
    folder o-- file
@enduml