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

スポンサーリンク

Word VBAの「検索結果」をRange配列に格納!Find.Executeの効率的なループ処理で、マクロの速度を劇的に変える方法

皆さん、こんにちは!Word VBAの世界へようこそ。あなたは今、マクロの記録から一歩踏み出し、もっとパワフルで賢い自動化を目指したい、そんな熱い気持ちを抱いていることでしょう。素晴らしいです!

Word VBAの世界には、知れば知るほど奥深く、そして驚くほど効率的なテクニックがたくさん眠っています。今回は、そんな中でも特に「これは知っておいて損はない!」と断言できる、「検索結果をRange配列に格納して一括処理する」という、目から鱗のテクニックをご紹介します。

「え、検索結果って、一つずつ処理するんじゃないの?」と思ったあなた。その直感、実は少しだけ効率の改善の余地があるんです。このテクニックをマスターすれば、あなたのWord VBAマクロは、まるでスポーツカーのように軽快に、そしてパワフルに動作するようになるはずです。

さあ、一緒にWord VBAの「検索」という操作の本質に迫り、その効率化の極意を解き明かしていきましょう!

なぜ、検索結果を「配列」に格納するのが効率的なのか?

まず、なぜ検索結果を配列に格納することが、そんなにも効率的なのでしょうか?ここには、Word VBAのオブジェクトモデルの理解と、処理の「重み」が関係してきます。

1. オブジェクトの「重み」を理解する

Word VBAでは、テキストの検索や置換を行う際、`Find`オブジェクトという強力なツールを使います。しかし、`Find.Execute`メソッドを繰り返し呼び出すたびに、Wordはドキュメント全体をスキャンし、条件に合う箇所を探し出すという、それなりに「重い」処理を行います。

イメージしてみてください。あなたが図書館で特定のキーワードの本を探しているとします。

  • 逐次検索(ひとつずつ探す): 「あ、これかな?」と見つけるたびに、棚に戻ってまた最初から探す。これでは時間がかかりますよね。
  • 一括検索(まとめて探す): まず、図書館員に「このキーワードの本は全部で何冊ありますか?どこにありますか?」と質問し、リストをもらう。そして、そのリストを見ながら効率的に回収する。

Word VBAにおける`Find.Execute`の繰り返し処理は、前者(逐次検索)に似ています。一方、後述する「配列に格納する」方法は、後者(一括検索)のアプローチに近くなるのです。

2. `Find.Execute`の挙動と「ループの罠」

`Find.Execute`メソッドは、デフォルトでは、指定された条件に最初に一致した範囲を返します。もし、同じ条件で再度`Find.Execute`を実行すると、その「次」の範囲を探しに行きます。

‘ これはよくある、でも効率の悪い例です
Dim foundRange As Range
Set foundRange = ActiveDocument.Content
foundRange.Find.ClearFormatting
With foundRange.Find
.Text = “検索したい文字”
.Forward = True
.Wrap = wdFindContinue ‘ 検索を続行する
.Execute
Do While .Found
‘ ここで見つかった範囲 (foundRange) を処理する
Debug.Print foundRange.Text

‘ 次の検索のために、検索開始位置を更新
Set foundRange = ActiveDocument.Content
foundRange.Start = foundRange.Start + 1 ‘ 1文字進む
foundRange.End = foundRange.End
foundRange.Find.ClearFormatting
With foundRange.Find
.Text = “検索したい文字”
.Forward = True
.Wrap = wdFindContinue
.Execute
End With
Loop
End With

このコードは、直感的には「見つかったら、その次を探す」という処理をしていますが、実は`Find.Execute`を呼び出すたびに、Wordはドキュメント全体を(あるいは、指定した範囲を)「最初から」スキャンし直しているのです。

さらに、上の例では、毎回`ActiveDocument.Content`を取得し直し、`Start`と`End`を設定しています。これは非常に非効率的で、特にドキュメントが大きくなると、処理速度の低下に直結します。

3. 配列への格納で「スキャン回数」を劇的に減らす

ここで、私たちが目指す「配列に格納する」テクニックが活きてきます。このテクニックの核心は、一度の`Find.Execute`で、条件に合うすべての範囲を効率的に列挙し、それらを配列(またはコレクション)に格納してしまうことです。

これにより、ドキュメント全体のスキャンは、実質的に一度だけで済みます。あとは、格納された配列(コレクション)から各要素を取り出して、必要な処理を行うだけです。

Word VBAで「検索結果」をRange配列に格納する具体的な方法

では、具体的にどのようにすれば、検索結果をRange配列に格納できるのでしょうか?ここでは、最も一般的で実用的な方法を、コード例と共に解説していきます。

1. `Find.Execute`の「隠し機能」と`Find.Found`プロパティ

`Find.Execute`メソッドは、条件に一致する最初の範囲を見つけると、その範囲を`Find`オブジェクトのプロパティ(例えば`Range`プロパティ)で参照できるようになります。そして、`Find.Found`プロパティが`True`になります。

