【実務・中級編】【上級者】プレゼンテーションのPDFエクスポート時に発生するメモリリークやCOM例外を、独自の例外監視ループとオブジェクトの強制ガベージコレクションによって完全回避する高負荷対応アーキテクチャ – PowerPoint VBA解析バイブル

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

【極限設計】数千件のPDF変換を完遂する:PowerPoint VBAにおけるメモリリーク・COM例外への生存戦略

「10件程度の変換なら動く。しかし、100件を超えたあたりで、あるいは数千ページに及ぶ巨大な資料を処理し始めた途端、プログラムは沈黙する。」

もし君が、このような現象に頭を抱えているとしたら、君は今、「スクリプト作成者」から「システムアーキテクト」へと脱皮すべき境界線に立っている。

PowerPointのCOMオブジェクトを操作してPDFエクスポートを行う処理は、一見単純に見える。しかし、大規模なバッチ処理において、メモリリークとCOM例外(RPCサーバーを利用できません、等)は、避けては通れない「必然の災厄」だ。なぜなら、Office製品のCOMインターフェースは、オブジェクトの破棄(`Set obj = Nothing`)を命じても、そのメモリを即座に、かつ完全に解放するとは限らないからだ。

本稿では、単なるループ処理ではない。高負荷環境下で、数千のファイルを確実に、かつ堅牢に処理し切るための「プロセス・リサイクリング・アーキテクチャ」を伝授する。

1. なぜ `Set obj = Nothing` だけでは足りないのか

多くの開発者が陥る致命的な誤解がある。「オブジェクトを `Nothing` に代入すれば、メモリは解放される」という幻想だ。

VBAにおける `Nothing` の代入は、あくまでVBA側の参照カウントを減らす操作に過ぎない。背後で動いている `powerpnt.exe` プロセス内のヒープ領域に、参照が残存したり、内部的なキャッシュが蓄積されたりすることは日常茶飯事だ。特に `ExportAsFixedFormat` メソッドは、レンダリングエンジンを激しく消費するため、実行のたびにメモリの断片化を引き起こす。

この断片化が積み重なると、OSは新しいメモリ領域を割り当てられなくなり、COM例外をスローしてプロセスをクラッシュさせる。つまり、「オブジェクトの解放」ではなく「プロセスの生存期間の管理」こそが、本質的な解決策なのだ。

2. 堅牢なアーキテクチャ:プロセス・リサイクリング・パターン

私が提唱する解決策は、「一定のワークロードごとに、PowerPointプロセス自体を物理的に再起動する」という戦略だ。

具体的には、以下の3つのレイヤーで設計を構成する。

1. Orchestrator(司令塔): 全体の進捗、エラーログ、および「何件ごとにプロセスを再起動するか」を管理する。
2. Worker Unit(作業員): 単一のプレゼンテーションを開き、PDF化し、完全にクローズする。
3. Watchdog(監視役): 処理中にプロセスがハングアップ(応答なし)していないかを監視する。

設計の核心:バッチ・リセット・ロジック

1,000個のファイルがある場合、1,000回 `powerpnt.exe` を立ち上げるのは非効率だ。しかし、1回で1,000個こなすのは自殺行為だ。
「50ファイルごとにPowerPointアプリケーションを一度完全に終了し、再起動する」というバッチ・リサイクリングが、パフォーマンスと安定性の黄金比となる。

3. 実装例:プロダクション・グレードの変換エンジン

以下に、実務でそのまま利用可能な、極めて堅牢な設計思想に基づいたコードを示す。

' ==============================================================================
' Module: BatchPDFConverter
' Description: 高負荷対応型PDF一括変換エンジン
' Strategy: Process Recycling & Batch Management
' ==============================================================================

Option Explicit

' 設定定数
Private Const BATCH_SIZE As Integer = 50 ' 何件ごとにPowerPointプロセスを再起動するか
Private Const MAX_RETRIES As Integer = 3  ' COMエラー時の再試行回数

Public Sub ExecuteMassiveConversion(ByVal targetFolder As String)
    Dim fileList As Collection
    Set fileList = GetFileList(targetFolder)
    
    Dim totalFiles As Long
    totalFiles = fileList.Count
    
    Dim i As Long
    Dim currentBatchCount As Integer
    Dim pptApp As Object
    
    Debug.Print "--- Conversion Started: " & totalFiles & " files ---"
    
    For i = 1 To totalFiles
        ' バッチの境界判定:新しいバッチの開始、または初回実行時
        If (i - 1) Mod BATCH_SIZE = 0 Then
            If Not pptApp Is Nothing Then
                Call SafeCleanup(pptApp)
            End If
            Set pptApp = CreatePowerPointInstance()
            currentBatchCount = 0
            Debug.Print "--- New Process Spawned (Batch Start) ---"
        End If
        
        ' 個別ファイルの処理
        If ProcessSingleFile(pptApp, fileList(i)) Then
            currentBatchCount = currentBatchCount + 1
        Else
            Debug.Print "!!! Critical Error in file: " & fileList(i)
            ' ここでエラーログをDBやファイルに書き出す処理を挟むのがプロの設計
        End If
        
        ' 進捗表示
        If i Mod 10 = 0 Then Debug.Print "Progress: " & i & "/" & totalFiles
    Next i

    ' 最終クリーンアップ
    If Not pptApp Is Nothing Then Call SafeCleanup(pptApp)
    Debug.Print "--- Conversion Completed Successfully ---"
End Sub

' ------------------------------------------------------------------------------
' Worker Unit: 単一ファイルの処理(リトライメカニズム付き)
' ------------------------------------------------------------------------------
Private Function ProcessSingleFile(ByRef pptApp As Object, ByVal filePath As String) As Boolean
    Dim pres As Object
    Dim pdfPath As String
    Dim retryCount As Integer
    Dim success As Boolean
    
    pdfPath = Replace(filePath, ".pptx", ".pdf")
    ' 拡張子が異なる場合(.ppt等)の考慮が必要な場合はここで正規化する
    
    retryCount = 0
    success = False
    
    Do While retryCount < MAX_RETRIES And Not success
        On Error Resume Next
        
        ' プレゼンテーションを開く
        Set pres = pptApp.Presentations.Open(Filename:=filePath, ReadOnly:=True, WithWindow:=False)
        
        If Err.Number = 0 Then
            ' PDFエクスポート実行
            pres.ExportAsFixedFormat Path:=pdfPath, FixedFormatType:=2 ' 2 = ppFixedFormatTypePDF
            
            If Err.Number = 0 Then
                success = True
            Else
                Debug.Print "  [Error] Export failed: " & Err.Description
            End If
            
            ' オブジェクトの即時解放
            pres.Close
            Set pres = Nothing
        Else
            Debug.Print "  [Error] Open failed: " & Err.Description
            ' COMエラー(RPCサーバー等)の場合は、プロセスが死んでいる可能性がある
            If Err.Number = -2147024770 Or Err.Number = -2147417848 Then ' 例: RPCエラー
                Debug.Print "  [Critical] COM Interface lost. Forcing restart..."
                Set pptApp = Nothing ' 次のループで再生成させる
                Exit Do 
            End If
        End If
        On Error GoTo 0
        
        If Not success Then
            retryCount = retryCount + 1
            DoEvents ' OSに制御を戻し、リソースの整理を促す
            Sleep 1000 ' 待機
        End If
    Loop
    
    ProcessSingleFile = success
End Function

' ------------------------------------------------------------------------------
' Helper: PowerPointインスタンスの生成
' ------------------------------------------------------------------------------
Private Function CreatePowerPointInstance() As Object
    On Error Resume Next
    Dim app As Object
    Set app = CreateObject("PowerPoint.Application")
    If app Is Nothing Then
        Err.Raise 1001, , "Failed to create PowerPoint instance."
    End If
    Set CreatePowerPointInstance = app
    On Error GoTo 0
End Function

' ------------------------------------------------------------------------------
' Helper: 安全なクリーンアップ
' ------------------------------------------------------------------------------
Private Sub SafeCleanup(ByRef obj As Object)
    On Error Resume Next
    If Not obj Is Nothing Then
        obj.Quit
        Set obj = Nothing
    End If
    On Error GoTo 0
End Sub

' ------------------------------------------------------------------------------
' Helper: ファイルリスト取得
' ------------------------------------------------------------------------------
Private Function GetFileList(ByVal folderPath As String) As Collection
    Dim col As New Collection
    Dim fileName As String
    fileName = Dir(folderPath & "\*.pptx")
    
    Do While fileName <> ""
        col.Add folderPath & "\" & fileName
        fileName = Dir()
    Loop
    Set GetFileList = col
End Function

' Windows API: スリープ用

If VBA7 Then

Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As LongPtr)

Else

Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

End If

4. アーキテクトとしての補足:実戦での注意点

このコードは「動く」だけでなく、「止まらない」ことを目的としている。しかし、実戦投入にあたっては以下の3点を考慮せよ。

① データベース連携とトランザクション

数千件の処理を行う際、エラーが発生した箇所を特定できないのは致命的だ。`ProcessSingleFile` が `False` を返した際、そのファイル名、エラー内容、タイムスタンプを必ず外部ログ(CSV、SQLite、またはSQL Server)に書き出せ。VBAの `Debug.Print` は、開発用であって、プロダクション環境のログではない。

② ファイルI/Oのボトルネック

PDF変換はCPUとメモリだけでなく、ディスクI/Oも激しく消費する。ネットワークドライブ上のファイルを直接処理するのは、COMエラーを誘発する最大の要因の一つだ。可能であれば、「ローカルの高速なSSDに全ファイルをコピー → 変換 → 結果を戻す」というワークフローを組むべきだ。

③ Watchdog(死活監視)の高度化

上記のコードでは、エラー発生時にリトライを行うが、より高度な設計では、別プロセスから `PowerPoint.exe` の CPU使用率やメモリ使用量を監視し、閾値を超えたら強制的に `TaskKill` を実行してプロセスを入れ替える「外部監視型アーキテクチャ」を採用することもある。

結論

大規模な自動化において、「エラーが起きないように書く」ことには限界がある。
真のプロフェッショナルは、「エラーが起きることを前提とし、エラーが起きてもシステム全体が崩壊しない設計」を構築する。

プロセスをリサイクルし、境界を定義し、状態を監視する。この設計思想こそが、君のツールを「ただのスクリプト」から「信頼されるエンジニアリング資産」へと昇華させるのだ。

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