【実務・中級編】【中級者向け】受信メールの添付ファイルが画像の場合のみ、OCR処理を外部API経由で呼び出しテキスト化する連携 – Outlook VBA解析バイブル

スポンサーリンク

【Outlook VBA×外部API】受信画像の自動OCRテキスト化:実務で破綻しない堅牢な連携アーキテクチャ

業務自動化を進める中で、避けて通れない壁がある。「PDFや画像で送られてくる請求書や納品書を、どうにかして自動でテキスト化したい」という現場からの切実な要求だ。

世の中の入門記事では、「受信イベントで添付ファイルを保存し、適当なAPIを叩く」といったおもちゃのようなコードが溢れている。だが、それをそのまま本番環境に投入すれば、1週間も経たずに「Outlookのフリーズ」「APIのレートリミット超過による無限ループ」「マルチスレッド競合によるファイルロック例外」のトリプルコンボで運用停止に追い込まれるだろう。

今回は、Outlook VBAと外部OCR API(例:Azure AI VisionやGoogle Cloud Visionなど、HTTP POSTを受け付けるエンドポイント)を連携させ、実務の負荷に耐えうる堅牢なイベント駆動型アーキテクチャを構築する極意を伝授する。

—

なぜ安易なVBA実装は破綻するのか?(アーキテクチャの罠)

Outlook VBAでメール監視を行う際、多くのエンジニアが犯す最大の過ちは、`NewMail` イベントや `Items_ItemAdd` イベントの中で、重いHTTP通信やファイルI/Oを同期処理(Blocking)で実行することだ。

1. OutlookのUIスレッドブロック問題

Outlookはシングルスレッドベースで動作する側面が大きい。イベントハンドラ内で重い外部API通信を行うと、APIのレスポンスが返ってくる数秒間、Outlook全体がフリーズする。ユーザーは「メールが固まった」と勘違いし、何度も操作して二重起動を引き起こす。

2. ファイルアクセスの排他制御(Race Condition)

メール受信直後のイベント発火と同時に添付ファイルの保存を試みると、Outlookがまだファイルをロックしている最中で `Permission Denied (エラー 70)` が発生する。

3. メモリリークとオブジェクトのライフサイクル

VBAにおけるOutlookのCOMオブジェクト、特に `NameSpace`, `Folder`, `Items` などの参照を適切に解放しないと、Outlookを終了してもプロセスがメモリ上に残り続け、徐動作が重くなる。

これらをクリアするため、本稿では「非同期的な遅延実行(OnTimeメソッドの活用)」「厳格なMIMEタイプ判定」「堅牢なエラーハンドリング」を組み込んだプロダクションコードを提示する。

—

全体設計とシーケンス

1. 検知: `Application_NewMailEx` イベントで新着メールのEntryIDのみを捕捉(重いItemオブジェクトは直接触らない)。
2. 安全遅延: ファイルの書き込み完了を担保するため、`Application.OnTime` で処理を数秒遅延させる。
3. 判定: 添付ファイルの拡張子が画像(`.jpg`, `.png`, `.jpeg` 等)か厳密にチェック。
4. API連携: `MSXML2.ServerXMLHTTP.6.0` を用いて、画像をBase64エンコードして外部OCR APIへPOST送信。
5. 後処理: 取得したテキストをメールの本文に追記し、処理済みフラグ(カテゴリ)を付与。

—

プロダクションコード実装

以下のコードは、`ThisOutlookSession` モジュールに配置するメインのイベントハンドラと、標準モジュールに配置する実処理ロジックの構成だ。

1. `ThisOutlookSession` モジュール

(※イベントをフックするため、必ずこの特殊モジュールに記述すること)

Option Explicit

‘ 新着メールを高速に検知するための最速イベント
Private Sub Application_NewMailEx(ByVal EntryIDCollection As String)
On Error GoTo ErrorHandler

Dim vID As Variant
Dim ns As Outlook.NameSpace
Dim mailItem As Object

