【Word VBA上級編】正規表現とRangeオブジェクトで極める「コンテキスト非依存型スマートクォート変換エンジン」の構築
単なる「文字の置き換え」と高をくくっているなら、今すぐその認識を改めなさい。
Word文書における直線引用符(ストレートクォート:`”` や `’`)を、文脈に応じた左右のスマートクォート(`“` `”` / `‘` `’`)へ自動変換する処理は、テキスト処理における極めて難易度の高いコンテキスト解析の領域である。
組み込みの「入力オートフォーマット」に依存したバッチ処理や、愚直な `Selection.Find` のループ処理は、書式(ボールド、イタリック、文字色)の破壊、縮約形(`don’t` や `’90s`)のアポストロフィ判定ミス、そして大量ページ文書でのパフォーマンス崩壊を引き起こす。
本稿では、世界最高峰のエンジニアリング視点から、「VBScript.RegExp」による文脈判定とWord「Rangeオブジェクト」による書式非破壊のピンポイント置換を融合させた、プロダクションレベルの変換エンジンを設計・解説する。
—
1. なぜ既存の「置換」ロジックは全滅するのか?
開発現場で安易に実装されたVBAツールが爆発する原因は、主に以下の3点に集約される。
(1) DOM非破壊原則の欠如(書式の喪失)
`ActiveDocument.Content.Text` に対して文字列処理(`Replace` 関数や RegExp の `Replace` メソッド)を直接実行すると、文書内のフォントサイズ、ハイパーリンク、文字色、下線、さらにはインライン画像がすべて消失する。Wordにおけるテキストは単なる「文字列」ではなく、高度な装飾情報を持った「オブジェクトの階層構造(DOM)」である。
(2) 文脈(Context)解釈の限界
単一の引用符 `’` は、以下の4つの文脈で全く異なる文字へ変換されなければならない。
1. 開始引用符 (`‘`): 直前にスペースまたは行頭があり、直後に文字がある場合(例: `’Hello’`)
2. 終了引用符 (`’`): 直前に文字があり、直後にスペースや句読点がある場合(例: `’Hello’`)
3. アポストロフィ/縮約形 (`’`): 単語内部に存在する場合(例: `don’t`, `it’s`)
4. 年代省略記号 (`’`): 数値の前に位置する場合(例: `’90s`)
既存の単純なルールエンジンでは、`don’t` のアポストロフィが左引用符 `‘don’t` に誤変換される悲劇を回避できない。
(3) `Selection` オブジェクトの乱用による画面描画のオーバーヘッド
`Selection.Find` をループさせるコードは、1回の検索ごとに画面描画とカーソル移動を伴うため、100ページの文書で実行すれば処理時間は数分におよび、最悪の場合フリーズする。非同期処理の存在しないVBAにおいては、「非表示の `Range` オブジェクト」によるメモリ上での局所操作が絶対条件となる。
—
2. 堅牢なエンジニアリング・アーキテクチャ
本ツールでは、以上の問題を解決するために「ハイブリッド・インデックスマッピング方式」を採用する。
[ ActiveDocument (Word DOM) ]
│
├─ 1. テキスト抽出 (メモリ上の文字列取得)
│
[ VBScript.RegExp Engine ] ── 2. 高速パターン解析 (位置・文脈の決定)
│
├─ 3. マッチした絶対インデックス (Start / End) の配列化
│
[ Localized Range Mutator ] ── 4. 文書上のターゲット Range のみを直接更新
1. メモリ上でのパース: 文書全体のプレーンテキストを取得し、`VBScript.RegExp` で超高速に正規表現マッチングを実施。
2. オフセットの記録: 変更すべき箇所の「文字の開始位置(Start)」と「終了位置(End)」、および「置換後の正しい文字」をスタック(配列)に格納。
3. 逆順(Reverse Order)での DOM 置換: 記録したインデックスに基づき、文書の末尾から先頭に向かって Word の `Range` オブジェクトをアタッチし、文字のみをピンポイント書き換え。
(※先頭から置換すると、文字数の増減によって後続のインデックスがズレるため、逆順処理が厳格な鉄則である)
—
3. プロダクション適用可能な完成コード
以下のコードをWordの標準モジュールにペーストして使用されたい。参照設定の追加は不要(Late Binding実装)とし、環境依存を排除している。
Attribute VB_Name = “ModSmartQuotesEngine”
Option Explicit
‘ ==============================================================================
‘ 構造体定義:置換タスク情報を保持
‘ ==============================================================================
Private Type TQuoteMatch
StartIndex As Long
EndIndex As Long
ReplacementText As String
End Type
‘ ==============================================================================
‘ メインエントリポイント
‘ ==============================================================================
Public Sub ExecuteSmartQuotesConversion()
Dim startTime As Single
startTime = Timer
‘ 画面描画と各種イベントの停止によるパフォーマンス極限化
Dim originalScreenUpdating As Boolean
originalScreenUpdating = Application.ScreenUpdating
Application.ScreenUpdating = False
‘ Undo履歴の一括化 (Word 2013以上対応の防護コード)
Dim undoObj As UndoRecord
Set undoObj = Application.UndoRecord
undoObj.StartCustomRecord “コンテキスト依存型スマートクォート変換”
On Error GoTo ErrorHandler
‘ 処理実行
Dim doc As Document
Set doc = ActiveDocument
Dim totalConverted As Long
totalConverted = ProcessDocumentQuotes(doc)
‘ 正常終了処理
undoObj.EndCustomRecord
Application.ScreenUpdating = originalScreenUpdating
MsgBox “スマートクォート変換が完了しました。” & vbCrLf & _
“変換箇所: ” & totalConverted & ” 箇所” & vbCrLf & _
“実行時間: ” & Format(Timer – startTime, “0.00”) & ” 秒”, _
vbInformation, “処理完了”
Exit Sub
ErrorHandler:
undoObj.EndCustomRecord
Application.ScreenUpdating = originalScreenUpdating
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “実行時エラー”
End Sub
‘ ==============================================================================
‘ コア・ロジック:正規表現解析と Range 置換
‘ ==============================================================================
Private Function ProcessDocumentQuotes(ByRef doc As Document) As Long
Dim docText As String
docText = doc.Content.Text
‘ VBScript.RegExp の動的生成(参照設定不要)
Dim regEx As Object
Set regEx = CreateObject(“VBScript.RegExp”)
regEx.Global = True
regEx.IgnoreCase = True
regEx.Multiline = True
‘ タスクマッチを保持する動的配列
Dim matchesList() As TQuoteMatch
Dim matchCount As Long
matchCount = 0
‘ ————————————————————————–
‘ パス 1: ダブルクォート (“) の分析
‘ ————————————————————————–
‘ 開始ダブルクォート “ : 直前が空白、行頭、開き括弧
‘ 終了ダブルクォート ” : それ以外(文字、句読点の直後)
‘ ————————————————————————–
regEx.Pattern = “”” Calibration “”” ‘ ダミー(パース用構文)
regEx.Pattern = “”””
Dim regMatches As Object
Set regMatches = regEx.Execute(docText)
Dim matchItem As Object
Dim precedingChar As String
Dim targetMatch As TQuoteMatch
For Each matchItem In regMatches
ReDim Preserve matchesList(matchCount)
targetMatch.StartIndex = matchItem.FirstIndex
targetMatch.EndIndex = matchItem.FirstIndex + matchItem.Length
‘ 前置文字の判定
If targetMatch.StartIndex = 0 Then
precedingChar = “”
Else
precedingChar = Mid$(docText, targetMatch.StartIndex, 1)
End If
‘ コンテキスト判定ロジック
If precedingChar = “” Or precedingChar Like “[ ” & vbTab & vbCr & vbLf & “([{<]" Then
targetMatch.ReplacementText = ChrW(&H201C) ' “ (開始ダブル)
Else
targetMatch.ReplacementText = ChrW(&H201D) ' ” (終了ダブル)
End If
matchesList(matchCount) = targetMatch
matchCount = matchCount + 1
Next matchItem
' --------------------------------------------------------------------------
' パス 2: シングルクォート (') および アポストロフィの分析
' --------------------------------------------------------------------------
' A. 縮約形・所有格 (don't, it's, John's) -> 常に ’ (右シングル/アポストロフィ &H201D ではなく &H2019)
‘ B. 年代省略 (’90s) -> ’ (&H2019)
‘ C. 開始シングル ‘ (&H2018) : 直前が空白/行頭/括弧
‘ D. 終了シングル ’ (&H2019) : その他
‘ ————————————————————————–
regEx.Pattern = “‘”
Set regMatches = regEx.Execute(docText)
Dim followingChar As String
For Each matchItem In regMatches
ReDim Preserve matchesList(matchCount)
targetMatch.StartIndex = matchItem.FirstIndex
targetMatch.EndIndex = matchItem.FirstIndex + matchItem.Length
‘ 前後文字の取得
If targetMatch.StartIndex = 0 Then
precedingChar = “”
Else
precedingChar = Mid$(docText, targetMatch.StartIndex, 1)
End If
If targetMatch.StartIndex + 1 >= Len(docText) Then
followingChar = “”
Else
followingChar = Mid$(docText, targetMatch.StartIndex + 2, 1)
End If
‘ コンテキスト高度判定
If precedingChar Like “[a-zA-Z0-9]” And followingChar Like “[a-zA-Z]” Then
‘ 単語内部のアポストロフィ (don’t, rock’n’roll)
targetMatch.ReplacementText = ChrW(&H2019)
ElseIf precedingChar Like “[ ” & vbTab & vbCr & vbLf & “]” And followingChar Like “[0-9]” Then
‘ 年代省略 (’90s)
targetMatch.ReplacementText = ChrW(&H2019)
ElseIf precedingChar = “” Or precedingChar Like “[ ” & vbTab & vbCr & vbLf & “([{<]" Then
' 通常の開始シングルクォート
targetMatch.ReplacementText = ChrW(&H2018)
Else
' 通常の終了シングルクォート
targetMatch.ReplacementText = ChrW(&H2019)
End If
matchesList(matchCount) = targetMatch
matchCount = matchCount + 1
Next matchItem
If matchCount = 0 Then
ProcessDocumentQuotes = 0
Exit Function
End If
' --------------------------------------------------------------------------
' パス 3: マッチリストのソート(StartIndex の降順)
' 文末から置換しなければ、インデックスがズレてDOM破壊が起きる
' --------------------------------------------------------------------------
QuickSortMatches matchesList, LBound(matchesList), UBound(matchesList)
' --------------------------------------------------------------------------
' パス 4: Safe Range Mutation (書式を破壊しない直接置換)
' --------------------------------------------------------------------------
Dim i As Long
Dim targetRange As Range
For i = UBound(matchesList) To LBound(matchesList) Step -1
' Word DOM の Range オブジェクトを構築
Set targetRange = doc.Range(Start:=matchesList(i).StartIndex, End:=matchesList(i).EndIndex)
' 検証:意図しない文字列の置換を防ぐ防護策
If targetRange.Text = """" Or targetRange.Text = "'" Then
' Textプロパティへの代入は、そのRangeのフォント書式(Bold, Color等)を維持する
targetRange.Text = matchesList(i).ReplacementText
End If
Next i
ProcessDocumentQuotes = matchCount
End Function
' ==============================================================================
' サポートアルゴリズム:インデックス降順用クイックソート
' ==============================================================================
Private Sub QuickSortMatches(ByRef arr() As TQuoteMatch, ByVal inLow As Long, ByVal inHigh As Long)
Dim pivot As Long
Dim tmp As TQuoteMatch
Dim mLow As Long
Dim mHigh As Long
mLow = inLow
mHigh = inHigh
pivot = arr((inLow + inHigh) \ 2).StartIndex
Do While mLow <= mHigh
Do While arr(mLow).StartIndex < pivot And mLow < inHigh
mLow = mLow + 1
Loop
Do While pivot < arr(mHigh).StartIndex And mHigh > inLow
mHigh = mHigh – 1
Loop
If mLow <= mHigh Then
tmp = arr(mLow)
arr(mLow) = arr(mHigh)
arr(mHigh) = tmp
mLow = mLow + 1
mHigh = mHigh - 1
End If
Loop
If inLow < mHigh Then QuickSortMatches arr, inLow, mHigh
If mLow < inHigh Then QuickSortMatches arr, mLow, inHigh
End Sub
---
4. チーフアーキテクトによるコード解説とハマり所
このコードには、上級エンジニアが押さえるべき「Word APIの挙動の裏側」が詰まっている。ポイントを厳密に解説する。
① `Unicode (ChrW)` による明示的文字コード指定
コード内で `“` や `’` を直接ソースコードにハードコーディングしてはならない。VBAエディタ(VBE)のコードページ(Shift-JIS環境等)によっては、ソースコード上の特殊文字が文字化けし、予期せぬ文字へ置換されるリスクがある。
本コードでは、すべて Unicode ポイント (`ChrW(&H2018)`~`ChrW(&H201D)`) を直接指定することで、環境に依存しない完全な可搬性を担保している。
② クイックソート(降順)による DOM 整合性の維持
テキストの置換を行う際、「先頭から置換する」のは素人の書くコードである。
例えば、位置10の1文字を置換し、それがたまたま2文字のUnicodeシーケンスに変わった場合、位置20にあったはずの文字は位置21にズレる。これにより、以降の置換位置がすべて破壊される。
本アーキテクチャでは、パースした全マッチを `StartIndex` の降順にソートし、「文書の末尾から先頭に向かって」 置換を実行している。これにより、先行するテキストの位置インデックスは常に不変であり、ズレは一切発生しない。
③ `Range.Text` 代入時の書式保持ルール
WordのDOMにおいて、`Range.Text = “X”` を実行した際、その `Range` に適用されていた「文字装飾(フォント、サイズ、太字、斜体、文字色)」は継承される。
一方で `Range.InsertAfter` や `Selection.TypeText` を使うと、前後のカーソル位置のスタイルに引っ張られ、書式が崩れる原因となる。ピンポイントでの `Range.Text` 書き換えこそが、極限の安全性を確保する手法である。
—
5. エンタープライズ適用時の運用・拡張上の注意点
実務環境へのデプロイ(アドイン化や社内配布)を行う場合、以下のエッジケースに対する設計上の考慮が必要となる。
1. ヘッダー・フッターおよびテキストボックス(StoryRanges)の扱い
上記メインコードの `doc.Content` は、文書の「本文(Main Text Story)」のみを対象としている。
ヘッダー、フッター、描画オブジェクト内のテキストボックス、脚注(Footnotes)も全自動変換の対象とする場合は、以下のように `StoryRanges` コレクションを列挙するループでカプセル化する必要がある。
Dim storyRange As Range
For Each storyRange In doc.StoryRanges
Do
‘ 各 StoryRange に対して ProcessDocumentQuotes と同等の処理を適用
Set storyRange = storyRange.NextStoryRange
Loop Until storyRange Is Nothing
Next storyRange
2. 変更履歴(Track Changes)との競合
変更履歴機能が有効(`doc.TrackRevisions = True`)になっている文書で本ツールを実行すると、大量の「文字置換の履歴」が生成され、ファイルサイズが膨れ上がる上に処理速度が極端に低下する。
本番ツールとしては、実行前に `doc.TrackRevisions` の状態をチェックし、警告を発するか一時的にオフにするガード節(Guard Clause)を実装するのがプロの設計である。
—
結論
Word VBA開発において、「動くコード」を書くことと「プロダクションに耐えうるコード」を書くことの間には巨大な鴻溝(こうこう)が存在する。
今回提示した「RegExpによる高速コンテキスト解析」+「Rangeインデックスの降順Mutation」のパターンは、スマートクォート変換にとどまらず、高度な校正ツールや複雑な文書構造解析ツールを構築する際の万能の設計パターン(Design Pattern)となる。
この思想をコードに刻み込み、頑丈で美しい自動化ツールを現場に届けてほしい。
