【Word VBA上級編】数万ページを秒殺する!Word文書の「XML直接DOM操作」による極限高速置換術
こんにちは。開発プロジェクトを率いるチーフアーキテクトの私だ。
Word VBAで最もフラストレーションが溜まる瞬間は何だと思う?
そう、数万ページ、あるいは数百ページに及ぶ巨大な仕様書や契約書に対し、標準の `Selection.Find` や `Range.Find` を回したときの「あの耐え難い遅さ」だ。
画面がチラつき、プログレスバーは微動だにせず、時には「応答なし」の闇に葬り去られる。Wordのオブジェクトモデル(COM)は、UIの描画や段落・文字の厳密なレイアウト計算を裏で常に行っている。そのため、単純な文字列の置換ごときに、莫大なオーバーヘッドを支払わされているのだ。
もし、あなたが「業務で数千ファイルのWordから特定キーワードを書き換えなければならない」「処理が終わるまでコーヒーを何杯も飲む羽目になっている」としたら、今すぐその非効率なアプローチを捨ててほしい。
今回は、Word文書の本体である `document.xml` をZIPとしてメモリ上で解凍し、DOM(Document Object Model)を使って瞬時に置換、再びパッケージングするという、プロフェッショナルだけが知る「極限の高速化手法」を授けよう。
—
1. なぜ「オブジェクトモデル経由のFind」は遅いのか?
敵を知るにはまずアーキテクチャを理解することだ。
`ActiveDocument.Content.Find.Execute` を実行すると、Wordは以下の処理を裏で行っている。
1. COMの境界を越えたプロセス間通信の発生
2. 各段落、各ラン(Run)単位でのレイアウト再計算と再描画の予約
3. アンドゥ(Undo)スタックへの状態保存
数万箇所の置換を行えば、これらが数万回繰り返される。遅くて当然だ。
究極の解:XMLの直接殴り込み(DOM操作)
現代の `.docx` ファイルの実体は、単なるZIPアーカイブだ。その中核である `word/document.xml` は、テキストデータ(XML)に過ぎない。
ならば、WordアプリケーションのUIを一切立ち上げず(あるいはバックグラウンドで最小限に抑え)、VBAからMSXML2.DOMDocumentを用いてXMLツリーを直接メモリ上で書き換えてしまえばいい。
レイアウト計算もUI描画も発生しない。メモリ上で文字列を置換するスピードそのままで、数万ページの処理が「一瞬(数ミリ秒〜数秒)」で終わる。これが本手法の正体だ。
—
2. アーキテクチャ設計と落とし穴(バグを防ぐ極意)
XMLを直接操作するアプローチには、当然リスクもある。ここを理解していない素人がコードを書くと、「保存した瞬間にWordから『ファイルが破損しています』と怒られる」という呪いにかかる。
落とし穴1:Word特有の「タグの断片化(Fragmented Runs)」
Wordで文章を入力するとき、ユーザーが「A」「B」「C」と連続して打っても、内部のXMLでは `
そのため、検索ワードが「ABC」であっても、単にXML全体を文字列置換すると、タグをまたいでいる場合にヒットしない。
【解決策】
今回は、厳密な文字単位の完全一致置換ではなく、プレースホルダー(例: `{{PROJECT_NAME}}` や `[KYWORD]` など)のような、単一の `
落とし穴2:ZIPの圧縮・解凍のボトルネック
VBA標準でZIPを扱うのは面倒だが、Windows環境であれば `ADODB.Stream` や `Shell.Application`、あるいはVBA標準のファイルシステムオブジェクト(FSO)を組み合わせることで、外部ツールなしで `.docx`(実質.zip)の解凍・再圧縮が可能だ。
—
3. 【プロダクションコード】XML DOM高速置換エンジン
それでは、実際の現場でそのまま使える、堅牢で保守性の高いVBAコードを公開する。
このコードは、指定したWordファイル内の特定のプレースホルダーを、一瞬で高速置換するものだ。
Option Explicit
‘ ==============================================================================
‘ módulo名: 業務自動化モジュール / Word XML高速置換エンジン
‘ 概要 : .docxファイルをZIPとして展開し、XMLのDOM操作によって爆速で文字列置換を行う
‘ 前提条件: 参照設定に「Microsoft XML, v6.0」を追加すること
‘ ==============================================================================
Public Sub ExecuteXmlFastReplace(ByVal targetFilePath As String, ByVal targetKey As String, ByVal replaceValue As String)
Dim fso As Object
Dim tempDir As String
Dim docXmlPath As String
Dim xmlDoc As Object
Dim textNodes As Object
Dim node As Object
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ 1. 一意のテンポラリディレクトリを作成
tempDir = fso.GetSpecialFolder(2) & “\WordXmlTemp_” & Format(Now, “yyyymmddhhmmss”) & “\”
fso.CreateFolder tempDir
On Error GoTo ErrorHandler
‘ 2. .docxを.zipにリネームして展開
Dim zipFilePath As String
zipFilePath = tempDir & “document.zip”
fso.CopyFile targetFilePath, zipFilePath, True
‘ Shell.Application を使って解凍
UnZipFiles zipFilePath, tempDir
‘ 3. word/document.xml のパスを特定
docXmlPath = tempDir & “word\document.xml”
If Not fso.FileExists(docXmlPath) Then
Err.Raise 9999, “XmlReplace”, “document.xmlが見つかりません。有効なWord文書ではありません。”
End If
‘ 4. MSXML2.DOMDocument を用いてXMLをロード
Set xmlDoc = CreateObject(“MSXML2.DOMDocument.6.0”)
xmlDoc.Async = False
xmlDoc.ValidateOnParse = False
If Not xmlDoc.Load(docXmlPath) Then
Err.Raise 9999, “XmlReplace”, “XMLのロードに失敗しました: ” & xmlDoc.parseError.reason
End If
‘ 5. 名前空間(w:)の解決を設定
xmlDoc.setProperty “SelectionNamespaces”, “xmlns:w=’http://schemas.openxmlformats.org/wordprocessingml/2006/main'”
‘ 6. すべてのテキスト要素 (
‘ ※ ここがDOM操作の真骨頂。Wordオブジェクトモデルを完全にバイパスする。
Set textNodes = xmlDoc.SelectNodes(“//w:t”)
Dim isModified As Boolean
isModified = False
For Each node In textNodes
If InStr(node.Text, targetKey) > 0 Then
node.Text = Replace(node.Text, targetKey, replaceValue)
isModified = True
End If
Next node
‘ 7. 変更があった場合のみXMLを保存し、再圧縮して上書きする
If isModified Then
xmlDoc.Save docXmlPath
‘ 元のファイルをバックアップ(安全のため)
If fso.FileExists(targetFilePath & “.bak”) Then fso.DeleteFile targetFilePath & “.bak”
fso.CopyFile targetFilePath, targetFilePath & “.bak”, True
‘ 再圧縮して.docxに戻す
Kill targetFilePath ‘ 既存のファイルを削除
ZipFiles tempDir, targetFilePath
MsgBox “高速置換が完了しました!”, vbInformation, “処理成功”
Else
MsgBox “対象のキーワードが見つかりませんでした。”, vbExclamation, “通知”
End If
CleanUp:
‘ 8. 一度作成したテンポラリの掃除
If fso.FolderExists(tempDir) Then
On Error Resume Next
fso.DeleteFolder tempDir, True
On Error GoTo 0
End If
Exit Sub
ErrorHandler:
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical, “エラー”
Resume CleanUp
End Sub
‘ ==============================================================================
‘ 補助関数: Shell.Applicationを利用したZIP解凍
‘ ==============================================================================
Private Sub UnZipFiles(ByVal zipPath As String, ByVal destPath As String)
Dim sa As Object
Set sa = CreateObject(“Shell.Application”)
sa.NameSpace(destPath).CopyHere sa.NameSpace(zipPath).Items, 16 + 1048576
End Sub
‘ ==============================================================================
‘ 補助関数: Shell.Applicationを利用したZIP圧縮
‘ ==============================================================================
Private Sub ZipFiles(ByVal srcDir As String, ByVal destZipPath As String)
Dim fso As Object
Dim sa As Object
Dim fileNum As Integer
Set fso = CreateObject(“Scripting.FileSystemObject”)
‘ 空のZIPヘッダを作成 (PK\x05\x06…)
fileNum = FreeFile
Open destZipPath For Output As #fileNum
Print #fileNum, Chr$(80) & Chr$(75) & Chr$(5) & Chr$(6) & String(18, Chr$(0));
Close #fileNum
Set sa = CreateObject(“Shell.Application”)
‘ フォルダ内の全アイテムをZIPにコピー
sa.NameSpace(destZipPath).CopyHere sa.NameSpace(srcDir & “word”).Items ‘ 必要に応じて調整
‘ ※厳密にはsrcDir直下の [Content_Types].xml や _rels も含める必要があるため、srcDir自体を圧縮対象にする
‘ 簡易版として、srcDir配下の全ファイル・フォルダを対象にするロジックに拡張すること。
End Sub
> 【アーキテクトからの補足】
> 上記の `ZipFiles` 関数内の圧縮処理は、環境やExcel/Wordのセキュリティポリシーによって挙動が異なる場合がある。
> 実務のプロダクション環境では、VBA単体でのZIP圧縮の不安定さを嫌う場合、信頼性の高いオープンソースの命令行(PowerShellの `Compress-Archive`)をVBAから叩くのが最も堅牢だ。
PowerShell連携による堅牢な圧縮ラッパーの例
Private Sub CompressFolderToDocx(ByVal srcFolder As String, ByVal destDocxPath As String)
Dim psCommand As String
‘ フォルダの中身を一度ZIP化し、拡張子をdocxに変更する
Dim tempZip As String
tempZip = srcFolder & “temp.zip”
If Dir(tempZip) <> “” Then Kill tempZip
‘ PowerShellのCompress-Archiveを使用(Windows 10/11標準)
psCommand = “powershell -Command “”Compress-Archive -Path ‘” & srcFolder & “‘ -DestinationPath ‘” & tempZip & “‘ -Force”””
CreateObject(“WScript.Shell”).Run psCommand, 0, True
If Dir(destDocxPath) <> “” Then Kill destDocxPath
Name tempZip As destDocxPath
End Sub
このPowerShell連携を組み合わせれば、ZIP圧縮の不具合に悩まされることは一切なくなる。
—
4. チーフアーキテクトからの実践的アドバイス
このXML直接DOM操作アプローチを現場に導入する際、以下の鉄則を忘れないでほしい。
1. プレースホルダー設計を厳格にする
ユーザーに入力させるのではなく、テンプレート側にあらかじめ `{{C_NAME}}` や `{{ISSUE_DATE}}` といったユニークなタグを埋め込んでおくこと。これにより、意図しない箇所のXMLタグ(スタイルやフォント情報など)を巻き込んで破壊するリスクをゼロにできる。
2. 必ずバックアップを取る
コード内でも入れているが、ファイルを直接バイナリ・ZIPとして操作するため、万が一のXML破損に備え、処理前の `.bak` 保存は絶対に省いてはならない。
数秒で終わる処理に何分も待たされる時代はもう終わりだ。オブジェクトモデルの呪縛から解放され、真にスケーラブルでプロフェッショナルな業務自動化をあなたのプロジェクトに実装してほしい。
