攝影或3C

Python-docx 進階手術室:從底層 XML 到超連結混合技; from docx.opc.constants import RELATIONSHIP_TYPE as RT ; part = doc.part ; r_id = part.relate_to(url, RT.HYPERLINK, is_external=True)

在使用 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)」。

在實務經驗裡,只要不是要插到清單最前面(index=0),
通常會優先用 addnext()(或偶爾 addprevious())來定位。
原因:你通常是「找到某個既有節點」,然後要插在它前後;
這種情境下用兄弟節點 API 比較直觀、安全,不需要自己計算 index。
确保最前面時才用 insert(0, elem) 是比較穩定的組合拳。

3. 超連結的奧義

超連結無法純用高階 API 建立,
也無法純用底層 XML (因為要註冊 .rels 關聯)。
所以必須使用 混合技

  1. 用 doc.part.relate_to 處理關聯 (High-level)。
  2. 用 OxmlElement 建立元素 (Low-level)。
  3. 用 doc.add_paragraph 建立容器 (High-level)。
  4. 用 insert/addnext/addprevious 移動位置 (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(…) 的作用就是:

  1. 登記:把網址登記在 .rels 帳本裡。
  2. 領號碼牌:拿到一個 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

OPC 是 Open Packaging Conventions (開放封裝約定) 的縮寫。
Conventions : 大會會議

這是什麼?
它是微軟 Office 2007 (OOXML) 檔案格式的核心基礎架構。

簡單來說,OPC 規定了:

ZIP 容器:所有的 .docx, .xlsx, .pptx 檔案,
本質上都必須是一個 ZIP 壓縮檔。
零件 (Parts):
ZIP 裡面的每一個檔案(如 document.xml, image1.png)都稱為一個「零件」。
關聯 (Relationships):
零件之間如何互相引用(例如 document.xml 引用了 image1.png),
必須透過 .rels 檔案來描述。
內容類型 (Content Types):必須有一個 [Content_Types].xml 檔案,
告訴軟體每一個零件是什麼格式(是 XML?是圖片?還是字型?)。
為什麼 python-docx 會有這個模組?
docx.opc 是 python-docx 函式庫中,
專門用來處理這個底層封裝邏輯的模組。

它不關心你是 Word、Excel 還是 PowerPoint。
它只關心:「怎麼讀 ZIP」、「怎麼解析 .rels」、「怎麼管理 Parts」。
所以當你看到 docx.opc.constants 時,
這裡面定義的常數(如 RELATIONSHIP_TYPE),
都是 OPC 標準規範中定義好的通用字串,
不只 Word 用,Excel 和 PPT 也都通用。

    # 4. 插隊 (跟在標題後面)
    anchor_paragraph.addnext(p4_elem)

在 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)

推薦hahow線上學習python: https://igrape.net/30afN

儲蓄保險王

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