摘要
- 目標:用 Python 在 Word 文件中,找到指定的標題,於其後插入圖片,同時確保圖片不會被自動目錄(TOC, Table of Conents)收錄。
- 重點:不依賴非標準 API;以底層 XML 安全插入段落;圖片置於 Normal 段落;可搭配 TOC 驗證。
你會學到
- 如何在不破壞文件結構的前提下,在「指定標題之後」插入新段落與圖片
- 為何圖片不會進 TOC,以及如何建立或更新 TOC 驗證
- 常見坑位與穩健的解法
一、為什麼圖片會「進」或「不進」目錄
- Word 的目錄是依「大綱層級」或「Heading 樣式」來收錄項目。
- 若圖片跟標題在同一個段落(或圖片所在段落被套用 Heading 樣式),更新 TOC 時可能被收錄。
- 解法:把圖片放在標題「下一個段落」且設定為「Normal」或無大綱層級的樣式。
二、核心做法概覽
- 步驟
- 尋找目標標題段落(文字比對,支援第幾個同名標題)
- 在其後插入一個全新的段落(底層 XML:w:p)
- 在新段落加入 run 並插入圖片,控制寬度與對齊
- 以 Normal 樣式確保不進 TOC
- 為何不使用 Paragraph.insert_paragraph_after
- 多數穩定版 python-docx 沒有該方法,直接用 OxmlElement(‘w:p’) 新建段落更通用。
三、完整範例程式(穩定相容)
- 需求:python-docx、你的 template.docx、sample.png
- 成果:在目標標題後插入圖片,輸出到新檔
# pip install python-docx
from docx import Document
from docx.shared import Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
"""
WD_ALIGN_PARAGRAPH 是舊名(較常見的別名)。
WD_PARAGRAPH_ALIGNMENT 是新名(語義更明確)。
兩者等價,枚舉成員相同;可以選其一即可,不要同時混用以免困惑。
"""
from docx.text.paragraph import Paragraph
from docx.oxml import OxmlElement
from copy import deepcopy
from pathlib import Path
TEMPLATE_PATH = Path("template.docx") # 來源範本
OUTPUT_PATH = Path("output_with_image.docx")
IMAGE_PATH = Path("sample.png")
target_heading_text = "PCBA (L6) Functional Test Plan"
target_index = 0
width_inches = 6.2
def insert_paragraph_after(paragraph) -> Paragraph:
# 在 paragraph 後插入新段落(通用、相容)
parent = paragraph._p.getparent() # w:body 或 w:tc
#docx.oxml.document.CT_Body
new_p = OxmlElement('w:p')
# 可選:複製段落屬性(對齊/縮排/間距),也可移除讓它完全走樣式預設
if paragraph._p.pPr is not None:
new_p.append(deepcopy(paragraph._p.pPr))
parent.insert(parent.index(paragraph._p) + 1, new_p)
return Paragraph(new_p, paragraph._parent)
def insert_image_after_heading(doc: Document, heading_text: str, image_path: Path,
target_index: int = 0, width_inches: float = 6.2,
picture_paragraph_style: str = "Normal",
align_center: bool = True) -> bool:
# 正規化比對,容錯空白與大小寫
norm_search = ' '.join(heading_text.lower().split())
counter = 0
for p in doc.paragraphs:
norm_para = ' '.join(p.text.lower().split())
if norm_search in norm_para:
if counter == target_index:
pic_para = insert_paragraph_after(p)
if picture_paragraph_style:
pic_para.style = picture_paragraph_style # 確保不是 Heading
run = pic_para.add_run()
run.add_picture(str(image_path), width=Inches(width_inches))
if align_center:
pic_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
return True
counter += 1
return False
# 執行
assert TEMPLATE_PATH.exists(), f"找不到範本: {TEMPLATE_PATH.resolve()}"
assert IMAGE_PATH.exists(), f"找不到圖片: {IMAGE_PATH.resolve()}"
doc = Document(TEMPLATE_PATH)
ok = insert_image_after_heading(doc, target_heading_text, IMAGE_PATH,
target_index=target_index, width_inches=width_inches,
picture_paragraph_style="Normal", align_center=True)
doc.save(OUTPUT_PATH)
print("插入結果:", "成功" if ok else "未找到指定標題")
print("輸出檔案:", OUTPUT_PATH.resolve())輸出:
def insert_paragraph_after(paragraph)
下面逐行解釋這個函式做了什麼、為什麼要這樣做,以及可能的變體與注意點。
函式目的
- 在指定的 paragraph 後面,插入一個「全新的段落」,並回傳這個新段落的 Paragraph 物件,方便你後續 add_run()、add_picture()、設定樣式或對齊。
逐行說明
- def insert_paragraph_after(paragraph) -> Paragraph:
- 宣告函式,參數 paragraph 是 python-docx 的 Paragraph 物件;回傳型別也是 Paragraph。
- parent = paragraph._p.getparent()
- paragraph._p 是底層的 XML 節點 w:p(CT_P)。
- getparent() 取得此段落所在的父節點,通常是:
- w:body:文件本文
- w:tc:表格儲存格內
- 我們需要父節點來把新段落插入到正確的位置。
- new_p = OxmlElement(‘w:p’)
- 以底層方式建立一個新的 w:p(WordprocessingML 的段落元素)。
- 之所以用 OxmlElement 而不是高階 API,是因為 python-docx 沒有「在任意段落後插入新段」的現成方法,這樣做通用、穩定。
- if paragraph._p.pPr is not None:
new_p.append(deepcopy(paragraph._p.pPr))- pPr 是段落屬性(paragraph properties),包含對齊、縮排、行距、段前/段後間距、邊框等。
- 若原段有 pPr,就用 deepcopy 複製一份放到新段,讓新段在版面屬性上與原段一致。
- 可移除此段,讓新段完全走樣式預設(例如 Normal),視需求而定。
- 用 deepcopy 而不是 clone,是為了相容不同版本的 lxml/python-docx,避免 AttributeError。
- parent.insert(parent.index(paragraph._p) + 1, new_p)
- 找出原段落在父節點中的索引位置,再在其「後一位」插入新段。
- 這一步確保新段出現在目標段落「正後方」,而不是文件尾或其他位置。
- return Paragraph(new_p, paragraph._parent)
- 把底層的 XML 節點 new_p 包裝成 python-docx 的高階 Paragraph 物件,並指定相同的 _parent(文件/段落集合的容器),以便後續用熟悉的 API 操作。
- 有了這個 Paragraph 物件,你就能做:
- p = insert_paragraph_after(some_heading)
- p.style = “Normal”
- run = p.add_run(); run.add_picture(…)
為什麼這樣寫是「通用、相容」的
- 不依賴不存在的高階方法(例如某些版本沒有 insert_paragraph_after)。
- 不使用不可用的 clone 方法,改用 deepcopy,跨版本較穩。
- 適用於文件本文與表格儲存格內的段落,因為父節點自適應 w:body 或 w:tc。
常見變體
- 不繼承段落屬性(讓新段用預設樣式):
- 移除 deepcopy 區塊;改用 p.style = “Normal” 或自訂樣式。
- 在目標段之前插入:
- parent.insert(parent.index(paragraph._p), new_p),
不需要減 1。因為 insert(pos, node) 會把新節點插入到 pos 位置,原本位於 pos 的節點會被往後推。 如果寫成 index(…) – 1,當目標本來就是第一個子節點時,會變成 insert(-1, new_p),這通常代表插到倒數第二位置,反而錯位,甚至在某些情況拋錯或行為不可預期。
- parent.insert(parent.index(paragraph._p), new_p),
- 插入多個空白段或控制間距:
- 連續呼叫 insert_paragraph_after,或對新段設置段前/段後間距。
注意事項
- paragraph._p 與 paragraph._parent 屬於「受保護」的內部屬性,雖然在實務上廣泛使用,但未來版本理論上可能變動;目前 python-docx 社群做法也是如此,穩定度足夠。
- 若文件含有節點像是段落內的欄位、書籤、內容控制(content control),這種插入方式仍可行,但要留意你插入的位置是否在預期的範圍(例如表格內/外)。
總結
這個函式用底層 XML 精準地把「新段落」插在「指定段落之後」,並回傳一個可用的 Paragraph 物件。這是用 python-docx 在任意位置插入內容的標準、穩健做法。
四、如何驗證圖片沒進目錄(TOC)
- 方式A:用已含 TOC 的範本
- 在 Word 先插入「自動目錄」,另存 template_with_toc.docx。
- 用上面程式改讀這個範本,插圖後開檔→右鍵目錄→更新欄位→更新整個目錄。圖片不會出現在目錄中。
- 方式B:用程式插入 TOC 欄位
- 插入欄位 { TOC \o “1-3” \h \z \u },開檔後一樣手動更新欄位即可顯示。
五、關鍵細節與最佳實務
- 指定第幾個同名標題:target_index
- 若文件內有多個相同標題名稱,這是必要的 disambiguation。
- 段落樣式控制
- pic_para.style = “Normal” 或你自訂、但大綱層級為「本文」的樣式。避免 Heading 1–9。
- 對齊與寬度
- 段落對齊使用 pic_para.alignment;圖片寬度用 Inches 控制。高度會等比例縮放。
- 路徑小技巧(Windows)
- IMAGE_PATH 建議用原始字串 r”D:\Temp\xxx.png” 或 Path 物件,避免跳脫字元問題。
- 文字比對策略
- 若標題含特殊字元、換行或域,建議改為「樣式先過濾 + 精確比對」:
- if p.style and p.style.name in {“Heading 1″,”Heading 2″,”Heading 3”} and p.text.strip() == heading_text.strip():
- 若標題含特殊字元、換行或域,建議改為「樣式先過濾 + 精確比對」:
- 表格中的標題
- 仍可行;插入的新段落會在同一儲存格內。若你希望圖片跑到表格外,需先定位表格後再在表格之後插入段落(屬進階操作)。
六、常見錯誤與修正
- AttributeError: Paragraph 沒有 insert_paragraph_after
- 以 OxmlElement(‘w:p’) + parent.insert(…) 解決。
- CT_PPr 沒有 clone 方法
- 用 deepcopy 複製 pPr,而非 clone。
- 圖片進了目錄
- 確認圖片所在段落不是 Heading 樣式;若仍進入,檢查該樣式的大綱層級是否被設為 1–9。
七、延伸應用
- 一次插入多張圖片:在同一個標題後重複 insert_paragraph_after,再 add_picture。
- 為圖片加標題(Caption):插圖後再插一段文字,樣式用「Caption」,並可加入交叉參照。
- 自動批次處理多個檔案:把 Document 開檔與儲存包成函式,遍歷資料夾。
結語
這個方法的核心是:以底層 XML 精準插入「新段落」,並將圖片置於非大綱層級的樣式。它避開了 python-docx 的版本差異與少數不穩定 API,實務上穩、可維護、且容易擴充到批次處理與加上 TOC 驗證。
推薦hahow線上學習python: https://igrape.net/30afN