Set ns = Application.GetNamespace(“MAPI”)

‘ NewMailExは複数のEntryIDをカンマ区切りで返すことがあるためカンマで分割
vID = Split(EntryIDCollection, “,”)

Dim i As Long
For i = LBound(vID) To UBound(vID)
Set mailItem = ns.GetItemFromID(vID(i))

‘ プレーンなMailItemか確認し、かつ未処理のフラグがある場合のみ対象とする
If TypeName(mailItem) = “MailItem” Then
If mailItem.Categories <> “OCR_Processed” Then
‘ UIスレッドをブロックしないよう、処理を遅延実行(OnTime)へキューイング
‘ ※引数付きプロシージャを呼び出すため、EntryIDを渡す
Application.OnTime Now + TimeValue(“00:00:03”), “‘ProcessOcrQueue “”” & mailItem.EntryID & “””‘”
End If
End If
Next i

CleanUp:
Set mailItem = Nothing
Set ns = Nothing
Exit Sub

ErrorHandler:
Debug.Print “NewMailEx Error: ” & Err.Description
Resume CleanUp
End Sub

2. 標準モジュール(例: `Module_OcrEngine`)

実際のファイル保存、APIリクエスト、メールの更新処理を行うコアロジック。

Option Explicit

‘ 外部APIのエンドポイントと認証キー(環境に合わせて変更すること)
Const API_ENDPOINT As String = “https://api.example-ocr-service.com/v1/ocr”
Const API_KEY As String = “YOUR_SECURE_API_KEY_HERE”

Public Sub ProcessOcrQueue(ByVal entryID As String)
Dim ns As Outlook.NameSpace
Dim mailItem As Outlook.MailItem
Dim att As Outlook.Attachment
Dim tempFolder As String
Dim targetFilePath As String
Dim extractedText As String

On Error GoTo ErrorHandler

Set ns = Application.GetNamespace(“MAPI”)
Set mailItem = ns.GetItemFromID(entryID)

‘ すでに処理済みならスキップ
If mailItem.Categories = “OCR_Processed” Then Exit Sub

‘ 添付ファイルが存在しない場合は終了
If mailItem.Attachments.Count = 0 Then Exit Sub

tempFolder = Environ(“TEMP”) & “\”

Dim j As Long
For j = 1 To mailItem.Attachments.Count
Set att = mailItem.Attachments(j)

‘ 画像ファイル(拡張子)の判定
If IsImageFile(att.FileName) Then
targetFilePath = tempFolder & att.FileName

‘ ファイルの排他制御を考慮し、すでに存在する場合は削除
If Dir(targetFilePath) <> “” Then Kill targetFilePath

‘ 添付ファイルの保存
att.SaveAsFile targetFilePath

‘ 外部OCR APIを呼び出してテキストを取得
extractedText = CallOcrApi(targetFilePath)

If extractedText <> “” Then
‘ メールの本文にOCR結果を追記
mailItem.Body = mailItem.Body & vbCrLf & “— [OCR Extracted Text (” & att.FileName & “)] —” & vbCrLf & extractedText & vbCrLf
End If

‘ 一時ファイルの削除
If Dir(targetFilePath) <> “” Then Kill targetFilePath
End If
Next j

‘ 既読化と処理済みカテゴリの付与
mailItem.UnRead = False
mailItem.Categories = “OCR_Processed”
mailItem.Save

CleanUp:
Set att = Nothing
Set mailItem = Nothing
Set ns = Nothing
Exit Sub

ErrorHandler:
Debug.Print “ProcessOcrQueue Error [” & Err.Number & “]: ” & Err.Description
‘ エラーログをメールのフラグやメモに残すなどのフォールバックをここに実装
Resume CleanUp
End Sub

‘ 拡張子による画像判定ロジック
Private Function IsImageFile(ByVal fileName As String) As Boolean
Dim ext As String
ext = LCase(Mid(fileName, InStrRev(fileName, “.”) + 1))
Select Case ext
Case “jpg”, “jpeg”, “png”, “bmp”, “tiff”
IsImageFile = True
Case Else
IsImageFile = False
End Select
End Function

