攝影或3C

python-docx 實務攻略: 在指定標題段落後自動插入圖片、並讓圖片不進目錄 ; 在標題段落之後插入新段落→新增 run→放入圖片,並設定 新段落.style 為 Normal 樣式避免納入目錄

摘要

  • 目標:用 Python 在 Word 文件中,找到指定的標題,於其後插入圖片,同時確保圖片不會被自動目錄(TOC, Table of Conents)收錄。
  • 重點:不依賴非標準 API;以底層 XML 安全插入段落;圖片置於 Normal 段落;可搭配 TOC 驗證。

你會學到

  • 如何在不破壞文件結構的前提下,在「指定標題之後」插入新段落與圖片
  • 為何圖片不會進 TOC,以及如何建立或更新 TOC 驗證
  • 常見坑位與穩健的解法

一、為什麼圖片會「進」或「不進」目錄

  • Word 的目錄是依「大綱層級」或「Heading 樣式」來收錄項目。
  • 若圖片跟標題在同一個段落(或圖片所在段落被套用 Heading 樣式),更新 TOC 時可能被收錄。
  • 解法:把圖片放在標題「下一個段落」且設定為「Normal」或無大綱層級的樣式。

二、核心做法概覽

  • 步驟
    1. 尋找目標標題段落(文字比對,支援第幾個同名標題)
    2. 在其後插入一個全新的段落(底層 XML:w:p)
    3. 在新段落加入 run 並插入圖片,控制寬度與對齊
    4. 以 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),這通常代表插到倒數第二位置,反而錯位,甚至在某些情況拋錯或行為不可預期。
  • 插入多個空白段或控制間距:
    • 連續呼叫 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

儲蓄保險王

儲蓄險是板主最喜愛的儲蓄工具,最喜愛的投資理財工具則是ETF,最喜愛的省錢工具則是信用卡