【実務・中級編】【中級者向け】添付ファイル名に特定のキーワードが含まれる場合のみ保存する正規表現活用術 – Outlook VBA解析バイブル

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

InStrの限界を超えろ:RegExpで実装する堅牢な添付ファイル抽出アーキテクチャ

業務自動化の現場において、「特定のメールから添付ファイルを保存する」というタスクは、一見すると初歩的な課題に見える。多くの開発者が、`InStr` 関数や `Like` 演算子を用いた単純な文字列比較でこの課題を解決しようとする。

しかし、断言しよう。その設計は、実務の荒波の中ではすぐに崩壊する。

クライアントから送られてくるファイル名は、常に整然としているわけではない。「請求書_20231001.pdf」もあれば、「【重要】見積書(案).xlsx」もあり、さらには「INV_12345.pdf」といった、パターン化された識別子を含むものもある。これらを `InStr` で一つずつ条件分岐させていくコードは、保守性を著しく低下させ、バグの温床となる。

本記事では、`VBScript.RegExp` を活用し、プロフェッショナルが現場に投入するレベルの「堅牢で、拡張性が高く、メンテナンスしやすい」添付ファイル抽出ロジックの設計思想と実装を伝授する。

1. なぜ `InStr` では足りないのか?

プロフェッショナルな設計において、もっとも避けるべきは「ロジックのスパゲッティ化」である。

単純な文字列比較によるフィルタリングには、以下の致命的な欠陥がある。

1. パターンの複雑化への弱さ: 「請求書」と「見積書」の両方に対応しようとすると、`If…ElseIf…` が増殖し、条件の網羅性が失われる。
2. 表記ゆれへの対応不能: 「請求書」と「請求書 」(末尾スペース)、「請求書(NEW)」といったバリエーションを正確に、かつスマートに制御できない。
3. 境界条件の制御不能: 例えば「請求書」という文字が含まれていれば良いが、実は「請求書作成マニュアル.pdf」といった、抽出対象外のファイルまで拾ってしまうリスクがある。

これらを解決するのが、正規表現(Regular Expression)だ。正規表現を用いることで、「特定のキーワードを含み、かつ特定の拡張子を持つ」といった複雑な条件を、たった一行のパターン定義に集約できる。

2. アーキテクチャ設計:関心の分離

堅牢なツールを作るためには、コードを「メール処理」「正規表現判定」「ファイル保存」の3つのレイヤーに分離すべきである。

  • Email Layer: Outlookオブジェクトモデルを操作し、アイテムを走査する。
  • Logic Layer: `VBScript.RegExp` を用いて、ファイル名が要件を満たすかを判定する。
  • IO Layer: ファイルシステムへの書き込み、重複回避、エラーハンドリングを行う。

この分離が行われていないコードは、後で「保存先をクラウドストレージに変えたい」「判定条件をDBから読み込みたい」といった変更要求が出た際に、すべてを書き直す羽目になる。

3. プロダクション・グレードの実装例

以下に、そのまま実務に投入できるレベルのコードを示す。ここでは、参照設定の手間を省き、環境依存を最小限にするために Late Binding(遅延結合) を採用している。

Option Explicit

