マトリョーシカを解き明かせ:Word VBAで「入れ子になった表」を再帰的に走査する極意
こんにちは。現場の荒波を乗り越えてきた皆さんの、頼れるエンジニアの先輩です。
「マクロの記録」から一歩踏み出し、自分でコードを書き始めたあなた。今、こんな壁にぶつかっていませんか?
「文書の中に表があるのはわかった。でも、表の中にさらに表が入っている(ネストされている)場合、どうやって全てのデータを取り出せばいいんだ……?」
これは、自動化エンジニアが必ず直面する「階層構造(ヒエラルキー)」という壁です。マクロの記録では、表面的な「表の1行目、2行目……」という操作しか記録できません。しかし、本物のプロフェッショナルは、構造がどれほど複雑になっても、一撃で、漏れなく、美しくデータを抽出します。
今日は、そのための最強の武器「再帰呼び出し(Recursion)」を伝授します。ここをマスターすれば、あなたのWord VBAスキルは「記録レベル」から「設計レベル」へと昇華するでしょう。
1. なぜ「普通のループ」では足りないのか?
通常、表のデータを処理するときは、以下のようなコードを書きますよね。
' 初級レベルの書き方
For Each tbl In ActiveDocument.Tables
' 表の処理...
Next tbl
このコードは、文書の「一番上の階層」にある表しか見ていません。
もし、あるセルのなかに別の表が隠れていたら? その中のセルに、さらに表があったら……?
この構造は、まるでロシアの人形「マトリョーシカ」です。
外側の表を開けると、中に小さな表がある。その中の表を開けると、さらに小さな表がある。
「表の中の表」の深さがいくつあるか事前に分からないとき、`For` ループを何重にも書くことは不可能です。ここで登場するのが、「自分自身を呼び出す」という魔法、再帰(さいき)なのです。
2. アルゴリズムの設計図:再帰的走査のロジック
再帰処理の考え方は非常にシンプルです。エンジニアとしての思考を、以下のステップに分解しましょう。
1. 現在の表を処理する(テキストを読み取る、など)。
2. 現在の表のすべてのセルを一つずつ見ていく。
3. もし、セルの中に「新しい表」を見つけたら……
4. 「その新しい表」を対象にして、手順1に戻る(自分自身を呼び出す)。
この「自分自身を呼び出す」という動作が、深さ(階層)がどれほどあっても、一番奥の表にたどり着くまで繰り返されます。
3. 実戦コード:ネストされた表を全走査するマスターピース
それでは、実際に動くコードを見てみましょう。単なるコードの羅列ではなく、プロの現場で通用する「堅牢な構造」を持たせています。
Option Explicit
''' <summary>
''' メインルーチン:文書内のすべての表を探索開始する
''' </summary>
Sub StartRecursiveTableTraversal()
Dim tbl As Table
Dim depth As Integer
Debug.Print "--- 走査開始 ---"
' 文書内のトップレベルの表をすべてループ
For Each tbl In ActiveDocument.Tables
' 再帰関数の呼び出し(初期深度は0)
TraverseTable tbl, 0
Next tbl
Debug.Print "--- 走査終了 ---"
MsgBox "走査が完了しました。イミディエイトウィンドウを確認してください。"
End Sub
''' <summary>
''' 再帰関数:表を深掘りしながら走査する
''' </summary>
''' <param name="targetTable">現在処理中の表オブジェクト</param>
''' <param name="depth">現在の階層の深さ(インデント用)</param>
Sub TraverseTable(targetTable As Table, depth As Integer)
Dim rowIdx As Integer
Dim colIdx As Integer
Dim cell As Cell
Dim indent As String
Dim i As Integer
' 見栄えのために階層に応じたインデントを作成
indent = ""
For i = 1 To depth
indent = indent & " " ' 深くなるほどスペースを増やす
Next i
' 1. 現在の表の行と列の数を取得
' ※表が壊れている場合を考慮し、エラーハンドリングを検討するのがプロ
On Error Resume Next
For rowIdx = 1 To targetTable.Rows.Count
For colIdx = 1 To targetTable.Columns.Count
' セルを取得(結合セルがある場合はエラーになる可能性があるため注意)
Set cell = targetTable.Cell(rowIdx, colIdx)
If Not cell Is Nothing Then
' セル内のテキストを取得(末尾の制御文字を除去)
' Wordのセル末尾には Chr(13) & Chr(7) という特殊な記号が入るため、これを除去するのが鉄則
Dim cellText As String
cellText = CleanCellText(cell.Range.Text)
' 結果を表示(イミディエイトウィンドウ)
Debug.Print indent & "Table(Depth:" & depth & ") Row:" & rowIdx & " Col:" & colIdx & " -> [" & cellText & "]"
' 2. 【ここが核心】セルの中に「表」が含まれているかチェック
If cell.Tables.Count > 0 Then
' もし表があれば、その表に対して自分自身(TraverseTable)を呼び出す!
' 階層を一つ深くして(depth + 1)再帰
Dim nestedTable As Table
Set nestedTable = cell.Tables(1)
TraverseTable nestedTable, depth + 1
End If
End If
Set cell = Nothing
Next colIdx
Next rowIdx
On Error GoTo 0
End Sub
''' <summary>
''' セル内の不要な制御文字(改行・区切り文字)を掃除する
''' </summary>
Function CleanCellText(rawText As String) As String
Dim clean As String
clean = rawText
' Wordのセル特有の「End of Cell Marker」を削除
clean = Replace(clean, Chr(13) & Chr(7), "")
' 前後の空白をトリミング
CleanCellText = Trim(clean)
End Function
4. 伝説のエンジニアが教える「陥りやすい3つの罠」
このコードを動かす際、あるいは自分で改良する際に、必ず意識してほしいポイントがあります。ここを知っているかどうかが、初心者とプロの分かれ目です。
① 「セルの末尾記号」の正体
Wordのセル内のテキストを取得すると、必ず末尾に `Chr(13) & Chr(7)` という、目に見えない制御文字が付着します。これを知らずに `If cellText = “完了”` と比較すると、「見た目は完了なのに、なぜか一致しない!」という地獄に陥ります。必ず `CleanCellText` のような掃除用関数を通す習慣をつけてください。
② 結合セルという名の地雷
`targetTable.Cell(rowIdx, colIdx)` を使う際、表の中で「セルの結合」が行われていると、VBAは「その座標にセルが存在しない」と判断してエラーを吐きます。
実務レベルでは、`For Each cell In targetTable.Range.Cells` のように、座標ではなく「セルオブジェクトそのもの」をループさせる方が、結合セルに対して強固(ロバスト)なコードになります。
③ パフォーマンスと `Selection` の回避
初心者はよく `cell.Select` を使って、画面上の選択範囲を動かしながら処理しようとします。しかし、これは絶対に避けてください。
画面の更新(描画)は、VBAにおいて最も重い処理の一つです。今回のように `Range` オブジェクトや `Table` オブジェクトを直接操作する手法こそが、高速かつ安定した自動化の鍵です。
5. まとめ:構造を制する者は、Wordを制す
再帰処理は、最初は少し頭がこんがらがるかもしれません。でも、一度「自分自身を呼び出す」という感覚を掴んでしまえば、複雑な文書構造はもはや恐れるに足りないものになります。
- 階層構造には「再帰」を使う。
- セルの制御文字は必ず掃除する。
- 結合セルやパフォーマンスに気を配る。
これらをクリアすれば、あなたの書くコードは「動くだけのマクロ」から「信頼されるシステム」へと進化します。
さあ、勇気を持って、このコードを自分のプロジェクトに組み込んでみてください。もしエラーが出ても大丈夫。それは、あなたが新しい知識を手に入れるための貴重なサインなのですから。
応援していますよ。頑張ってくださいね!
