極限の文書比較:Word VBAにおける独自差分エンジンの構築とメモリアーキテクチャ
標準機能の「比較(Document.Compare)」に甘んじているうちは、真の自動化エンジニアとは呼べない。
ビジネスの現場、特に法務文書や技術仕様書の厳密な差分抽出において、Word標準の比較機能はあまりにもブラックボックスであり、かつ挙動が重すぎる。また、変更履歴(Track Changes)ベースの出力は、その後のシステム連携や自動処理においてノイズとなることが多い。
我々シニアエンジニアが求めるのは、「制御可能な差分」だ。どの要素を比較し、どのようにハイライトし、いかにしてメモリを汚さずに処理を完結させるか。本稿では、Word Object Modelの深淵に触れながら、Windows APIを併用した極限の文書比較ツールの設計思想を解説する。
—
1. 比較アルゴリズムの選定:Rangeオブジェクトの局所最適化
Word文書を比較する際、最も愚かなアプローチは「文字単位でループを回す」ことだ。Wordの`Characters`コレクションは極めて低速であり、数万文字の文書では即座にフリーズを引き起こす。
我々が採用すべきは、Word単位(Words)あるいは段落単位(Paragraphs)での比較、そして必要に応じたRangeの再定義である。
オブジェクトライフサイクルの管理
VBAにおいて、`Document`や`Range`オブジェクトはCOMラッパーである。これらをループ内で無造作に生成・破棄すると、内部的な参照カウントの不整合やメモリリークの原因となる。特に大規模文書を扱う場合、`Set rng = Nothing`による明示的な解放は「作法」ではなく「義務」である。
—
2. 実装:プロフェッショナル仕様の差分ロジック
以下に示すコードは、2つの文書を単語レベルで比較し、削除された要素と追加された要素を独自の色付けでマーキングするプロトタイプである。
Option Explicit
‘ Windows API: 描画更新を完全に停止させ、パフォーマンスを極限まで引き上げる
Private Declare PtrSafe Function LockWindowUpdate Lib “user32” (ByVal hwndLock As LongPtr) As Long
Private Declare PtrSafe Function GetTickCount Lib “kernel32″ () As Long
”’
”’
Public Sub CompareDocumentsAdvanced()
Dim docBase As Document, docNew As Document
Dim wordsBase As Object, wordsNew As Object
Dim i As Long, j As Long
Dim startTime As Long
‘ エラーハンドリングと最適化の設定
On Error GoTo ErrorHandler
Application.ScreenUpdating = False
Application.Calculation = xlCalculationManual ‘ Excel連携時用
‘ 文書オブジェクトの取得(ここでは開いている2つの文書を想定)
‘ 本来はファイルパスからReadOnlyで開くのが定石
If Documents.Count < 2 Then
MsgBox "比較には2つの文書を開いておく必要があります。", vbCritical
Exit Sub
End If
Set docBase = Documents(1)
Set docNew = Documents(2)
' 描画ロック(WinAPI)
LockWindowUpdate Application.hwnd
startTime = GetTickCount()
' 単語コレクションの取得
' 注: Wordsコレクションは1-based index
Dim countBase As Long: countBase = docBase.Words.Count
Dim countNew As Long: countNew = docNew.Words.Count
' 基本ロジック:簡易的な前方一致比較(LCSアルゴリズムの軽量版)
' 実際のプロダクトでは、ここで動的計画法(DP)を用いたLCSを実装するが、
' VBAの配列制限と速度を考慮し、ここではポインタ移動による比較を行う。
i = 1: j = 1
Do While i <= countBase And j <= countNew
If docBase.Words(i).Text <> docNew.Words(j).Text Then
‘ 差分発見:docNew側にハイライト(追加箇所)
With docNew.Words(j)
.Font.ColorIndex = wdRed
.Range.HighlightColorIndex = wdYellow
End With
‘ ここでポインタ調整ロジックが入る(今回は単純化)
End If
i = i + 1
j = j + 1
‘ 1000単語ごとにDoEventsを挟み、OSのハングアップを防ぐ
If i Mod 1000 = 0 Then DoEvents
Loop
Debug.Print “処理時間: ” & (GetTickCount() – startTime) & “ms”
CleanUp:
LockWindowUpdate 0
Application.ScreenUpdating = True
Set docBase = Nothing
Set docNew = Nothing
Exit Sub
ErrorHandler:
MsgBox “致命的エラー: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
—
3. システムアーキテクトが考慮すべき「三つの壁」
① Rangeオブジェクトの「不連続性」
Wordの`Words`コレクションは、記号や改行コードを一つの「単語」としてカウントする。また、フィールドコードや隠し文字が存在する場合、インデックスがズレるという罠がある。堅牢なツールを作るには、`Range.Text`を取得する前に`Range.Fields.Unlink`を実行するか、テキストのみを一旦配列にダンプして比較する手法が推奨される。
② メモリ消費の最適化(Large Address Aware)
32bit版のWord(いまだに多くのエンタープライズ環境に残っている)では、利用可能なメモリが2GBに制限されている。数千ページの文書を比較する場合、`Document`オブジェクトをそのまま扱うのは危険だ。
解決策:
- 比較ロジックはVBA配列(`Variant`配列)上で行う。
- 差分のアドレス(Start, End)のみを`Collection`や`Dictionary`に記録。
- 最後に一括してフォーマットを適用する。
③ Windows APIによるUIフリーズの回避
`Application.ScreenUpdating = False` だけでは不十分なケースがある。特に大規模なループ処理中、Windows側が「応答なし」と判断し、ホワイトアウトすることがある。`LockWindowUpdate` APIを使用することで、Wordの再描画プロセスを低レベルで抑制し、かつOSからの描画要求を無視させることで処理速度を15〜30%向上させることができる。
—
4. レガシー保守とモダン連携の交差点
この比較ツールを単体で終わらせてはならない。シニアエンジニアであれば、この出力をJSONやXML形式でエクスポートする機能を実装すべきだ。
‘ 差分データを外部出力するための構造体(クラスモジュール化を推奨)
Public Sub ExportDiffLog(ByVal diffText As String, ByVal position As Long)
‘ ログファイルへの追記処理
‘ ADODB.Stream等を用いてUTF-8で書き出すのが現代的な作法
End Sub
Word VBAはもはや独立した島ではない。Python(Pandas)で生成した中間データをWordで整形し、その結果をVBAで比較して、最終的な監査ログを基幹システムへ戻す。このパイプラインの一部として、今回の比較ロジックを組み込むことが求められている。
結論
Word VBAを単なる「マクロ」として軽視する者は、その背後にあるCOMアーキテクチャの強大さを理解していない。`Range`を制する者は文書を制する。そして、Windows APIを適切に配置し、オブジェクトのライフサイクルを厳密に管理することで、標準機能を凌駕する圧倒的なパフォーマンスのツールを構築することが可能だ。
現場の最前線に立つ諸君には、ぜひこの「制御された破壊と創造」を楽しんでいただきたい。
