【テクニカル・上級編】Word VBAで『検索結果』をRange配列に格納する:Find.Executeの効率的なループ処理 – Word VBA解析バイブル

スポンサーリンク

伝統的なVBA開発において、多くの凡庸なプログラマが陥る罠がある。それは、`Selection`オブジェクトへの依存と、場当たり的な`Find.Execute`のループ処理だ。

大規模な法的文書や数千ページの技術仕様書を相手にする時、画面の更新を止めるだけでは不十分だ。真のシニアエンジニアは、「検索(探索)」と「処理(実行)」を完全に分離する。

今回は、Word VBAの心臓部である`Range`オブジェクトを、あたかもメモリ上のポインタ配列のように扱い、検索結果を高速に一括取得・処理する「プロフェッショナル・パターン」を詳解する。

1. なぜ「見つけながら処理する」のが悪手なのか

標準的なリファレンスに載っているような、`Do While .Execute` の中で直接フォントを変えたり文字列を置換したりするコードには、2つの致命的な欠陥がある。

1. 無限ループのリスク: 置換後の文字列が検索条件に合致する場合、カーソル位置の制御を誤ると容易に無限ループに陥る。
2. DOM再計算のオーバーヘッド: `Find.Execute`が成功するたびにWordのレイアウトエンジンは再計算を試みる。処理中にドキュメントの構造(Range)が変わることは、エンジンにとって最大の負荷だ。

これを回避するためには、まず検索結果の「座標(Range)」のみを抽出し、静的なコレクションに封じ込める。処理はその「スナップショット」に対して行うのが、大規模システムにおける鉄則である。

2. 検索結果をRange配列(Collection)に格納する極限のロジック

以下に示すコードは、指定した検索条件に合致するすべての箇所を`Collection`に格納し、一括処理するための基盤だ。

実装:FindAllToCollection

Option Explicit

”’

”’ 指定したRange内から特定のキーワードを検索し、合致するすべてのRangeをCollectionとして返す
”’

”’ 検索対象のRange(Document全体ならActiveDocument.Content) ”’ 検索文字列 ”’ Rangeオブジェクトが格納されたCollection
Public Function FindAllMatches(ByVal searchTarget As Range, ByVal findText As String) As Collection
Dim results As New Collection
Dim findRange As Range
Dim firstAddress As Long

‘ 検索専用のRangeオブジェクトを複製(元のRangeを汚染しない)
Set findRange = searchTarget.Duplicate

With findRange.Find
.ClearFormatting
.Replacement.ClearFormatting
.Text = findText
.Forward = True
.Wrap = wdFindStop ‘ 非常に重要:ループ防止のため末尾で止める
.Format = False
.MatchCase = False
.MatchWholeWord = False
.MatchByte = False
.MatchAllWordForms = False
.MatchSoundsLike = False
.MatchWildcards = False
End With

‘ 初回の実行
Do While findRange.Find.Execute
‘ 検索にヒットした箇所のRangeを複製して保持
‘ そのまま格納するとfindRangeの変化に追従してしまうため、Duplicateが必須
results.Add findRange.Duplicate

‘ 検索範囲をヒットした箇所の直後から、元のターゲットの末尾までにリセット
‘ これにより、同一箇所を二度踏むリスクをゼロにする
findRange.Collapse wdCollapseEnd
findRange.End = searchTarget.End

‘ 終端に達したか、検索範囲が消失した場合は脱出
If findRange.Start >= searchTarget.End Then Exit Do
Loop

Set FindAllMatches = results
End Function

3. チーフアーキテクトの視点:なぜ `Duplicate` なのか?

上記のコードで最も重要なのは `findRange.Duplicate` だ。

VBAの`Range`オブジェクトは、ドキュメント上の特定の範囲を指し示す「生きた参照」である。`Find.Execute`が成功するたびに、`findRange`オブジェクト自身の開始点と終了点が書き換わる。もし `results.Add findRange` と記述してしまえば、コレクションの中身はすべて「最後に検索された場所」を指す同じポインタで埋め尽くされることになる。

`Duplicate` メソッドは、メモリ上にその瞬間の範囲情報を保持した新しいRangeインスタンスを生成する。これにより、検索が進行しても過去に取得した座標が破壊されない。

4. 取得したRange配列の一括処理

検索結果が確保できれば、あとはそれを回すだけだ。このフェーズでは検索エンジンは動いていないため、非常に高速かつ安全に処理できる。

Public Sub MassUpdateExample()
Dim matches As Collection
Dim targetRange As Range
Dim i As Long

‘ 画面更新停止とバックグラウンド処理の最適化
Application.ScreenUpdating = False

‘ 検索実行
Set matches = FindAllMatches(ActiveDocument.Content, “重要機密”)

‘ 逆順で処理するのが定石(文字列長が変わる編集を行う場合、後ろから処理すれば前方の座標がズレない)
For i = matches.Count To 1 Step -1
Set targetRange = matches(i)

‘ ここで高度な処理を行う
With targetRange
.HighlightColorIndex = wdYellow
.Font.Bold = True
‘ API連携や外部DBへの書き出しなどをここで行う
End With

‘ メモリ解放の明示
Set targetRange = Nothing
Next i

Application.ScreenUpdating = True
MsgBox matches.Count & ” 箇所の処理を完了しました。”, vbInformation
End Sub

5. メモリ最適化とレガシー環境への配慮

Windows APIを直接叩くレベルのエンジニアであれば、大規模文書処理における「メモリリーク」を警戒すべきだ。

1. オブジェクトの解放: `Set obj = Nothing` は、ループ内では気休めに過ぎないという説もあるが、WordのCOMラッパーにおいては、参照カウントを明示的に減らすことは安定動作への唯一の道だ。
2. Undoバッファの肥大化: 大量のRange操作を行うと、Wordの「元に戻す(Undo)」バッファがメモリを食いつぶす。数千件の処理を行う場合は、定期的にドキュメントを保存するか、一時的に保存してバッファをクリアする設計を検討せよ。
3. DoEventsの挿入: 10,000件を超えるような処理では、Windowsのメッセージキューが滞留し「応答なし」判定を受ける。100件ごとに `DoEvents` を呼び出し、OSに制御を戻すのが紳士的なアーキテクチャだ。

結論

Word VBAの本質は、テキスト操作ではなく「Rangeという名のポインタ操作」にある。

`Find.Execute`の結果を直ちに処理せず、一度`Collection`や配列に退避させる。この単純な「疎結合」の導入こそが、スパゲッティコードを堅牢なシステムへと昇華させる境界線だ。

我々シニアエンジニアの仕事は、コードを書くことではない。オブジェクトのライフサイクルを支配し、最も効率的なデータフローを設計することにある。 このパターンを君のツールキットに加えておけば、レガシーなWord自動化案件も、一転して至高の芸術へと変わるだろう。

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