‘ 外部API(HTTP POST)通信の核心部分
Private Function CallOcrApi(ByVal filePath As String) As String
Dim http As Object
Dim base64Data As String
Dim jsonPayload As String
Dim responseText As String

On Error GoTo ApiError

‘ 画像ファイルをBase64文字列に変換
base64Data = FileToBase64(filePath)
If base64Data = “” Then Exit Function

‘ JSONペイロードの構築(API仕様に合わせて調整)
jsonPayload = “{“”image””: “”” & base64Data & “””}”

‘ ServerXMLHTTP.6.0 のインスタンス化(優れたタイムアウト制御とスレッド安全性)
Set http = CreateObject(“MSXML2.ServerXMLHTTP.6.0”)

‘ タイムアウト設定 (接続:10秒, 送受信:30秒)
http.setTimeouts 10000, 10000, 30000, 30000

http.Open “POST”, API_ENDPOINT, False
http.setRequestHeader “Content-Type”, “application/json”
http.setRequestHeader “Authorization”, “Bearer ” & API_KEY

http.send jsonPayload

If http.Status = 200 Then
responseText = http.responseText
‘ ※本来はここでJSONパーサー(VBA-JSON等)を使うべきだが、
‘ 簡易的にレスポンスをそのまま返す(実務ではJSONパースを推奨)
CallOcrApi = responseText
Else
CallOcrApi = “API Error: Status ” & http.Status & ” – ” & http.statusText
End If

ApiCleanUp:
Set http = Nothing
Exit Function

ApiError:
CallOcrApi = “API Connection Error: ” & Err.Description
Resume ApiCleanUp
End Function

‘ バイナリファイルをBase64へエンコードするヘルパー(ADODB.Stream使用)
Private Function FileToBase64(ByVal filePath As String) As String
Dim stream As Object
Dim dom As Object
Dim node As Object

On Error GoTo StreamError

Set stream = CreateObject(“ADODB.Stream”)
stream.Type = 1 ‘ adTypeBinary
stream.Open
stream.LoadFromFile filePath

Set dom = CreateObject(“MSXML2.DOMDocument”)
Set node = dom.createElement(“tmp”)
node.dataType = “bin.base64”
node.nodeTypedValue = stream.Read

FileToBase64 = Replace(node.text, vbLf, “”)

StreamCleanUp:
If Not stream Is Nothing Then stream.Close
Set stream = Nothing
Set node = Nothing
Set dom = Nothing
Exit Function

StreamError:
FileToBase64 = “”
Resume StreamCleanUp
End Function

—

現場のエンジニアへ送る運用上の鉄則

1. 認証情報のハードコーディング厳禁
プロダクションコードでは、APIキーをVBAのコード内に直書きしてはならない。WindowsレジストリやWindows資格情報マネージャー、あるいは安全な外部設定ファイルから動的に読み込む設計に昇華させること。
2. JSONパーサーの導入
サンプルコードでは `http.responseText` をそのまま返しているが、実務では `VBA-JSON`(Tim Hall氏のライブラリ等)を導入し、レスポンスのJSONから正確にテキストプロパティを抽出する構造にすべきだ。
3. 例外時のリトライとデッドレター処理
外部APIが一時的なメンテナンスやネットワークエラーで落ちた場合、メールが未処理のまま放置される。`Categories = “OCR_Error”` のようなエラー用カテゴリを付与し、管理者にアラートメールを飛ばす例外ハンドリングの作り込みこそが、プロのエンジニアの仕事である。

VBAはレガシーな言語と揶揄されがちだが、Outlookのイベントモデルと最新のWeb APIを正しくブリッジさせれば、強固でメンテナンスフリーなローコード・インテグレーション基盤に変貌する。あなたの現場の自動化プロジェクトを、次のステージへと引き上げてほしい。

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