在使用 python-docx 時,add_paragraph() 雖然方便,
但它只能把內容加在最後面。
如果你想插隊(插在最前面、插在表格後),
或是建立超連結,就必須學會操作底層 XML。
本教學包含 5 種核心技法,由淺入深,一次搞懂。
第一步:準備測試環境
首先,我們生成一個乾淨的 demo_origin.docx 作為手術對象。
from docx import Document
import os
# 確保目錄存在
os.makedirs(r'D:\Temp', exist_ok=True)
def create_demo_doc():
doc = Document()
doc.add_heading('原始文件標題', 0)
doc.add_paragraph('這是原始文件的第一段內容。')
doc.add_paragraph('這是原始文件的第二段內容。')
doc.save(r'D:\Temp\demo_origin.docx')
print("測試檔案 'demo_origin.docx' 已生成!")
if __name__ == "__main__":
create_demo_doc()demo_origin.docx:
第二步:五大技法實戰 (完整程式碼)
這段程式碼整合了所有技巧,請直接執行。
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn
from docx.opc.constants import RELATIONSHIP_TYPE as RT
def run_advanced_demo():
# 讀取剛剛生成的檔案
doc = Document(r'D:\Temp\demo_origin.docx')
body = doc.element.body # 取得 XML 的 Body 元素
# =====================================================
# 方法一:高階 API (High-Level)
# =====================================================
print("--- 執行方法一:標準加入 ---")
# 限制:只能 Append 到文件「最後面」
doc.add_paragraph('方法一:我是高階 API 產生的段落 (在最後面)')
# =====================================================
# 方法二:底層 XML (Low-Level) - 絕對位置插入
# =====================================================
print("--- 執行方法二:底層 XML 組裝 ---")
# 手動組裝 XML 結構: <w:p> -> <w:r> -> <w:t>
p_elem = OxmlElement('w:p') # 1. 段落
r_elem = OxmlElement('w:r') # 2. 樣式片段
t_elem = OxmlElement('w:t') # 3. 文字
t_elem.text = '方法二:我是底層 XML 組裝的段落 (插在最前面)'
r_elem.append(t_elem) # 裝箱
p_elem.append(r_elem) # 裝箱
# 插入到 Body 的 Index 0 (最前面)
body.insert(0, p_elem)
# =====================================================
# 方法三:混合技 (Hybrid) - 先建後移 (絕對位置)
# =====================================================
print("--- 執行方法三:混合技 (Insert) ---")
# 1. 用高階 API 快速建立 (預設在最後)
new_p = doc.add_paragraph('方法三:我是混合技產生的段落 (原本在後,被移到前)')
new_p.runs[0].bold = True
# 2. 取得底層 XML 元素
p_element = new_p._p
# 3. 安全移除 (找爸爸刪除自己)
if p_element.getparent() is not None:
p_element.getparent().remove(p_element)
# 4. 插在 Index 1 (剛好在方法二的後面)
body.insert(1, p_element)
# =====================================================
# 方法四:相對定位法 (Relative) - 使用 addnext 插隊
# =====================================================
print("--- 執行方法四:相對定位 (addnext) ---")
# 情境:我想要插在「原始文件標題」的後面
# 1. 建立段落
p4 = doc.add_paragraph('方法四:我是用 addnext 插隊的段落 (在標題後面)')
p4.runs[0].font.italic = True
p4_elem = p4._p
# 2. 安全移除
if p4_elem.getparent() is not None:
p4_elem.getparent().remove(p4_elem)
# 3. 找到錨點 (Anchor)
# 目前順序:[0]方法二 -> [1]方法三 -> [2]原始標題
anchor_paragraph = body[2]
# 4. 插隊 (跟在標題後面)
anchor_paragraph.addnext(p4_elem)
# =====================================================
# 方法五:超連結混合技 (Hyperlink Hybrid) - 大魔王
# =====================================================
print("--- 執行方法五:超連結混合技 ---")
url = "https://www.google.com"
link_text = "方法五:點我前往 Google (我是超連結,我也插在最前面)"
# 1. [高階] 註冊關聯 (取得 rId)
part = doc.part
# docx.parts.document.DocumentPart
r_id = part.relate_to(url, RT.HYPERLINK, is_external=True)
# 2. [底層] 手工打造 <w:hyperlink>
hyperlink = OxmlElement('w:hyperlink')
hyperlink.set(qn('r:id'), r_id) # 設定關聯 ID
# 3. 建立內容 (Run + 樣式 + Text)
run = OxmlElement('w:r')
rPr = OxmlElement('w:rPr')
# 設定藍色
c = OxmlElement('w:color')
c.set(qn('w:val'), '0000FF')
rPr.append(c)
# 設定底線
u = OxmlElement('w:u')
u.set(qn('w:val'), 'single')
rPr.append(u)
run.append(rPr)
# 設定文字
t = OxmlElement('w:t')
t.text = link_text
run.append(t)
hyperlink.append(run) # 把 Run 裝進 Hyperlink
# 4. [混合技] 借殼上市
# 建立一個空段落容器
p_container = doc.add_paragraph()
# 把超連結塞進去
p_container._p.append(hyperlink)
# 5. [底層] 移動位置 (移到最前面 Index 0)
p_link_elem = p_container._p
# 安全移除
if p_link_elem.getparent() is not None:
p_link_elem.getparent().remove(p_link_elem)
# 插隊當老大
body.insert(0, p_link_elem)
# 存檔
output_path = r'D:\Temp\demo_result_final.docx'
doc.save(output_path)
print(f"操作完成!請開啟 '{output_path}' 查看結果。")
if __name__ == "__main__":
run_advanced_demo()輸出:
demo_result_final.docx
段落 vs 超連結段落
關鍵差異
超連結多了一個 <w:hyperlink> 標籤,它像一個「夾心層」,把 <w:r> 包在裡面。而這個標籤最重要的屬性是 r:id,它負責告訴 Word:「這個連結要連去哪裡(網址)」。
第三步:技法解析
1. 為什麼要用 getparent().remove()?
在之前的嘗試中,我們直接用 body.remove(p),這有時會報錯 ValueError: Element is not a child of this node。
這是因為 doc.add_paragraph() 產生的元素,其父節點參照可能與我們手上的 body 變數不同步。
最安全的寫法是:「找到該元素的親生父親 (getparent()),然後叫父親把它移除」。
2. insert vs addnext
- body.insert(i, elem):
- 絕對位置。
- 適合:「我要放在第 0 個」、「我要放在第 1 個」。
- 不依賴其他段落是否存在。
- anchor.addnext(elem):
- 相對位置。
- 適合:「我要跟在標題後面」、「我要跟在表格後面」。
- 必須先找到一個存在的「錨點 (Anchor)」。
3. 超連結的奧義
超連結無法純用高階 API 建立,也無法純用底層 XML (因為要註冊 .rels 關聯)。
所以必須使用 混合技:
- 用 doc.part.relate_to 處理關聯 (High-level)。
- 用 OxmlElement 建立標籤 (Low-level)。
- 用 doc.add_paragraph 建立容器 (High-level)。
- 用 insert/addnext 移動位置 (Low-level)。
這套流程是 python-docx 開發者的必備技能!
推薦hahow線上學習python: https://igrape.net/30afN
# 1. [高階] 註冊關聯 (取得 rId)
part = doc.part
# docx.parts.document.DocumentPart
r_id = part.relate_to(url, RT.HYPERLINK, is_external=True) 這一步是建立超連結最關鍵、也最抽象的一步。如果不做這一步,你就算在 XML 裡寫了一百個 <w:hyperlink>,Word 打開來也只會是一串死文字,點都點不動。
讓我們深入解析這兩行程式碼在做什麼。
1. part = doc.part
- doc:是你的 Document 物件,代表整份 Word 文件。
- part:是 docx.parts.document.DocumentPart 物件。
- 意義:在 OOXML (Office Open XML) 的架構中,.docx 是一個 ZIP 包,裡面有很多個「零件 (Part)」。
- word/document.xml 是一個 Part(主文件內容)。
- word/styles.xml 是一個 Part(樣式)。
word/media/image1.png也是一個 Part。
- 為什麼要拿它?:因為我們要操作的是「主文件內容 (document.xml)」這個零件的關聯表 (Relationships)。
2. part.relate_to(…)
這行程式碼做了三件大事:
A. 建立關聯 (Create Relationship)
它會在 word/_rels/document.xml.rels 這個檔案(也就是我們之前提到的「帳本」)裡,新增一筆記錄。
這筆記錄大概長這樣:
<Relationship Id="rId5"
Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
Target="https://www.google.com"
TargetMode="External"/>Id=”rId5″:這是系統自動分配的一個唯一代號。
Target=”…”:這是你傳進去的網址 (url)。
TargetMode=”External”:表示這是一個外部連結(網頁),而不是文件內部的書籤。
B. 回傳 ID (r_id)
函數執行完後,會把剛剛分配到的代號(例如 ‘rId5’)回傳給你。
C. 為什麼需要這個 ID?
因為在 Word 的正文 XML (document.xml) 裡,不允許直接寫網址。
你不能這樣寫:
<!-- ❌ 錯誤寫法 -->
<w:hyperlink url="https://www.google.com"> ... </w:hyperlink>你必須這樣寫:
<!-- ✅ 正確寫法 -->
<w:hyperlink r:id="rId5"> ... </w:hyperlink>Word 讀到 r:id=”rId5″ 時,會去查那本「帳本 (.rels)」:
「喔,rId5 是什麼?… 查到了,是連去 Google 的!」
總結
part.relate_to(…) 的作用就是:
- 登記:把網址登記在
.rels帳本裡。 - 領號碼牌:拿到一個 rId 號碼牌。
之後你在 XML 裡只要填這個號碼牌 (rId),Word 就知道你要連去哪裡了。這就是為什麼這一步是「高階 API」但卻是「底層 XML」運作的基礎。
part.relate_to(…) 的本質工作,就是幫你編修 word/_rels/document.xml.rels 的內容。
為什麼不自己手動修 XML?
理論上,你也可以自己用 lxml 去打開 .rels 檔,
塞一個 節點進去,然後自己算 rId (要確保不重複)。
但這樣做非常危險且麻煩:
ID 衝突:你必須遍歷所有現有的 ID,確保新 ID 唯一。
檔案鎖定:.rels 檔可能正在被其他程式讀取。
格式錯誤:XML 命名空間 (xmlns) 寫錯一點點,整份文件就會打不開。
所以 python-docx 提供的 relate_to 方法,就是一個安全的封裝,
幫你處理掉這些髒活累活,讓你只要關心「網址」和「類型」就好。
推薦hahow線上學習python: https://igrape.net/30afN
在 lxml (python-docx 的底層 XML 引擎) 中,
有 addprevious(elem) 這個方法。
addprevious vs addnext
anchor.addnext(elem):插在錨點的後面 (Next Sibling)。
anchor.addprevious(elem):插在錨點的前面 (Previous Sibling)。
為什麼我很少提 addprevious?
雖然它存在,但在實際操作 Word XML 時,我們比較少用它,原因有二:
思維習慣:我們通常習慣「由上而下」建立文件(先標題,再段落,再表格),
所以「插在後面」比較符合直覺。
替代方案:addprevious 其實就等於 insert(anchor_index, elem)。
如果你要插在某人前面,
其實就是插在那個人的位置上(把他擠到後面去)。
實戰範例
如果你想用 addprevious 來實作「插在標題前面」,程式碼會長這樣:
# 假設 anchor 是標題
anchor_paragraph = body[2]
# 建立新段落
p_new = doc.add_paragraph("我是插隊在前面的段落")
p_elem = p_new._p
# 安全移除
if p_elem.getparent() is not None:
p_elem.getparent().remove(p_elem)
# 插在標題前面
anchor_paragraph.addprevious(p_elem)