【Word VBA極限講座】数百MB級の大規模文書を完全掌握する:分割・結合・ページ番号連続性のアーキテクチャ
Word VBAにおける真の地獄を見たことがあるだろうか。
数百ページに及び、数千個の図表と複雑なフィールドコードが埋め込まれた数百MB級のドキュメント。これを無計画なコードで処理すれば、Wordは瞬発的にメモリリークを起こし、`COMException`を吐き、あるいは「メモリ不足です」という冷酷なダイアログとともに沈黙する。
本稿では、レガシーかつ強大なWordオブジェクトモデルの限界を突破し、大規模文書の「安全な分割」と「ページ番号・目次の完全性を保った結合」をなし得る、チーフアーキテクトとしての極限の知見を授ける。
—
1. Wordオブジェクトモデルの裏側:なぜ巨大文書の処理は破綻するのか
多くのプログラマが犯す最大の過ちは、`Selection`オブジェクトの乱用と、オブジェクトの解放(ライフサイクル管理)の軽視だ。
UIと密結合した `Selection` は、画面の描画更新(ScreenUpdating)を伴うため極めて低速である。さらに、Word VBAの背後にあるCOMコンポーネントは、参照カウント方式でメモリ管理を行っている。VBAの自動ガベージコレクションはアテにならない。`.Range` をチェーンで繋ぎまくったコードは、確実にメモリリークを引き起こす。
鉄則:Document, Range, Collapse の三位一体
巨大文書を扱う際の基本方針は以下の3点に集約される。
1. `Selection`を一切使わず、`Range`オブジェクトのみで完結させる。
2. 画面描画とバックグラウンドの警告を完全にシャットアウトする。
3. ループ内で生成したオブジェクトは、必ず明示的に `Nothing` を代入して解放する。
—
2. 大規模文書の「安全な分割」アーキテクチャ
文書を章(Heading 1など)ごとに分割し、個別ファイルとして切り出す処理を実装する。ここで重要なのは、単にテキストを切り出すだけでなく、元の文書構造やセクションプロパティをどう維持するかである。
以下のコードは、見出し1を基準に文書を無駄なメモリ消費なしで切り出し、個別ファイルとして保存するチーフアーキテクトレベルのルーチンだ。
Option Explicit
Public Sub SplitDocumentByHeading1()
‘ ——————————–ើញ:大規模文書の安全な分割ルーチン
‘ 実行前の環境退避とメモリ最適化を伴う実装
‘ —————————————————————-
Dim tStart As Single
tStart = Timer
‘ 1. パフォーマンス・安定性のための環境設定
Call ToggleEnvironment(False)
On Error GoTo ErrorHandler
Dim srcDoc As Document
Set srcDoc = ActiveDocument
Dim rngTarget As Range
Dim rngNext As Range
Dim chapterIndex As Long
chapterIndex = 1
Dim headingFound As Boolean
‘ 検索オブジェクトの構築(段落単位での走査)
Dim rngSearch As Range
Set rngSearch = srcDoc.Content
With rngSearch.Find
.ClearFormatting
.Style = srcDoc.Styles(wdStyleHeading1)
.Forward = True
.Wrap = wdFindStop
.Format = True
Do While .Execute
‘ ヒットした見出しの範囲を特定
Set rngTarget = rngSearch.Duplicate
‘ 次の見出し、または文書末尾までの範囲を取得
‘ (※実際のコードでは次の見出し位置を取得するロジックをここに挟む)
‘ 別名で保存する処理へ
Call ExportChapter(srcDoc, rngTarget, chapterIndex)
chapterIndex = chapterIndex + 1
rngSearch.Collapse wdCollapseEnd
Loop
End With
MsgBox “分割完了: 処理時間 ” & Format(Timer – tStart, “0.00”) & ” 秒”, vbInformation
CleanUp:
‘ 2. 環境の復元とメモリ解放
Call ToggleEnvironment(True)
Set rngSearch = Nothing
Set rngTarget = Nothing
Set srcDoc = Nothing
Exit Sub
ErrorHandler:
MsgBox “致命的なエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
Private Sub ExportChapter(ByRef parentDoc As Document, ByRef targetRange As Range, ByVal index As Long)
Dim newDoc As Document
Set newDoc = Documents.Add(Visible:=False)
‘ 範囲を新規ドキュメントへ転送(書式・オブジェクトを維持)
targetRange.Copy
newDoc.Content.PasteAndFormat wdFormatOriginalFormatting
‘ 保存パスの構築
Dim savePath As String
savePath = parentDoc.Path & “\Chapter_” & Format(index, “00”) & “.docx”
newDoc.SaveAs2 FileName:=savePath, FileFormat:=wdFormatDocumentDefault
newDoc.Close SaveChanges:=wdDoNotSaveChanges
Set newDoc = Nothing
End Sub
Private Sub ToggleEnvironment(ByVal state As Boolean)
With Application
.ScreenUpdating = state
.DisplayAlerts = wdAlertsNone ‘ アートワークや上書き確認の抑制
.Calculation = IIf(state, wdCalculationAutomatic, wdCalculationManual)
End With
If state Then
Application.DisplayAlerts = wdAlertsAll
End If
End Sub
—
3. 結合の難所:ページ番号の連続性と目次の再構築
分割したファイルを再び1つに結合する際、エンジニアが直面する最大の壁が「ページ番号の不連続性」と「セクション区切りの破綻」である。
Wordでは、セクションごとに「前のセクションに続ける(`wdPageNumberRestartContinue`)」か「1から開始する(`wdPageNumberRestartPage`)」が保持される。単にファイルを `InsertFile` で結合していくと、各章の先頭でページ番号が「1」にリセットされる悲劇が起きる。
これを制御し、さらに結合後に目次(TOC)を一括更新するコードが以下だ。
Public Sub MergeDocumentsWithPageContinuity()
Dim tStart As Single
tStart = Timer
Call ToggleEnvironment(False)
On Error GoTo ErrorHandler
Dim masterDoc As Document
Set masterDoc = Documents.Add(Visible:=False)
‘ 分割されたファイルのパスリスト(実際にはFileSystemObject等で動的に取得)
Dim filePaths(1 To 3) As String
filePaths(1) = “C:\Data\Chapter_01.docx”
filePaths(2) = “C:\Data\Chapter_02.docx”
filePaths(3) = “C:\Data\Chapter_03.docx”
Dim i As Long
Dim targetRange As Range
For i = LBound(filePaths) To UBound(filePaths)
Set targetRange = masterDoc.Content
targetRange.Collapse wdCollapseEnd
If i > 1 Then
‘ セクション区切りを挿入して結合
targetRange.InsertBreak wdSectionBreakNextPage
Set targetRange = masterDoc.Content
targetRange.Collapse wdCollapseEnd
End If
‘ ファイルの挿入
targetRange.InsertFile FileName:=filePaths(i), ConfirmConversions:=False, Link:=False, Attachment:=False
‘ 結合したセクションのページ番号を「前のセクションから継続」に強制設定
Dim currentSec As Section
Set currentSec = masterDoc.Sections(masterDoc.Sections.Count)
currentSec.Headers(wdHeaderFooterPrimary).PageNumbers.RestartNumberingAtSection = False
Set targetRange = Nothing
Set currentSec = Nothing
Next i
‘ 目次およびフィールドコードの強制再構築
Call RefreshAllFields(masterDoc)
masterDoc.SaveAs2 FileName:=”C:\Data\Master_Final.docx”, FileFormat:=wdFormatDocumentDefault
masterDoc.Close SaveChanges:=wdDoNotSaveChanges
MsgBox “結合およびページ番号の連続性確保が完了しました。”, vbInformation
CleanUp:
Call ToggleEnvironment(True)
Set masterDoc = Nothing
Exit Sub
ErrorHandler:
MsgBox “結合プロセスでエラー: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
Private Sub RefreshAllFields(ByRef doc As Document)
Dim sto As Range
For Each sto In doc.StoryRanges
Dim lngStrs As Long
lngStrs = sto.StoryType
Do
‘ フィールドの更新
Dim fld As Field
For Each fld in sto.Fields
fld.Update
Next fld
Set sto = sto.NextStoryRange
Loop Until sto Is Nothing
Next sto
End Sub
—
4. チーフアーキテクトの視座:Windows APIによるメモリ防衛とシステム間連携
ここまでの実装でVBAレベルの最適化は完了しているが、真にエンタープライズなシステム(RPAや外部C#プロセスからの呼び出しなど)では、WordのCOMプロセス自体がゾンビ化するリスクヘッジが必要となる。
ガベージコレクションの明示的強制
VBAには明示的な `GC.Collect()` が存在しない。そのため、オブジェクトの参照を断ち切った後、以下のWindows APIを使用して、未使用となったCOMプロセスのメモリフットプリントをOSレベルで強制的に回収させるアプローチが極限環境では有効となる。
‘ 32bit/64bit両対応のAPI宣言
If VBA7 Then
Declare PtrSafe Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As LongPtr, _
ByVal dwMinimumWorkingSetSize As LongPtr, _
ByVal dwMaximumWorkingSetSize As LongPtr) As Long
Declare PtrSafe Function GetCurrentProcess Lib “kernel32” () As LongPtr
Else
Declare Function SetProcessWorkingSetSize Lib “kernel32” ( _
ByVal hProcess As Long, _
ByVal dwMinimumWorkingSetSize As Long, _
ByVal dwMaximumWorkingSetSize As Long) As Long
Declare Function GetCurrentProcess Lib “kernel32” () As Long
End If
Public Sub FlushMemory()
‘ Wordが抱え込んだ不要なワーキングセットをOSに返却する
Dim hProc As LongPtr
hProc = GetCurrentProcess()
Call SetProcessWorkingSetSize(hProc, -1, -1)
End Sub
この `FlushMemory` メソッドを、大規模ファイルの分割ループの「数ファイルごと」に挟み込むことで、数千ページ規模のバッチ処理であっても、メモリ消費量を一定に保ったまま安定稼働させることが可能となる。
—
5. 結び:技術に妥協するな
Word VBAは「古い言語」ではない。オブジェクトモデルの挙動、COMのライフサイクル、そして背後にあるOSのリソース管理機構を正しく理解していれば、C#やPython製のモダンなライブラリ(OpenXML等)では実装コストが高すぎる複雑なレイアウト制御や変更履歴の保持を、圧倒的な速度で自動化できる強力な武器となる。
「動けばいい」という甘えを捨て、メモリの1バイト、オブジェクトの1ライフサイクルにまで目を光らせる者だけが、巨大文書を真に掌握することができるのだ。
