ファイルやフォルダのパス情報取得について(例題:拡張子一括変更マクロ)
- 導入
- ファイルやフォルダのパス情報取得の具体例まとめ
- 例題:拡張子一括変更マクロ(設計)
- 例題:拡張子一括変更マクロ(FileSystemObjectを使った実装)
- 例題:拡張子一括変更マクロ(FileSystemObjectを使わない実装)
- 補足(例外について)
- おまけ(plantUMLのソース)
導入
先日、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を使って設計図を描いてみます。
要件定義(やりたいことの文章化)
拡張子一括変更マクロでやりたいことを文章で定義すると、以下の様になります。
対象フォルダに存在する全てのファイルの拡張子を所定の拡張子に変更すること。 なお、対象フォルダのサブフォルダ配下の全ファイルを処理の対象とする。
設計(クラス図)
要件定義から考えて、フォルダクラスとファイルクラスを以下の様にモデル化してみました。
設計(シーケンス図)
クラス図で洗い出した操作をどの様なフローで呼び出されるべきかを考えると、サブフォルダを全て処理するためには拡張子一括変更処理を再帰呼び出しする必要がありました。
シーケンス図を以下の様に描いてみます。
設計(クラス図(再考))
シーケンス図から、「対象フォルダパス」と「変更後拡張子」の情報がクラス図に無いことに気付きました。
拡張子一括変更処理を呼び出す側がその情報を持っていることを、クラス図に追記してみます。
こんな感じでしょうか?
議論の余地はかなりあると思いますが。。。
例題:拡張子一括変更マクロ(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"に変更するように上記のマクロを実行すると実行時エラーとなります。
拡張子変更後のファイルの存在チェックや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