【実務・中級編】【中級者向け】FSOを活用した「月別・取引先別」フォルダの自動生成と添付ファイルの仕分け保存 – Outlook VBA解析バイブル

スポンサーリンク
スポンサーリンク

[Mastering Outlook VBA]

――「ただ保存する」のは素人の仕事だ。FSOで構築する、堅牢な階層型ファイル管理システム

業務自動化の現場において、多くの開発者が陥る「罠」がある。
それは、「動くコード」を書くことに満足し、「壊れない設計」を軽視することだ。

Outlookの添付ファイルを保存するだけのツールを作るなら、`SaveAsFile` メソッドを一行書くだけで済む。しかし、実務で運用される「プロダクション・レベル」のツールに求められるのは、単なる保存ではない。

  • 「2023年/10月/株式会社〇〇/」といった階層構造を、フォルダが存在しない状態から動的に生成できるか?
  • 送信者名に含まれる「/」や「*」といった、ファイルシステムで使用不可能な禁止文字をどうハンドリングするか?
  • 同名のファイルが既に存在した場合、上書きしてデータを破壊せずに、別名で安全に保存できるか?

これらが設計段階で考慮されていなければ、そのツールは数週間後、予期せぬエラーで止まり、現場の信頼を失うことになる。

本記事では、`FileSystemObject` (FSO) を駆使し、プロフェッショナルが実装すべき「月別・取引先別」の自動仕分けロジックを伝授する。

1. なぜ `MkDir` ではなく `FileSystemObject` なのか

VBA標準の `MkDir` ステートメントは非常に脆弱だ。親フォルダが存在しない状態で子フォルダを作ろうとすれば、無慈悲な実行時エラーを吐いて停止する。また、フォルダの存在確認(`Dir` 関数)も、文字列操作の柔軟性に欠ける。

我々が採用すべきは、FileSystemObject (FSO) だ。
FSOはオブジェクト指向に基づいたファイル操作を提供し、「フォルダの存在確認」「パスの構成要素の分解」「階層の作成」を、極めてロジカルかつ堅牢に記述することを可能にする。

2. 設計思想:堅牢性を担保する3つの柱

プロフェッショナルな設計には、以下の3つのコンポーネントが不可欠だ。

1. Sanitization(無害化): 送信者名などの文字列から、ファイル名に使えない禁止文字を排除する。
2. Recursive Directory Creation(再帰的ディレクトリ作成): 深い階層のフォルダを、一気に、かつ安全に作成する。
3. Collision Avoidance(衝突回避): 同名ファイルが存在する場合、`ファイル名_1.pdf` のように連番を付与して保存する。

3. プロダクション・コードの実装

以下に、これらすべての要件を満たした実装例を示す。このコードは、単なる「コピペ用」ではない。各モジュールが役割を分担した、保守性の高い構造になっている。

事前準備

VBAエディタの [ツール] -> [参照設定] から、「Microsoft Scripting Runtime」 にチェックを入れておいてください。これにより、FSOのインテリセンス(入力補完)が有効になり、実行速度も向上します。

Option Explicit

' ==============================================================================
' Module: AttachmentManager
' Description: 添付ファイルの階層保存を司るメインロジック
' ==============================================================================

Private Const ROOT_PATH As String = "C:\AutomatedStorage\" ' 保存ルートを指定

''' <summary>
''' 受信メールの添付ファイルを、月別・取引先別の階層に保存するメインルーチン
''' </summary>
''' <param name="itm">対象のMailItem</param>
Public Sub ProcessMailAttachments(ByVal itm As Outlook.MailItem)
    Dim fso As New FileSystemObject
    Dim targetFolder As String
    Dim senderName As String
    Dim mailDate As Date
    Dim yearStr As String, monthStr As String
    
    On Error GoTo ErrorHandler

    ' 1. メタデータの抽出
    mailDate = itm.ReceivedTime
    yearStr = Format(mailDate, "yyyy")
    monthStr = Format(mailDate, "mm")
    
    ' 送信者名を取得し、ファイル名として不適切な文字を除去(Sanitization)
    senderName = SanitizeFileName(itm.SenderName)
    
    ' 2. 保存先パスの構築 (Root\Year\Month\SenderName)
    targetFolder = BuildPath(ROOT_PATH, yearStr, monthStr, senderName)
    
    ' 3. フォルダの作成(存在しない場合は階層的に作成)
    CreateFolderRecursive fso, targetFolder
    
    ' 4. 添付ファイルの保存ループ
    Dim att As Outlook.Attachment
    Dim savePath As String
    
    For Each att In itm.Attachments
        ' ファイル名を取得
        Dim fileName As String
        fileName = att.FileName
        
        ' 保存先のフルパスを決定(衝突回避ロジックを含む)
        savePath = GetUniqueFilePath(fso, targetFolder, fileName)
        
        ' 保存実行
        att.SaveAsFile savePath
        Debug.Print "Saved: " & savePath
    Next att

    Exit Sub

