Word の目次は静的なテキストではなく、フィールド(Field)である。この違いが、手動編集時に観察されるすべての挙動を説明する。目次を右クリックで更新できる、見出しの変化に追従して自動的に再描画される、ページ番号が常に実際の位置と一致する——これらは、フィールドが開封時や描画時に再評価されるためだ。コードで目次を生成する場合、この機構は一つの直接的な帰結をもたらす。フィールドを挿入した後、更新メソッドを明示的に一度呼び出さなければ、保存された文書の目次は空である。
本記事では Spire.Doc for Python を用いて、目次生成の完全な流れを示す。挿入と更新、階層範囲の制御、フィールドスイッチのカスタマイズ、エントリスタイルの変更、コンテンツ変更後の再更新と目次削除。
pip install spire-doc
デモ入力はシステム設計書で、3 個のレベル 1 見出し、4 個のレベル 2 見出し、1 個のレベル 3 見出し、計 8 個の見出し段落を含み、本文は技術方案の記述である。サンプル文書とスクリーンショットは英語コンテンツを使用し、中日両記事で同一素材を共用する。
フィールドと UpdateTableOfContents
AppendTOC(1, 3) は段落に TOC フィールドを挿入する。引数の 1 と 3 は収録する見出し階層の範囲を指定する。この手順はフィールドの構造を書き込むだけで、目視可能なエントリは生成しない。Document.UpdateTableOfContents() が全文の見出しを走査し、ページ番号を計算し、フィールドの内容を埋める——二つの呼び出しは不可分である。
from spire.doc import *
from spire.doc.common import *
document = Document()
section = document.AddSection()
section.PageSetup.PageSize = PageSize.A4()
# 目次フィールドを挿入
toc_paragraph = section.AddParagraph()
toc_paragraph.AppendTOC(1, 3)
# 改ページ後に見出しコンテンツを追加
page_break = section.AddParagraph()
page_break.AppendBreak(BreakType.PageBreak)
# 見出し段落を追加(例)
heading = section.AddParagraph()
heading.AppendText("1. System Architecture")
heading.ApplyStyle(BuiltinStyle.Heading1)
section.AddParagraph().AppendText("The system adopts a layered architecture...")
# ... 追加の見出し ...
# 目次の内容を埋める——必須
document.UpdateTableOfContents()
document.SaveToFile("toc.docx", FileFormat.Docx2016)
UpdateTableOfContents() を省略した場合、保存された文書の目次段落にはプレースホルダテキスト"TOC"のみが含まれ、エントリ数は 0 になる。Word でこの文書を開くと、ユーザーが右クリックから「フィールドの更新」を手動で実行するまで目次の内容は表示されない——自動化フローでは受け入れがたい。
更新を呼び出した後、目次には 8 個のエントリが生成され、それぞれ TOC1、TOC2、TOC3 の三つの段落スタイルが適用され、見出し階層と一対一に対応する。各エントリのテキスト構造は「見出しテキスト + Tab + ページ番号」で、例えば"1. System Architecture\t2"となる。全エントリのページ番号が 2 と表示されるのは、改ページの後のコンテンツが第 2 ページから始まるためである。
階層範囲と Title スタイル
AppendTOC の二つの引数は収録する見出し階層を制御する。AppendTOC(1, 3) はレベル 1 から 3 を収録し、AppendTOC(1, 2) はレベル 1 と 2 のみを収録し、レベル 3 の見出しは目次に現れない。同じ 8 見出しの文書で AppendTOC(1, 2) を使うと、エントリ数は 7 に減る——1 個のレベル 3 見出しが除外される。技術文書ではレベル 3 の見出しは細かすぎることが多く(例「1.1.1 Container Orchestration」)、レベル 2 までに絞ると目次がすっきりする。正式な報告書では通常 1-3 階層、操作マニュアルでは 1-2 階層を採用する。
見落としやすい点として、BuiltinStyle.Title で設定したタイトル段落(目次の上の"目 次"など)が UpdateTableOfContents によって TOC1 エントリとして収録されることがある。Word の Title スタイルは内部でアウトラインレベル 1 にマッピングされており、Heading1 と同階層だからだ。目次の上に Title 段落があると、生成される目次に第 1 ページを指す余分なエントリが一つ増える。回避するには、タイトルを目次フィールドの後、改ページの前に配置するか、Title スタイルではなく通常段落に手動でフォーマットを設定する。
フィールドスイッチ:\o と \n
AppendTOC メソッドはデフォルトのフィールドスイッチ組合せをカプセル化する。より細かい制御が必要な場合は、TableOfContent クラスでフィールドスイッチ文字列を直接構築し、doc.TOC プロパティに登録する。
document = Document()
section = document.AddSection()
# \o "1-3" レベル 1-3 の見出しを収録
# \n "1-1" レベル 1 のエントリにページ番号を表示しない
toc = TableOfContent(document, r'{\o "1-3" \n "1-1"}')
para = section.AddParagraph()
para.Items.Add(toc)
para.AppendFieldMark(FieldMarkType.FieldSeparator)
para.AppendText("TOC")
para.AppendFieldMark(FieldMarkType.FieldEnd)
document.TOC = toc
# ... 見出しコンテンツを追加 ...
document.UpdateTableOfContents()
\n "1-1" の効果は出力から直接検証できる。レベル 1 のエントリ"1. System Architecture""2. API Design""3. Deployment"の後に Tab とページ番号がなく、見出しテキストのみが表示される。レベル 2 と 3 のエントリは通常通りページ番号を表示する。この書式は章レベルの見出しが複数ページにまたがる場合に適している——"1. System Architecture"が第 2 ページから第 4 ページまで続く場合、固定のページ番号を表示するより表示しない方が読者を誤導しない。
TableOfContent を構築した後、必ず document.TOC に代入する。そうしないと UpdateTableOfContents がカスタムフィールドオブジェクトを見つけられず、更新時に null 参照例外を投げる。AppendFieldMark と AppendText("TOC") はフィールドのマークアップ構造を構成する。FieldSeparator はフィールドの命令と結果を区切り、FieldEnd はフィールドの終端を示す——これらは AppendTOC のカプセル化メソッドでは自動処理されるが、手動構築時には省略できない。
エントリスタイル
目次エントリには TOC1、TOC2、TOC3 スタイルが使われる。これらは Word の組み込み段落スタイルで、UpdateTableOfContents の呼び出し後に対応する階層のエントリに自動的に適用される。スタイルの変更には二つの経路がある。
一つ目はカスタムスタイルの作成と適用である。Style.CreateBuiltinStyle は組み込みスタイルをテンプレートに新しいスタイルを作成する。フォント、サイズ、色を変更した後、文書のスタイルテーブルに追加し、目次段落を StyleName で照合して ApplyStyle する。
toc_style = Style.CreateBuiltinStyle(BuiltinStyle.Toc1, document)
toc_style.CharacterFormat.FontName = "Calibri"
toc_style.CharacterFormat.FontSize = 14.0
toc_style.CharacterFormat.TextColor = Color.get_CadetBlue()
document.Styles.Add(toc_style)
for i in range(document.Sections.Count):
section = document.Sections[i]
for j in range(section.Body.ChildObjects.Count):
obj = section.Body.ChildObjects[j]
if isinstance(obj, StructureDocumentTag):
for k in range(obj.ChildObjects.Count):
child = obj.ChildObjects[k]
if isinstance(child, Paragraph) and child.StyleName == "TOC1":
child.ApplyStyle(toc_style.Name)
目次エントリの段落は StructureDocumentTag に包まれており、走査時にはまずこのコンテナを見つけてからその子オブジェクトに入る必要がある。section.Paragraphs を直接走査するとエントリが漏れる。これは AppendTOC のカプセル化メソッドの内部構造による。
二つ目の経路はより直接的である。更新完了後に段落を走査し、StyleName が "TOC" で始まる段落について、ChildObjects 内の TextRange の CharacterFormat を個別に変更する——フォント、サイズ、色ともエントリごとに設定できる。この方式は新しいスタイルを作成せず、変更は現在の文書のこれらの段落にのみ作用する。二つの方式は組み合わせ可能である。スタイル API でフォントと色を統一し、直接変更で個別のエントリを微調整する。
コンテンツ変更後の更新と目次削除
文書のコンテンツは一度で完成するものではない。見出しの追加・削除後に目次を再生成するには、UpdateTableOfContents を再度呼び出すだけでよい。
document.LoadFromFile("toc.docx")
section = document.Sections[0]
# 文書末尾に新しいレベル 1 見出しを追加
new_heading = section.AddParagraph()
new_heading.AppendText("4. Security & Compliance")
new_heading.ApplyStyle(BuiltinStyle.Heading1)
section.AddParagraph().AppendText("The system complies with Level 3 security requirements, using TLS 1.3 for all sensitive data transmission.")
# 目次を再更新
document.UpdateTableOfContents()
document.SaveToFile("toc_updated.docx", FileFormat.Docx2016)
実測では、更新前の目次は 8 エントリで、レベル 1 見出しを一つ追加してから UpdateTableOfContents を再呼び出すと、エントリ数は 9 になり、新しいエントリ"4. Security & Compliance"が目次の末尾に現れる。旧い目次を削除してから再挿入する必要はない——フィールドの構造は変わっておらず、更新メソッドが見出しを再走査してフィールドの内容を上書きする。
目次の削除は逆方向の操作である。目次エントリの StyleName は "TOC" で始まる(TOC1、TOC2、TOC3)。正規表現で照合し、段落を順次削除する。
import re
body = document.Sections[0].Body
i = 0
while i < body.Paragraphs.Count:
if re.match(r"TOC\w+", body.Paragraphs[i].StyleName):
body.Paragraphs.RemoveAt(i)
i -= 1
i += 1
実測では 8 個の TOC 段落がすべて削除され、エントリ数はゼロになった。削除時にインデックスを戻す(i -= 1)必要がある。RemoveAt の後、後続の段落が前に詰まるため、戻さないと隣接する TOC 段落を飛ばす。
適用シーンと組み込み方法
目次生成は文書生産フローの末端に配置するのが適切である。上流のシステムが本文と見出しを生成した後、TOC フィールドを挿入して更新メソッドを呼び出し、保存すれば目次付きの最終稿が得られる。文書がその後も編集される場合——人がコンテンツを補完する、別のスクリプトが章を追加する——フィールドを再挿入する必要はない。フィールドの構造が残っていれば、UpdateTableOfContents を再度呼び出すだけで内容が更新される。
階層範囲とフィールドスイッチの選択は文書の種類による。技術文書では通常レベル 3 まで、操作マニュアルではレベル 2 まで、契約類の文書ではレベル 1 のみ。\n スイッチは章レベルの見出しが複数ページにまたがる場合に有用で、正式な報告書ではレベル 1 の見出しが数ページに続くことが多く、固定のページ番号を表示するより表示しない方がよい。
エントリスタイルのカスタマイズは、段落ごとの直接変更よりもスタイル API を使うことを推奨する。作成したスタイルは同バッチの他の文書に再利用できるが、段落ごとの変更は一回限りである。複数の文書で目次の外観を統一する場合は、スタイル定義を設定ファイルに抽出し、各文書の読み込み後に同じスタイルセットを作成して適用する。
本記事の全操作と出力は Spire.Doc for Python 環境で実測した。Free Spire.Doc for Python は目次フィールドの挿入と更新をサポートするが、単一文書は 10 ページまでで、長文書には商用版が必要である。