この`Find.Found`プロパティが`True`の間、`Find`オブジェクトは「まだ検索が続いている」状態と考えることができます。この性質を利用して、ループ処理で次の検索結果を取得していくのが基本戦略です。

2. 効率的なループ処理の実践

ここが今回の肝となる部分です!`Find.Execute`を繰り返し呼び出すのではなく、一度初期設定を行い、ループの中で「次」を見つけていく方法です。

Sub SearchAndStoreRanges()

Dim doc As Document
Set doc = ActiveDocument ‘ 現在アクティブなドキュメントを取得

Dim searchString As String
searchString = “検索したい文字” ‘ ここに検索したい文字列を入力

Dim foundRanges As Collection ‘ 検索結果のRangeオブジェクトを格納するコレクション
Set foundRanges = New Collection

Dim findObj As Find
Set findObj = doc.Content.Find ‘ ドキュメント全体を検索対象とする

‘ — 検索条件の設定 —
With findObj
.ClearFormatting ‘ 前回の検索条件をクリア
.Text = searchString ‘ 検索する文字列
.Forward = True ‘ 前方検索(ドキュメントの先頭から末尾へ)
.Wrap = wdFindStop ‘ ドキュメントの終端に達したら検索を停止
‘ wdFindContinue だと、先頭に戻って無限ループの可能性
.MatchCase = False ‘ 大文字・小文字を区別しない
.MatchWholeWord = False ‘ 単語全体を一致させるか(Falseなら部分一致もOK)
‘ 必要に応じて他のオプションも設定できます
‘ .MatchWildcards = True
‘ .MatchSoundsLike = False
‘ .MatchAllWordForms = False
End With

‘ — 最初の検索実行 —
If findObj.Execute Then
‘ 一致する最初の範囲が見つかった
‘ 見つかった範囲をコレクションに追加
foundRanges.Add findObj.Parent ‘ findObj.Parent は、見つかった範囲そのもの
‘ Range オブジェクトは参照渡しされるので、
‘ ここで追加しておけば、後で参照できる

‘ — ループで次の検索結果を取得 —
‘ Find.Execute を再度呼び出すと、直前の検索位置の「次」から検索してくれる
Do While findObj.Execute
‘ 見つかった範囲をコレクションに追加
foundRanges.Add findObj.Parent
Loop
End If

‘ — 検索結果の処理 —
If foundRanges.Count > 0 Then
MsgBox “「” & searchString & “」が ” & foundRanges.Count & ” 件見つかりました。”

‘ 例: 見つかったすべての範囲のテキストをイミディエイトウィンドウに表示
Dim r As Range
For Each r In foundRanges
Debug.Print “— 見つかった範囲 —”
Debug.Print r.Text ‘ 見つかった範囲のテキスト
Debug.Print “開始位置: ” & r.Start
Debug.Print “終了位置: ” & r.End
Debug.Print “——————–”

‘ ★ここからが本番!格納したRangeオブジェクトを使った一括処理★
‘ 例えば、見つかったすべての「検索したい文字」を太字にする
‘ r.Font.Bold = True ‘ ← これは注意!ループ中に書式変更すると、
‘ 次の検索範囲がずれる可能性がある
‘ 安全なのは、一度格納してから、
‘ 別のループで処理することです。
Next r

‘ — 安全な一括書式変更の例 —
Application.ScreenUpdating = False ‘ 画面更新を停止して高速化
For Each r In foundRanges
r.Font.Bold = True ‘ 見つかったすべての範囲のテキストを太字に
Next r
Application.ScreenUpdating = True ‘ 画面更新を再開

Else
MsgBox “「” & searchString & “」は見つかりませんでした。”
End If

‘ オブジェクトの解放 (VBAでは自動解放されることが多いですが、明示的に行うとより丁寧です)
Set findObj = Nothing
Set foundRanges = Nothing
Set doc = Nothing

End Sub

コードの解説とポイント

  • `Dim foundRanges As Collection`: 検索で見つかった`Range`オブジェクトを格納するために、`Collection`オブジェクトを使用しています。`Collection`は、要素の追加や削除が容易で、VBAでよく使われるデータ構造です。配列 (`Array`) と異なり、事前にサイズを決める必要がありません。
  • `Set findObj = doc.Content.Find`: `doc.Content`は、ドキュメント全体の`Range`オブジェクトを表します。その`.Find`プロパティで、ドキュメント全体を対象とした`Find`オブジェクトを取得します。
  • `.ClearFormatting`: これは非常に重要です! 前回の`Find`操作で設定された書式設定などが残っていると、意図しない検索結果になることがあります。必ずクリアしてから検索条件を設定しましょう。
  • `.Wrap = wdFindStop`: ここも注意が必要です。`wdFindContinue`を設定すると、ドキュメントの終端に達したときに先頭に戻って検索を続行しようとします。もし、検索対象の文字列がドキュメントの終端付近にあり、かつ`wdFindContinue`を設定していると、意図せず無限ループに陥る可能性があります。通常は`wdFindStop`で、ドキュメントの終端で検索を終了するのが安全です。
  • `If findObj.Execute Then … Loop`:
  • 最初に`findObj.Execute`を呼び出し、最初の検索結果を取得します。
  • `findObj.Parent`は、`Find`オブジェクトが適用されている`Range`(この場合は`doc.Content`)の中で、見つかった範囲そのものを指します。これを`foundRanges.Add`でコレクションに追加します。
  • `Do While findObj.Execute`のループ内で再度`Execute`を呼び出すことで、Wordは自動的に直前の検索位置の「次」から、次の検索結果を探してくれます。見つかるたびに`foundRanges.Add`でコレクションに追加していきます。
  • `foundRanges.Count`: コレクションに格納された要素の数を取得できます。
  • `For Each r In foundRanges`: コレクションに格納された各`Range`オブジェクト(変数`r`)に対して、一連の処理を行うことができます。
  • 画面更新の停止 (`Application.ScreenUpdating = False`): 大量のRangeオブジェクトに対して書式変更などを行う場合、画面がちらつくと処理速度が著しく低下します。`ScreenUpdating`を`False`にすると、画面更新が一時停止され、処理が高速化されます。処理の最後に`True`に戻すのを忘れないでください。

