【実務・中級編】【中級者向け】正規表現と組み合わせて、特定のパターンを持つ段落のみに書式を適用する – Word VBA解析バイブル

スポンサーリンク

【Word VBAを掌握する極限の知見】正規表現エンジンをWordに叩き込め!パターンマッチングによる高速・堅牢な段落装飾の極意

Word VBAにおける書式設定において、多くの開発者が「標準の `Find` オブジェクトの限界」に直面する。
「文頭の数字で始まる段落だけ太字にしたい」「特定のキーワードを含む行のスタイルを一括変更したい」――。GUIの高度な検索・置換ですら、ワイルドカードの貧弱な仕様に阻まれ、複雑な条件分岐を組もうとすれば途方もないネストの嵐と格闘する羽目になる。

ここで安易に `Paragraphs` コレクションを上から順に `For Each` で回し、 `.Range.Text` を一つずつ舐めるようなコードを書くようでは、エンジニア失格だ。数千ページのドキュメントでそんな処理を走らせれば、COMのプロセス間通信のオーバーヘッドで処理はフリーズし、実務では使い物にならない。

今回は、VBScriptの正規表現エンジン(RegExp)をWord VBAにマウントし、メモリ上で一網打尽にパースした上で、一撃で書式を焼き付ける「プロダクション・クオリティ」の設計と実装を伝授する。

1. 愚直なループが破綻する理由:Word VBAのパフォーマンス哲学

なぜ `For Each para In ActiveDocument.Paragraphs` が悪手なのか。
WordのVBAからDocumentオブジェクトを操作する際、背後ではCOM(Component Object Model)を介したブリッジ通信が発生している。VBA側からWordのUIスレッドに対して「テキストをくれ」「書式を変えろ」と命令するたびに、このオーバーヘッドが積もる。

さらに、`Paragraph.Range` を無闇に取得・操作すると、Wordの内部ストーリー(Story)が頻繁に再描画・再計算を強いられ、実行時間が幾何級数的に跳ね上がる。

堅牢な設計アプローチ

1. 一括読み込み(または範囲の特定): 文書全体のテキスト構造を極力効率よくハンドリングする。
2. 正規表現によるメモリ上での一括マッチング: VBAのメモリ空間上で `RegExp` オブジェクトを走らせ、対象となる段落のインデックスやRangeを特定する。
3. トランザクション的適用: 画面描画(`ScreenUpdating`)と警告(`DisplayAlerts`)を完全にシャットダウンし、一気に書式を流し込む。

2. 実装:正規表現×段落装飾 プロダクションコード

以下のコードは、「行頭が数字(1桁以上)で始まり、その後にスペースまたは句点続いている段落」を正規表現で検出し、フォントをメイリオ・太字・文字色を青に変更する実務直結のモジュールだ。

実務の現場でそのままコピー&ペーストし、即座に組み込めるレベルの堅牢性(エラーハンドリング、画面描画の抑制)を持たせてある。

Option Explicit

‘================================================================================
‘ módulo名: modParagraphRegExpStyler
‘ 概要: 正規表現を用いて、特定のパターンに合致する段落のみを高パフォーマンスに装飾する
‘================================================================================
Public Sub ApplyStyleByRegex()
‘ 1. 宣言とパフォーマンス最適化の準備
Dim startTime As Double
startTime = Timer

‘ 画面描画と警告を停止し、処理速度を限界まで引き上げる
With Application
.ScreenUpdating = False
.DisplayAlerts = wdAlertsNone
.Calculation = wdCalculationManual ‘ 自動計算も一時停止
End With

On Error GoTo ErrorHandler

‘ 2. 正規表現エンジンの初期化 (VBScript.RegExp)
Dim regEx As Object
Set regEx = CreateObject(“VBScript.RegExp”)

With regEx
‘ 【パターン定義】行頭(^)から数字([0-9]+)、その後にスペースまたは全角スペース(\s)が続く
.Pattern = “^[0-9]+[\s ]”
.IgnoreCase = False
.Global = True ‘ 1段落内に複数マッチする場合も考慮(今回は行頭限定なのでFalseでも可)
.MultiLine = True ‘ ^ を各行の先頭として認識させる
End With

‘ 3. ドキュメントの全段落を走査
Dim targetDoc As Document
Set targetDoc = ActiveDocument

Dim p As Paragraph
Dim targetRange As Range
Dim matchCount As Long
matchCount = 0

‘ 段落コレクションのループ
For Each p In targetDoc.Paragraphs
‘ 段落のテキストを取得(末尾の段落記号を除外するために .Duplicate または Trim を考慮)
‘ ※ Paragraph.Range.Text は末尾に vbCr (Chr(13)) を含むため、これを除去して判定する
Dim paraText As String
paraText = p.Range.Text