ErrorHandler:
    ' 実務ではここをログ出力や管理者への通知に置き換える
    MsgBox "Error in ProcessMailAttachments: " & Err.Description, vbCritical
End Sub

' ==============================================================================
' Module: Utilities (Internal Functions)
' ==============================================================================

''' <summary>
''' ファイル名として使用できない禁止文字をアンダースコアに置換する
''' </summary>
Private Function SanitizeFileName(ByVal text As String) As String
    Dim invalidChars As Variant
    Dim i As Integer
    ' Windowsの禁止文字リスト
    invalidChars = Array("\", "/", ":", "*", "?", """", "<", ">", "|")
    
    For i = LBound(invalidChars) To UBound(invalidChars)
        text = Replace(text, invalidChars(i), "_")
    Next i
    SanitizeFileName = text
End Function

''' <summary>
''' 指定されたパスを結合する(FSOのPathSeparatorを使用)
''' </summary>
Private Function BuildPath(ParamArray parts() As Variant) As String
    Dim fso As New FileSystemObject
    Dim fullPath As String
    Dim i As Integer
    
    fullPath = CStr(parts(0))
    For i = 1 To UBound(parts)
        If parts(i) <> "" Then
            fullPath = fso.BuildPath(fullPath, CStr(parts(i)))
        End If
    Next i
    BuildPath = fullPath
End Function

''' <summary>
''' フォルダが存在しない場合、親階層から順に作成する(再帰的作成)
''' </summary>
Private Sub CreateFolderRecursive(ByRef fso As FileSystemObject, ByVal targetPath As String)
    If fso.FolderExists(targetPath) Then Exit Sub
    
    Dim parentPath As String
    parentPath = fso.GetParentFolderName(targetPath)
    
    ' 親フォルダがなければ、自分自身を再帰的に呼び出す
    If Not fso.FolderExists(parentPath) Then
        CreateFolderRecursive fso, parentPath
    End If
    
    ' 親が確実に存在する状態で、自分を作成
    fso.CreateFolder targetPath
End Sub

''' <summary>
''' 同名ファイルが存在する場合、末尾に連番を付与して新しいパスを返す
''' </summary>
Private Function GetUniqueFilePath(ByRef fso As FileSystemObject, ByVal folderPath As String, ByVal fileName As String) As String
    Dim fullPath As String
    Dim baseName As String
    Dim extension As String
    Dim counter As Integer
    
    fullPath = fso.BuildPath(folderPath, fileName)
    
    ' すでにファイルが存在する場合、連番を振る
    If fso.FileExists(fullPath) Then
        baseName = fso.GetBaseName(fileName)
        extension = fso.GetExtensionName(fileName)
        counter = 1
        
        Do
            ' 例: filename_1.pdf
            fullPath = fso.BuildPath(folderPath, baseName & "_" & counter & "." & extension)
            counter = counter + 1
        Loop While fso.FileExists(fullPath)
    End If
    
    GetUniqueFilePath = fullPath
End Function

4. アーキテクトの視点:なぜこのコードは「強い」のか

このコードを読み解き、理解してほしい。

① 再帰的なフォルダ生成 (`CreateFolderRecursive`)

多くの開発者は `MkDir “C:\A\B\C”` と書き、`A` や `B` がないとエラーになることに気づきません。私の実装した再帰関数は、ターゲットのフォルダが存在するか確認し、なければ親を辿り、最下層まで「掘り進める」ように動作します。これにより、ディレクトリ構造の深さを一切気にせずに運用できます。

② ファイル名の「無害化」 (`SanitizeFileName`)

Outlookの送信者名は「株式会社A / 営業部」のように、スラッシュを含むことがあります。そのままフォルダ名にしようとすれば、OSは「サブディレクトリ」と誤認し、エラーを吐きます。これを事前に `_` に置換するプロセスこそが、自動化における「防波堤」となります。

③ 衝突回避のロジック (`GetUniqueFilePath`)

`SaveAsFile` は、同名ファイルがあると警告なしに上書きするリスクがあります。業務データにおいて、上書きは「死」を意味します。`Do While` ループを用いた連番付与ロジックを組み込むことで、過去のデータを守りつつ、新しいデータを安全に格納できます。

5. 最後に:保守性のために

このコードを実務に投入する際は、以下の点に注意してください。

1. ROOT_PATH の管理: 定数として定義していますが、将来的にネットワークドライブやクラウドストレージ(OneDrive同期フォルダ等)へ変更する可能性があるため、外部設定ファイル(INIファイルやDB)から読み込む設計に昇格させることも検討してください。
2. エラーハンドリングの拡張: 現在は `MsgBox` ですが、本番環境では「エラーログをテキストファイルに書き出す」処理に置き換えるのが定石です。

自動化エンジニアの価値は、コードの短さではなく、「運用開始から1年後、誰が触っても、どんなデータが入ってきても、システムが止まらないこと」にあります。

この知見を、あなたのプロジェクトに実装してください。

タイトルとURLをコピーしました