補足:Range配列 vs Collection

今回は`Collection`を使用しましたが、VBAの動的配列 (`Dim arr() As Range`) を使うことも可能です。

‘ 動的配列を使う場合の例 (Collectionよりも少し手間がかかります)
Dim foundRangesArr() As Range
Dim count As Long
count = 0 ‘ カウンター初期化

If findObj.Execute Then
‘ 配列のサイズを1つ拡張して要素を追加
ReDim Preserve foundRangesArr(count)
Set foundRangesArr(count) = findObj.Parent
count = count + 1

Do While findObj.Execute
ReDim Preserve foundRangesArr(count)
Set foundRangesArr(count) = findObj.Parent
count = count + 1
Loop
End If

‘ 処理部分
For i = 0 To UBound(foundRangesArr)
Debug.Print foundRangesArr(i).Text
Next i

`ReDim Preserve`は、配列のサイズを変更するたびに既存のデータを保持してくれる便利な機能ですが、配列のサイズが大きくなるにつれて処理に時間がかかるようになります。そのため、要素の追加・削除が頻繁に発生するようなケースでは、`Collection`の方が一般的に効率的と言えます。

陥りやすいエラーと、その回避策

このテクニックを実践する上で、いくつか注意しておきたい点があります。

1. 無限ループの危険性:

  • 原因: `.Wrap = wdFindContinue` を設定し、かつ検索対象の文字列がドキュメントの終端付近に存在する場合。
  • 回避策: 通常は `.Wrap = wdFindStop` を使用しましょう。どうしてもループを続けたい場合は、検索開始位置を明示的に制御するなどの工夫が必要です。

2. 書式変更による検索位置のずれ:

  • 原因: 検索結果のRangeオブジェクトを処理するループの中で、そのRangeオブジェクトの書式(フォントサイズ、色、太字など)を変更してしまうと、Wordの内部的なドキュメント構造が変化し、次の`Find.Execute`が意図しない位置から始まってしまうことがあります。
  • 回避策:
  • 推奨: まず、すべての検索結果を`Collection`(または配列)に格納します。
  • その後、別のループで、格納した`Collection`(または配列)の各Rangeオブジェクトに対して書式変更などの処理を行います。`Application.ScreenUpdating = False` を活用すると、さらに効果的です。

3. オブジェクトの参照渡しと解放:

  • 原因: VBAでは、オブジェクトは「参照渡し」されることが多く、`Set`で代入されたオブジェクトは、元のオブジェクトと同一のものを指します。`Collection`に`Range`オブジェクトを追加した場合、`Collection`内の`Range`オブジェクトは、ドキュメント内の実際のRangeを参照しています。
  • 回避策: コードの最後に、使用したオブジェクト変数に`Nothing`を代入して解放する習慣をつけましょう。これにより、メモリリークを防ぎ、コードの可読性も向上します。

まとめ:Word VBAの基本は「効率」と「本質」の理解にあり!

いかがでしたか?今回は、Word VBAで検索結果を効率的に処理するための「Range配列(Collection)への格納」テクニックをご紹介しました。

このテクニックをマスターすることで、

  • 検索速度が劇的に向上する
  • コードがよりスマートで読みやすくなる
  • 複雑なドキュメント操作も効率的に行えるようになる

といったメリットが得られます。

マクロの記録から脱却し、より高度な自動化を目指す皆さんにとって、この「検索結果の配列格納」は、まさに王道とも言えるテクニックです。今回ご紹介したコード例を参考に、ぜひご自身のWord VBAプロジェクトで活用してみてください。

Word VBAの操作は、単にコマンドを並べるだけでなく、その背後にあるオブジェクトのライフサイクルや、処理の「重み」を理解することが、真の効率化への鍵となります。今回学んだことを活かして、あなたのWord VBAスキルをさらに一段階引き上げていきましょう!

ここをクリアすれば、Word VBAの「検索」という操作に関する基本はバッチリですよ。応援しています!

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