Python-docx 進階手術室:從底層 XML 到超連結混合技

加入好友
加入社群
Python-docx 進階手術室:從底層 XML 到超連結混合技 - 儲蓄保險王

在使用 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:

Python-docx 進階手術室:從底層 XML 到超連結混合技 - 儲蓄保險王

第二步:五大技法實戰 (完整程式碼)

這段程式碼整合了所有技巧,請直接執行。

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

輸出:

Python-docx 進階手術室:從底層 XML 到超連結混合技 - 儲蓄保險王

demo_result_final.docx

Python-docx 進階手術室:從底層 XML 到超連結混合技 - 儲蓄保險王

段落 vs 超連結段落

Python-docx 進階手術室:從底層 XML 到超連結混合技 - 儲蓄保險王

關鍵差異

超連結多了一個 <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 關聯)。
所以必須使用 混合技

  1. 用 doc.part.relate_to 處理關聯 (High-level)。
  2. 用 OxmlElement 建立標籤 (Low-level)。
  3. 用 doc.add_paragraph 建立容器 (High-level)。
  4. 用 insert/addnext 移動位置 (Low-level)。

這套流程是 python-docx 開發者的必備技能!

推薦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)

加入好友
加入社群
Python-docx 進階手術室:從底層 XML 到超連結混合技 - 儲蓄保險王

儲蓄保險王

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

You may also like...

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *