Word VBA差分検出エンジンの極致:`CompareDocuments`と`Range`操作による高速・高精度ドキュメント監査
多くのエンジニアは、Word文書の差分抽出と聞いて「文字列を全件走査して比較するアルゴリズム」をVBAで組もうとする致命的な過ちを犯します。Wordのドキュメントモデル(DOM)は単なるテキストデータではなく、スタイルの継承構造、描画レイヤー、XMLツリー構造が複雑に絡み合った重厚なオブジェクトの塊です。文字単位の泥臭いルックアップ処理は、処理速度の指数関数的な低下($O(N^2)$以上)を招き、大型文書ではメモリ枯渇やレスポンス停止(フリーズ)を引き起こします。
本稿では、Wordに秘められたネイティブ差分エンジンである`Application.CompareDocuments` APIを最大限に引き出し、検出された変更点(`Revisions`コレクション)を`Range`オブジェクトで高速に特定・ハイライト処理する、本番運用耐性を備えた堅牢なアーキテクチャを解説します。
さらに、画面描画を低レイヤーで抑止するWindows APIのコール、COMオブジェクトの正確なライフサイクル管理、GDIリソースの枯渇を防ぐ最適化手法まで、エンタープライズ領域のVBA開発に必要な真髄を提示します。
—
1. 差分検出の内部メカニズム:`CompareDocuments`のアーキテクチャ
Word VBAにおける文書比較の実装アプローチには大きく分けて2つ存在します。
1. `Document.Compare` メソッド(レガシーなアクティブ文書比較)
2. `Application.CompareDocuments` メソッド(推薦:非破壊型ドキュメント比較)
前者は既存の文書インスタンスを直接書き換えるリスクを孕むのに対し、後者の `Application.CompareDocuments` は、新旧2つの文書を入力オブジェクトとして受け取り、差分結果のみを保持する第三の隠蔽ドキュメントをメモリ上に新規生成します。
[Original Doc (File A)] ──┐
├──> [Application.CompareDocuments] ──> [Diff Target Doc (In-Memory)]
[Revised Doc (File B)] ──┘ │
└──> Revisions (Iterate Range)
このメソッドの内部では、WordのコアC++エンジンが超高速なLCS(Longest Common Subsequence: 最長共通部分系列)アルゴリズムを実行します。我々VBAアーキテクトが解くべき課題は、「いかにして生成された`Revisions`コレクション(変更履歴)を安全にトラバースし、UI描画負荷を最小限に抑えながら対象の`Range`に対して視覚的フィードバック(ハイライト・色付け)を適用するか」に集約されます。
—
2. Windows APIによる描画パイプラインの完全遮断
Word標準の `Application.ScreenUpdating = False` は、VBA開発者にとって周知の事実ですが、これだけではWord内部の重厚なレイアウト再計算エンジン(GDI/DirectWrite描画スレッド)を完全に沈黙させることはできません。大規模な差分(数千箇所のRevision)を走査して`Range`オブジェクトの書式を変更する場合、OSレベルでウィンドウの再描画メッセージ(`WM_SETREDRAW`)をブロックするのが最良の手段です。
また、ミリ秒単位での正確なパフォーマンスプロファイリングを行うため、Win32 APIの `QueryPerformanceCounter` を組み込みます。
画面描画制御と高精度タイマーのWin32 API定義
If VBA7 Then
Private Declare PtrSafe Function FindWindowA Lib “user32” ( _
ByVal lpClassName As String, _
ByVal lpWindowName As String) As LongPtr
Private Declare PtrSafe Function SendMessageA Lib “user32” ( _
ByVal hwnd As LongPtr, _
ByVal wMsg As Long, _
ByVal wParam As LongPtr, _
LParam As Any) As LongPtr
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” ( _
lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” ( _
lpFrequency As Currency) As Long
Else
Private Declare Function FindWindowA Lib “user32” ( _
ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
Private Declare Function SendMessageA Lib “user32” ( _
ByVal hwnd As Long, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
LParam As Any) As Long
Private Declare Function QueryPerformanceCounter Lib “kernel32” ( _
lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32” ( _
lpFrequency As Currency) As Long
End If
Private Const WM_SETREDRAW As Long = &HB
—
3. 本番環境準拠の完全VBA実装
以下に示すクラスモジュール/標準モジュール構造は、例外処理、COMオブジェクトの厳格な解放処理、差分種別(挿入・削除・書式変更)に応じたダイレクトな`Range`ハイライト処理を備えた製品品質のコードです。
【標準モジュール】`mod_DocumentDiffEngine.bas`
Option Explicit
‘ ==============================================================================
‘ 業務自動化アーキテクチャ: 高速Word文書差分比較エンジン
‘ 機能: 2つのWord文書を比較し、差分を別文書として出力。削除・挿入箇所をカスタムハイライト化
‘ ==============================================================================
Public Enum DiffColorSchema
Color_Insertion = wdYellow
Color_Deletion = wdBrightGreen
Color_Formatting = wdTurquoise
End Enum
Public Sub ExecuteDocumentComparison( _
ByVal originalPath As String, _
ByVal revisedPath As String, _
ByVal outputPath As String)
Dim appWord As Word.Application
Dim docOriginal As Word.Document
Dim docRevised As Word.Document
Dim docDiff As Word.Document
Dim startTime As Currency
Dim endTime As Currency
Dim frequency As Currency
‘ プロファイラ起動
QueryPerformanceFrequency frequency
QueryPerformanceCounter startTime
‘ 1. アプリケーション環境の最適化
Set appWord = Application
Call FreezeWordUI(appWord, True)
On Error GoTo ErrorHandler
‘ 2. ソースドキュメントの読み込み(読み取り専用・非表示)
Set docOriginal = appWord.Documents.Open( _
FileName:=originalPath, _
ReadOnly:=True, _
Visible:=False, _
AddToRecentFiles:=False)
Set docRevised = appWord.Documents.Open( _
FileName:=revisedPath, _
ReadOnly:=True, _
Visible:=False, _
AddToRecentFiles:=False)
‘ 3. Core Engine: CompareDocuments の呼び出し
‘ wdCompareTargetNew により、メモリ上に新しいドキュメントを生成
Set docDiff = appWord.CompareDocuments( _
OriginalDocument:=docOriginal, _
RevisedDocument:=docRevised, _
Destination:=wdCompareTargetNew, _
Granularity:=wdGranularityWordLevel, _
CompareFormatting:=True, _
CompareCaseChanges:=True, _
CompareWhitespace:=True, _
CompareTables:=True, _
CompareHeaders:=True, _
CompareFootnotes:=True, _
CompareTextmoves:=True)
‘ 原本ファイルは早期解放(メモリ空間のフットプリント削減)
docOriginal.Close SaveChanges:=wdDoNotSaveChanges
Set docOriginal = Nothing
docRevised.Close SaveChanges:=wdDoNotSaveChanges
Set docRevised = Nothing
‘ 4. Revisionsコレクション走査と Rangeオブジェクトの特定・着色
Call HighlightDocumentRevisions(docDiff)
‘ 5. 結果の保存
docDiff.SaveAs2 FileName:=outputPath, FileFormat:=wdFormatXMLDocument
docDiff.Close SaveChanges:=wdDoNotSaveChanges
Set docDiff = Nothing
QueryPerformanceCounter endTime
‘ 後処理
Call FreezeWordUI(appWord, False)
Dim elapsedMs As Double
elapsedMs = (endTime – startTime) / frequency 1000
MsgBox “差分抽出およびハイライト処理が正常に完了しました。” & vbCrLf & _
“処理時間: ” & Format(elapsedMs, “0.00”) & ” ms”, vbInformation, “処理完了”
Exit Sub
ErrorHandler:
‘ 異常系リカバリ処理
Call FreezeWordUI(appWord, False)
‘ 参照カウンタの明示的クリーンアップ
If Not docOriginal Is Nothing Then docOriginal.Close SaveChanges:=wdDoNotSaveChanges
If Not docRevised Is Nothing Then docRevised.Close SaveChanges:=wdDoNotSaveChanges
If Not docDiff Is Nothing Then docDiff.Close SaveChanges:=wdDoNotSaveChanges
MsgBox “致命的エラーが発生しました: ” & Err.Description & ” (Code: ” & Err.Number & “)”, _
vbCritical, “システムエラー”
End Sub
‘ ——————————————————————————
‘ 高速Revision走査・Rangeハイライトルーチン
‘ ——————————————————————————
Private Sub HighlightDocumentRevisions(ByVal targetDoc As Word.Document)
Dim rev As Word.Revision
Dim revRange As Word.Range
Dim revCount As Long
Dim i As Long
revCount = targetDoc.Revisions.Count
If revCount = 0 Then Exit Sub
‘ パフォーマンスの最適化: コレクションを直接 For Each で回すのではなく
‘ インデックスによる逆順走査、または最適化されたポインタ移動を行う。
‘ ※削除箇所の操作時にドキュメント構造が変化するため逆順走査が定石。
For i = revCount To 1 Step -1
Set rev = targetDoc.Revisions(i)
Set revRange = rev.Range
Select Case rev.Type
Case wdRevisionInsert
‘ 挿入された箇所を黄色でハイライト
revRange.HighlightColorIndex = Color_Insertion
‘ 履歴自体を「承諾」して文字列として確定させる(見た目のみハイライトとして残す)
rev.Accept
Case wdRevisionDelete
‘ 削除された箇所の文字列を保持しつつ、視覚的削除マーカーとして再構築
‘ 削除領域はAcceptすると消滅するため、特異な処理を行う
revRange.HighlightColorIndex = Color_Deletion
revRange.Font.StrikeThrough = True ‘ 取り消し線
rev.Accept ‘ 履歴状態を解除し、取り消し線付きテキストとして残す
Case wdRevisionProperty, wdRevisionStyleDefinition
‘ 書式変更点
revRange.HighlightColorIndex = Color_Formatting
rev.Accept
Case Else
‘ その他の変更(段落番号、表構造など)
rev.Accept
End Select
‘ COMオブジェクトの参照を各ループで明示的に解放(メモリリーク対策)
Set revRange = Nothing
Set rev = Nothing
Next i
End Sub
‘ ——————————————————————————
‘ Low-Level UI 制御ヘルパー関数の実装
‘ ——————————————————————————
Private Sub FreezeWordUI(ByVal app As Word.Application, ByVal freeze As Boolean)
Dim hwnd As LongPtr
On Error Resume Next
‘ ClassName “OpusApp” は Microsoft Word のメインウィンドウクラス名
hwnd = FindWindowA(“OpusApp”, vbNullString)
If freeze Then
app.ScreenUpdating = False
app.DisplayAlerts = wdAlertsNone
If hwnd <> 0 Then SendMessageA hwnd, WM_SETREDRAW, 0, ByVal 0&
Else
If hwnd <> 0 Then SendMessageA hwnd, WM_SETREDRAW, 1, ByVal 0&
app.DisplayAlerts = wdAlertsAll
app.ScreenUpdating = True
app.Refresh ‘ 画面の強制描画リフレッシュ
End If
On Error GoTo 0
End Sub
—
4. コードのロジック深掘りと設計のポイント
本アーキテクチャで採用されている高度な技術要件について、深層的な解説を行います。
① `CompareDocuments` のパラメータチューニング
`Granularity:=wdGranularityWordLevel` を指定することで、文字単位(CharacterLevel)の過剰なノイズ差分を排除し、単語・形態素レベルでの意味のある差分抽出を実現しています。また、`CompareFormatting:=True` を有効にすることで、フォントサイズ変更や太字化といった意図しないスタイルの崩れも検知可能にしています。
② `Revisions` の逆順ループ(`Step -1`)と `Accept` の罠
Word VBAにおいて、`Revision` オブジェクトに対して操作(ハイライトの変更や `.Accept` / `.Reject`)を行うと、ドキュメントの内部ポインタとインデックス番号が即座に再計算されます。
正方向(`1 To Count`)でループを回すと、要素の削除や統合が発生した瞬間にインデックスがずれ、「インデックスが範囲外です」というランタイムエラーや、特定の差分のスキップ(処理漏れ)が発生します。末尾(`revCount To 1 Step -1`)から評価を確定させていく手法は、DOM構造変更時の整合性を保つための不可欠な設計パターンです。
③ `wdRevisionDelete` の視覚化トリック
Word標準の仕様では、削除履歴(`wdRevisionDelete`)に対して `.Accept` を実行すると、そのテキストはドキュメント上から物理的に消滅します。
本コードでは、`revRange.HighlightColorIndex = Color_Deletion` および `revRange.Font.StrikeThrough = True` を事前適用してから `.Accept` を呼び出すことで、「履歴データとしては確定(Accept)させつつ、本文中には緑色ハイライトと取り消し線付きテキストとして可視化保存する」という高度な仕様を満たしています。
—
5. エンタープライズ運用におけるメモリ管理と保守性
VBA環境において最も恐れるべきは、長時間のバッチ処理や大量ファイル処理時に発生する「サイレントなメモリリークによるクラッシュ(C0000005 Access Violation)」です。
COM参照の決定論的破棄(Deterministic Finalization)
VBAのガベージコレクションは参照カウンタ方式(Reference Counting)に依存しています。プロシージャ内で生成された `Word.Range` や `Word.Revision` は、明示的に `Set obj = Nothing` を実行しない限り、スコープを抜けるまで内部的なCOMプロキシインスタンスが一時メモリ(HEAPS)に残留し続けます。
数百ページにおよぶドキュメントでは、`Revisions` 内のループで数千回単位の `Set revRange = rev.Range` が実行されます。ループ内部で毎サイクル `Set revRange = Nothing` を明示的に行っているのは、COM参照カウンタを確実に0にし、GDI/メモリリソースの枯渇(Out of Memoryエラー)を防ぐための極めて重要なプラクティスです。
—
結語
Word VBAを単なる「マクロ記録の延長」として扱うか、堅牢な「オフィスドキュメント処理エンジン」として掌握するかは、DOMの内部挙動およびWindows OSとの連携深度に対する理解で決まります。
今回紹介した `CompareDocuments` APIの非破壊的活用、Win32 APIを併用したUIパイプラインの制御、そして `Range` / `Revision` の完全なライフサイクル管理は、大規模な社内文書管理システムや契約書自動審査エンジンのバックエンドとして十分な堅牢性を提供します。レガシーコードの改善や新規システムのアーキテクチャ設計において、ぜひこの真髄をご活用ください。