‘ 末尾の改行コード(vbCr または vbCrLf)を除去
If Right(paraText, 1) = vbCr Then
paraText = Left(paraText, Len(paraText) – 1)
End If

‘ 正規表現マッチング判定
If regEx.Test(paraText) Then
‘ マッチした段落に対する処理
‘ 段落全体のRangeを取得し、必要に応じて書式を適用
Set targetRange = p.Range

‘ — 【装飾の適用ロジック】 —
With targetRange.Font
.Name = “メイリオ”
.Bold = True
.Color = RGB(0, 51, 102) ‘ 深い青
End With

‘ 段落自体のインデントやスタイルを変更する場合もここで制御可能
‘ p.Style = “見出し 2” 等の連携も有効

matchCount = matchCount + 1

‘ 巨大な文書でのメモリリークを防ぐためオブジェクトを解放
Set targetRange = Nothing
End If
Next p

‘ 4. 終了処理とパフォーマンスの復元
Call RestoreEnvironment

MsgBox “処理が完了しました。” & vbCrLf & _
“対象段落数: ” & matchCount & ” 件” & vbCrLf & _
“実行時間: ” & Format(Timer – startTime, “0.00”) & ” 秒”, _
vbInformation, “完了”

Exit Sub

ErrorHandler:
‘ 異常終了時も確実に環境を復元する(COMのフリーズを防ぐ)
Call RestoreEnvironment
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“Error: ” & Err.Description, vbCritical, “エラー”
End Sub

‘================================================================================
‘ 環境復元用のプライベートプロシージャ
‘================================================================================
Private Sub RestoreEnvironment()
With Application
.ScreenUpdating = True
.DisplayAlerts = wdAlertsAll
.Calculation = wdCalculationAutomatic
End With
End Sub

3. チーフアーキテクトが教える「現場でハマる罠」と回避策

このコードをそのままエンタープライズの現場に投入するにあたり、知っておくべき「落とし穴」と、それを回避するための知見を共有する。

1. 改行コード(`vbCr`)の罠

Wordの `Paragraph.Range.Text` が返す文字列の末尾には、必ず段落区切りを表す `vbCr`(Character 13)が付与されている。正規表現のパターンで `$`(行末)や `^`(行頭)を使う際、この制御文字の存在によって意図しないマッチミスが起きる。
上記のコードでは、あらかじめ末尾の `vbCr` を削ぎ落とした純粋な文字列に対して `RegExp.Test` を走らせている。この一手間を惜しむと、「なぜか最終行だけマッチしない」といった不可解なバグに数時間を溶かすことになる。

2. 大規模文書におけるメモリ管理

数百ページに及ぶ仕様書や契約書を相手にする場合、`For Each p In targetDoc.Paragraphs` のループ内で生成されるCOMオブジェクトの参照がスタックを圧迫することがある。
ループ内で `Set targetRange = p.Range` としたら、処理の最後(あるいは次のループの頭)で確実に参照を切り捨てる意識を持て。VBAのガベージコレクションは気まぐれだ。自分でクリーンアップする習慣こそが、落ちないマクロを作る。

3. スタイルとの競合

直接 `targetRange.Font.Name = “メイリオ”` とハードコーディングしているが、ドキュメント全体が「Wordのスタイル機能(標準スタイル)」に強く依存している場合、後からスタイルの更新によって上書きされてしまうリスクがある。
もしプロジェクトの設計方針として「スタイル駆動」が強制されているのであれば、フォントを直接いじるのではなく、マッチした段落の `p.Style` プロパティ自体を、あらかじめ用意したカスタムスタイルに書き換える設計にシフトすべきだ。

4. ささらなる高みへ:データベースや外部ファイル連携の布石

今回はハードコーディングされた正規表現パターンを用いたが、実務の現場では「どのパターンを検索してどう装飾するか」を外部のJSONファイルやExcel、あるいはデータベース(SQLiteやSQL Server)から動的に読み込ませたいという要件が必ず降ってくる。

その際、今回構築した `ApplyStyleByRegex` の構造(環境の保護 ➔ エンジンの初期化 ➔ 外部から受け取ったパターン配列のループ ➔ 適用 ➔ 環境の復元)がそのままベース architecture になる。

スクリプトの表面的な書き方を覚える段階はもう終わりにしよう。
Wordの内部構造(StoryとRange、Paragraphsコレクション)の重みを理解し、COM通信のボトルネックを支配した者だけが、実務で真に信頼される「高速でエレガントな自動化ツール」を構築できる。

あなたのコードベースにこの知見を組み込み、退屈な手作業を過去のものにしてほしい。

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