''' <summary>
''' 添付ファイルを特定のパターンに基づいて抽出・保存するメインルーチン
''' </summary>
Public Sub ProcessAttachmentsWithRegex()
    Dim outlookApp As Outlook.Application
    Dim ns As Outlook.NameSpace
    Dim inbox As Outlook.Folder
    Dim mailItem As Object ' MailItem
    Dim attachment As Outlook.Attachment
    
    ' 設定値(本来は外部設定ファイルやDBから取得すべき)
    Const TARGET_SAVE_PATH As String = "C:\Automation\ExtractedFiles\"
    Const REGEX_PATTERN As String = "(請求書|見積書|Invoice|Quote).*\.(pdf|xlsx|csv)$"
    
    ' 保存先ディレクトリの存在確認
    If Dir(TARGET_SAVE_PATH, vbDirectory) = "" Then
        MsgBox "保存先ディレクトリが存在しません: " & TARGET_SAVE_PATH, vbCritical
        Exit Sub
    End If

    Set outlookApp = New Outlook.Application
    Set ns = outlookApp.GetNamespace("MAPI")
    Set inbox = ns.GetDefaultFolder(olFolderInbox)

    ' 受信トレイのアイテムをループ(パフォーマンスのため、必要に応じてフィルタリングを検討)
    For Each mailItem In inbox.Items
        ' MailItem型であることを確認(会議出席依頼などを除外)
        If TypeOf mailItem Is Outlook.MailItem Then
            
            ' 添付ファイルの走査
            For Each attachment In mailItem.Attachments
                ' 正規表現による判定ロジックの呼び出し
                If IsTargetAttachment(attachment.FileName, REGEX_PATTERN) Then
                    ' 保存処理の実行
                    SaveAttachmentSecurely attachment, TARGET_SAVE_PATH
                End If
            Next attachment
            
        End If
    Next mailItem

    MsgBox "処理が完了しました。", vbInformation
End Sub

''' <summary>
''' 正規表現を用いてファイル名がパターンに合致するか判定する
''' </summary>
''' <param name="fileName">判定対象のファイル名</param>
''' <param name="pattern">正規表現パターン</param>
''' <returns>合致すればTrue</returns>
Private Function IsTargetAttachment(ByVal fileName As String, ByVal pattern As String) As Boolean
    Dim regExp As Object ' VBScript.RegExp
    
    ' Late Bindingによるインスタンス生成
    Set regExp = CreateObject("VBScript.RegExp")
    
    With regExp
        .Pattern = pattern
        .IgnoreCase = True    ' 大文字小文字を区別しない
        .Global = False       ' 最初に見つかった時点で終了
    End With
    
    IsTargetAttachment = regExp.Test(fileName)
    Set regExp = Nothing
End Function

''' <summary>
''' ファイル名の重複を回避しながら安全に添付ファイルを保存する
''' </summary>
''' <param name="attachment">保存対象の添付ファイルオブジェクト</param>
''' <param name="destinationFolder">保存先フォルダパス</param>
Private Sub SaveAttachmentSecurely(ByRef attachment As Outlook.Attachment, ByVal destinationFolder As String)
    Dim baseName As String
    Dim extension As String
    Dim finalPath As String
    Dim counter As Integer
    Dim fso As Object
    
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    ' ファイル名と拡張子の分離
    baseName = fso.GetBaseName(attachment.FileName)
    extension = fso.GetExtensionName(attachment.FileName)
    
    ' 初回の保存試行パス
    finalPath = destinationFolder & attachment.FileName
    
    ' 【重要】ファイル重複回避ロジック
    ' 同名のファイルが存在する場合、"filename_1.pdf" のように連番を付与する
    counter = 1
    Do While fso.FileExists(finalPath)
        finalPath = destinationFolder & baseName & "_" & counter & "." & extension
        counter = counter + 1
    Loop
    
    On Error GoTo ErrorHandler
    attachment.SaveAsFile finalPath
    Debug.Print "Saved: " & finalPath
    Exit Sub

ErrorHandler:
    Debug.Print "Error saving attachment: " & attachment.FileName & " - " & Err.Description
    ' 実務ではここでログファイルへの書き出しや管理者に通知する処理を検討せよ
End Sub

4. プロフェッショナルとしての「こだわり」解説

このコードが、単なる「動くコード」ではなく「プロダクション・コード」である理由を説明する。

① 正規表現パターンの設計

`”(請求書|見積書|Invoice|Quote).*\.(pdf|xlsx|csv)$”`
このパターンは、単なるキーワードマッチングではない。

  • `(請求書|見積書|Invoice|Quote)`:複数のキーワードを OR 条件で結合。
  • `.*`:キーワードの後に任意の文字が続くことを許容。
  • `\.(pdf|xlsx|csv)$`:末尾が特定の拡張子であることを強制。 これにより、「請求書に関するメモ.txt」といった無関係なファイルを確実に排除できる。

② ファイル名衝突の回避(Idempotencyの確保)

実務において、同じ名前のファイルが送られてくることは日常茶飯事だ。`attachment.SaveAsFile` をそのまま使うと、既存のファイルを上書きしてしまうリスクがある。
`Do While fso.FileExists(finalPath)` による連番付与ロジックは、データの損失を防ぐための最低限の防衛策である。

③ Late Binding の採用

`CreateObject(“VBScript.RegExp”)` を使用することで、実行環境の参照設定(References)に依存しない。これは、配布先のPCによって「参照不可でエラーが出る」という、VBA開発における最も古典的で回避困難なトラブルを未然に防ぐための知恵だ。

5. さらなる高みへ:スケーラビリティの検討

もし君が、このツールを数千件のメールを処理する大規模なワークフローの一部として組み込むなら、以下の点も考慮すべきだ。

  • パフォーマンス: `For Each mailItem In inbox.Items` は、受信トレイが肥大化していると極めて低速になる。`Items.Restrict` メソッドを使用して、あらかじめ日付などでフィルタリングした集合に対してループを回すのが定石である。
  • ログ管理: `Debug.Print` は開発用だ。実務では `FileSystemObject` を用いて、処理日時、ファイル名、成否を記録するログファイルを出力する機能を実装せよ。
  • 例外処理の細分化: ファイルがロックされている場合、ディスク容量が不足している場合など、エラーのパターンに応じたリカバリ策を検討せよ。

結論

「とりあえず動く」コードを書くのは誰にでもできる。しかし、「変化に強く、壊れにくい」コードを書くには、設計思想が必要だ。正規表現を使いこなし、オブジェクトの境界を意識し、例外的な状況を予測してコードに組み込む。

その積み重ねこそが、エンジニアとしての価値を決めるのである。

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