sample.zip為sample.docx修改副檔名為.zip
其中的document.xml 內容:

a:blip r:embed=”rId4″ cstate=”print”
目標
- 從 .docx(或同內容的 .zip)中找出文件內嵌圖片的 rId,
映射到實際檔案路徑,並讀出圖片位元組。 - 對比兩種做法:
- python-docx 的 qn + ElementTree 的 find/findall
- XPath(含前綴版與無前綴 fallback)
可直接貼進 Jupyter 執行。程式避免 Path.resolve 等易踩雷 API。
準備
- 檔案:sample.docx 或 sample.zip(二擇一;.docx 本質也是 ZIP 容器)
- 套件:lxml、python-docx
- 在 Jupyter 可先執行:%pip install lxml python-docx
教學程式(可直接複製執行)
# 若環境未安裝,解除註解以下一行:
# %pip install lxml python-docx
import zipfile
from pathlib import PurePosixPath
# POSIX = Portable Operating System Interface
# 主要針對類 Unix 系統的介面與行為做規範,
# 讓程式在不同 Unix/Unix-like 系統間更容易移植。
from lxml import etree
from docx.oxml.ns import qn # python-docx 內建的前綴→URI 轉換
#========================
# 1) 指定你的文件
#========================
# 填入 sample.docx 或 sample.zip 皆可(兩者都是 ZIP 容器)
DOCX_OR_ZIP = r"D:\Temp\sample.docx"
assert zipfile.is_zipfile(DOCX_OR_ZIP), "請提供 .docx 或 .zip(OOXML ZIP 容器)路徑"
#========================
# 2) 安全的 ZIP 內相對路徑拼接(不使用 resolve)
#========================
def join_zip_path(base_dir: str, rel_path: str) -> str | None:
"""
以 POSIX 規則將 base_dir 與 rel_path 串接並手動規範化 . 與 ..
回傳 ZipFile.namelist() 可用之相對路徑(不以 / 開頭)
base='word',rel='media/image1.png' → 'word/media/image1.png'
base='word',rel='./media/image1.png' → 'word/media/image1.png'(. 會被忽略)
base='word',rel='../media/image2.jpg' → 'media/image2.jpg'(往上跳一層)
base='word/charts',rel='../media/image3.png' → 'word/media/image3.png'
"""
if not rel_path:
return None
base = list(PurePosixPath(base_dir).parts)
rel = list(PurePosixPath(rel_path).parts)
parts = base[:]
for seg in rel:
if not seg or seg == ".":
continue
if seg == "..":
if parts:
parts.pop()
continue
parts.append(seg)
return "/".join(parts)
#========================
# 3) 讀取核心 XML:word/document.xml 與 word/_rels/document.xml.rels
#========================
with zipfile.ZipFile(DOCX_OR_ZIP) as z:
doc_xml = z.read("word/document.xml") #bytes
rels_xml = z.read("word/_rels/document.xml.rels") #bytes
parser = etree.XMLParser(remove_blank_text=True)
doc_root = etree.fromstring(doc_xml, parser=parser)
rels_root = etree.fromstring(rels_xml, parser=parser)
# lxml.etree._Element
#========================
# 4) 方法一:qn + find/findall
# - 不需自行維護 namespaces 字典 {前綴:命名空間URI}
# - 適合抓常見節點與屬性
#========================
def get_rids_with_qn(root_el):
rids = []
# a:blip 的 r:embed
for blip in root_el.findall(f".//{qn('a:blip')}"):
# qn('a:blip')
# '{http://schemas.openxmlformats.org/drawingml/2006/main}blip'
# blip : lxml.etree._Element
rid = blip.get( qn("r:embed") ) #'rId4'
# qn("r:embed")
# '{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed'
if rid:
rids.append(rid)
# 某些文件圖片在 pic:pic 節點內
for blip in root_el.findall(f".//{qn('pic:pic')}//{qn('a:blip')}"):
# qn('pic:pic')
# '{http://schemas.openxmlformats.org/drawingml/2006/picture}pic'
rid = blip.get(qn("r:embed"))
if rid:
rids.append(rid)
return sorted(set(rids))
rids_qn = get_rids_with_qn(doc_root)
print("A) qn + find/findall rId:", rids_qn)
# A) qn + find/findall rId: ['rId4']
#========================
# 5) 方法二:XPath
# 5-1) 前綴版:可讀性好,但需 namespaces 映射
#========================
NS = {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"pic": "http://schemas.openxmlformats.org/drawingml/2006/picture",
"wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
"a14": "http://schemas.microsoft.com/office/drawing/2010/main",
"w14": "http://schemas.microsoft.com/office/word/2010/wordml",
} #Dict[前綴,命名空間]
def get_rids_with_xpath_prefix(root_el):
rids = []
rids += root_el.xpath("//a:blip/@r:embed", namespaces=NS)
#要搜尋到: a:blip r:embed="rId4" cstate="print"
rids += root_el.xpath("//pic:pic//a:blip/@r:embed", namespaces=NS)
return sorted(set(rids))
"""
在 XPath 裡,/@r:embed 的「/」是必要語法的一部分,
表示「從前一個步驟選到的節點,取其屬性 r:embed 的值」。不能用空格取代。
寫法 //a:blip/@r:embed 是正確的:
它會在文件中所有 a:blip 元素上,
選取屬性 r:embed 的值(例如 "rId4")。
若某個 a:blip 沒有 r:embed,該元素就不會產生結果。
如果還想同時篩選出具有特定屬性或屬性值的 a:blip,可以在元素步驟加條件:
只要有 r:embed 的 a:blip: //a:blip[@r:embed]
同時有 cstate="print"://a:blip[@r:embed][@cstate='print']
#List[ lxml.etree._Element ]
若只想拿到元素節點(而不是屬性值),用上面兩個。
若要直接拿 rId 值,用 //a:blip[@cstate='print']/@r:embed。
//a:blip/@r:embed → 回傳每個 a:blip 的 r:embed 屬性值(如 "rId4")
//a:blip[@r:embed] → 回傳符合條件的 a:blip 元素節點本身(僅篩選出有該屬性的元素)
//a:blip[@cstate='print']/@r:embed →
先篩選 cstate='print' 的 a:blip,再取其 r:embed 屬性值
"""
rids_xpath_prefix = get_rids_with_xpath_prefix(doc_root)
print("B1) XPath(前綴)rId:", rids_xpath_prefix)
# 5-2) 無前綴 fallback:不依賴文件裡的前綴
A = NS["a"]; R = NS["r"]
def get_rids_with_xpath_noprefix(root_el):
return sorted(set(root_el.xpath(
"//*[local-name()='blip' and namespace-uri()=$A]"
"/@*[local-name()='embed' and namespace-uri()=$R]",
A=A, R=R
)))
rids_xpath_noprefix = get_rids_with_xpath_noprefix(doc_root)
print("B2) XPath(無前綴)rId:", rids_xpath_noprefix)
#========================
# 6) 合併 rId(去重)
#========================
rids = sorted(set(rids_qn) | set(rids_xpath_prefix) | set(rids_xpath_noprefix))
print("合併 rId:", rids)
#========================
# 7) 解析 _rels:rId -> Target
# - .rels 使用 package relationships 命名空間
# - 三種寫法擇一,這裡採用前綴版(最直覺)
#========================
PKG_REL = "http://schemas.openxmlformats.org/package/2006/relationships"
NS_RELS = {"rel": PKG_REL}
rid_to_target = {
el.get("Id"): el.get("Target")
for el in rels_root.xpath("//rel:Relationship", namespaces=NS_RELS)
}
targets = [rid_to_target.get(r) for r in rids]
print("rId → Target:", targets)
#========================
# 8) 轉成 ZIP 內實際路徑(以 word/ 為基準)並讀圖片 bytes
#========================
zip_paths = [join_zip_path("word", t) if t else None for t in targets]
print("ZIP 內圖片路徑:", zip_paths)
images = {}
with zipfile.ZipFile(DOCX_OR_ZIP) as z:
names = set(z.namelist())
for p in zip_paths:
if p and p in names:
images[p] = z.read(p)
print(f"讀到 {len(images)} 張圖片。前幾個鍵:", list(images.keys())[:3])
# 如需落地存檔,可解除以下範例:
# for i, (p, data) in enumerate(images.items(), 1):
# out = f"img_{i}{PurePosixPath(p).suffix}"
# with open(out, "wb") as f:
# f.write(data)
# print("saved:", out)輸出:

你會學到什麼
- qn 是什麼、為什麼好用
- qn 由 python-docx 提供(docx.oxml.ns.qn),
把 “前綴:名稱(local name)” 轉成
lxml 需要的擴展 QName(tag) “{uri}local”。 - 好處:不用自己維護 namespaces 字典;docx 常用前綴(w, a, r, pic, wp …)已內建。
- 適用:Element/屬性存取(find/findall/get/set)。不適用:XPath 字串內直接用 Clark notation。
- qn 由 python-docx 提供(docx.oxml.ns.qn),
- find/findall 的優勢與侷限
- 優勢:寫法直覺、學習成本低;搭配 qn 幾乎不會被前綴差異卡住。
- 侷限:只能走樹狀路徑,複雜條件(AND/OR、位置過濾、聚合)表達力不足。
- XPath 的兩種寫法與取捨
- 前綴版:可讀性最好,像 //a:blip/@r:embed;但必須提供 namespaces=…,且文件內前綴要吻合。
- 無前綴版:用 local-name() + namespace-uri() 避免前綴依賴,較冗長,勝在穩定。
- .rels 檔的命名空間
- document.xml.rels 使用 package relationships 命名空間(http://schemas.openxmlformats.org/package/2006/relationships)。
- 在 XPath 中必須用前綴或 local-name()/namespace-uri();不能把 {uri}Relationship 放進 XPath(會報錯)。
- ZIP 內路徑處理的坑
- OOXML 是 ZIP 容器,內部路徑一律使用 POSIX「/」。
- 用 PurePosixPath 規範化相對路徑(處理 ..、.)避免平台差異;不要用 OS 的實體路徑 API 來拼 ZIP 內部路徑。
- 圖片抽取流程的重要關聯
- document.xml 中的 a:blip/@r:embed 給出 rId。
- document.xml.rels 將 rId 對應到 Target 路徑(例如 media/image1.png)。
- 以 word/ 為基準拼成 ZIP 內實際路徑,最後從 ZIP 讀出位元組。
qn(‘a:blip’) ; qn(“r:embed”) ; qn(‘pic:pic’)

print( etree.tostring(rels_root, encoding=”unicode” , pretty_print=True) )

get_rids_with_qn()
最終可以獲得[‘rId4’]
再看rels_root 的內容
‘rId4’ 對應的Target=”media/image1.jpeg”
若僅修訂 document.xml 內容,讓圖片消失
檔案幾乎不會縮小
要刪除這一張圖,才能對docx瘦身
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" Target="webSettings.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/>
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
<Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" Target="fontTable.xml"/>
<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.jpeg"/>
</Relationships>實務建議
- 初學與日常維護
- 先選 qn + find/findall:簡單、穩、減少命名空間錯誤;對抓 blip 與 rId 已足夠。
- 只在需要複雜條件(例如依屬性篩選、一次抓多型資料、跨節點關係)時,才切到 XPath。
- 撰寫 XPath 的策略
- 優先用前綴版(可讀性好),並集中定義 namespaces 字典。
- 需要兼容「前綴不一致或缺失」的文件時,加上無前綴 fallback。兩段都跑、合併去重,實務最穩。
- 針對 .rels
- 一律用前綴或無前綴 XPath;或改用 Element.findall 的 Clark notation({uri}tag)但那不是 XPath 字串。
- 路徑與平台
- 只要處理 ZIP/OOXML 內部路徑,一律用 POSIX 風格與 PurePosixPath;不要把這些路徑丟給 Windows 路徑 API。
- 可靠性與除錯
- 每步都 print 出中間結果(rIds、targets、zip_paths、已讀圖片數量),可快速定位問題點。
- 用 set 去重,避免重複 rId 或重複圖片。
- 延伸
- 若需處理圖表、外部連結、嵌入物件,流程相同:從主文檔取得 rId → 讀對應 .rels → 拼路徑 → 讀取資源。
- 可將三種取得法封裝為函式與單元測試,之後套在任意 DOCX。
以下逐段與逐個函式解釋用途、重點與常見坑點。
為方便對照,先用小標題標出程式段落名稱,不重貼整段程式碼。
- 套件與基本設定
- 作用
- 匯入 zipfile:讀 .docx/.zip 內容(.docx 本質是 ZIP)。
- 匯入 lxml.etree:解析 XML。
- 匯入 docx.oxml.ns.qn:把 “前綴:名稱” 轉為擴展 QName “{uri}local”,供 Element API 使用。
- 匯入 PurePosixPath:用 POSIX 規則組合 ZIP 內部路徑。
- 重點
- 你可以用 sample.docx 或 sample.zip,兩者 zipfile 都能讀。
- assert zipfile.is_zipfile 確保路徑真的是 ZIP 容器,早期失敗早期發現。
- join_zip_path(base_dir, rel_path)
- 功能
- 把 .rels 的 Target(通常是相對路徑,如 media/image1.png 或 ../media/image2.jpg)與基準目錄(word/)以 POSIX 規則結合,並手動處理 . 與 ..。
- 為什麼不用 os.path 或 Path.resolve
- OOXML/ZIP 內部路徑永遠是 /,與作業系統無關。用 PurePosixPath 可避免 Windows 反斜線或磁碟機代號污染。
- 我們只處理 ZIP 內部字串,不需要也不應該觸碰實體檔案系統。
- 邏輯
- 拆 base_dir 與 rel_path 的 parts。
- 迭代 rel_path 的每個片段:
- “.” 忽略
- “..” 就把 parts 最後一段 pop 掉(回上一層)
- 其他字串就 append
- 最後用 “/” join 回去,得到像 “word/media/image1.png” 這樣的 ZIP 相對路徑。
- 常見坑
- rel_path 可能為空或 None,先行判斷。
- 不回傳以 “/” 開頭的絕對路徑,因為 ZipFile.namelist() 內的名稱都是相對路徑。
- 讀取核心 XML(document.xml 與 document.xml.rels)
- 功能
- 用 ZipFile 直接讀出兩個檔案:
- word/document.xml:主文檔,內含段落、圖片錨點等。
- word/_rels/document.xml.rels:主文檔對外部資源(含圖片)的關聯表。
- 用 etree.fromstring 解析成元素樹 root。
- 用 ZipFile 直接讀出兩個檔案:
- 重點
- XMLParser(remove_blank_text=True) 幫助後續輸出整齊,但對查詢無影響。
- 如果 zip 裡少其中一個檔案,這裡就會報錯,屬於早期應該發現的問題。
- get_rids_with_qn(root_el) — 方法一:qn + find/findall
- 功能
- 在 document.xml 裡找出所有圖片用到的 rId(關聯 ID),來源是 a:blip 節點的 r:embed 屬性。
- 兩段查找的原因
- “.//a:blip”:通用情況,多數圖片都會出現在 a:blip。
- “.//pic:pic//a:blip”:某些文件的圖片包在 pic:pic 節點內層,補抓一次。
- 為什麼用 qn
- find/findall 需要 Clark notation “{uri}local”。用 qn(“a:blip”)、qn(“r:embed”) 讓 python-docx 替你把前綴轉 uri,不必自己維護命名空間字典。
- 回傳值
- 去重後的 rId 清單(排序穩定,利於除錯與比較)。
- 常見坑
- 若文件使用 python-docx 不認得的前綴,qn 會丟 ValueError。但對 OOXML 常見前綴(w、a、r、pic…)是安全的。
- get_rids_with_xpath_prefix(root_el) — 方法二之一:XPath(前綴版)
- 功能
- 用 XPath 直接抓 //a:blip/@r:embed 與 //pic:pic//a:blip/@r:embed。
- 為何需要 NS 映射
- XPath 裡的 a:、r:、pic: 是前綴,不等於實際 URI。必須傳 namespaces=NS 讓引擎知道每個前綴對應的 URI。
- 優點
- 表達式短且可讀性高,適合熟悉 XPath 的人,也利於加入條件過濾。
- 缺點
- 文件若用非常規前綴(雖然 URI 一樣),寫死的前綴 XPath 還是能用,因為前綴對照由你提供;但你要保證 NS 字典正確且齊全。
- get_rids_with_xpath_noprefix(root_el) — 方法二之二:XPath(無前綴 fallback)
- 功能
- 用 local-name() 與 namespace-uri(),避免依賴文件內前綴。
- 表達式解讀
- 先找所有 blip 元素,且它們的 namespace-uri() 必須等於 A(DrawingML)。
- 然後取其屬性中名為 embed 且屬性的 namespace-uri() 等於 R(Relationships)的值。
- 優點
- 文件即使更換前綴也不受影響;對怪文件更堅固。
- 缺點
- 表達式冗長;你仍需要維護關鍵 URI 常數(A、R)。
- 合併 rId
- 為什麼要合併
- 三種找法可能重疊或取到不同位置的重複 rId。以 set 合併去重,再排序,讓後續映射更乾淨。
- 好處
- 即使某一種方法在特定文件失效,其他方法仍可補足,提高穩定度。
- 解析 .rels:rId → Target
- 背景
- document.xml 本身只有 rId,不知道實際檔案位置。document.xml.rels 把每個 rId 對應到 Target(路徑或 URL)。
- XPath 的選擇
- .rels 使用 package relationships 命名空間(http://schemas.openxmlformats.org/package/2006/relationships)。
- 這裡用前綴查詢 //rel:Relationship 並傳入 namespaces={“rel”: PKG_REL}。
- 為什麼不用 “{uri}Relationship” 放在 XPath
- Clark notation 是 Element API 的語法,XPath 不認得。若這樣寫會報 XPathEvalError。
- 回傳
- 建一張 dict:{Id -> Target},例如 {“rId5”: “media/image1.png”}。
- 組 ZIP 內實際路徑並讀圖片 bytes
- 為什麼基準是 “word/”
- document.xml 位於 word/,它的 .rels 內 Target 通常是相對於 word/ 的路徑(例如 media/image1.png 或 ../media/image2.jpg)。
- join_zip_path 的用途
- 把 “word/” 與 Target 正確合併(處理 .. 與 .)。
- 讀取
- 用 ZipFile.namelist() 比對檔名存在後,再 read 取出位元組。
- 收集成 images 字典:{zip內路徑: bytes},方便後續存檔或內存處理。
- 常見坑
- 有些 Target 可能指向外部 URL(TargetMode=”External”),這種不是 ZIP 內檔案,read 會失敗。你的程式現在會自動跳過(因為 zip 裡找不到那個名字)。
整體流程圖(文字版)
- document.xml
- 找 a:blip → 取 @r:embed → 得到 rId 清單
- document.xml.rels
- 查 //Relationship → 用 rId 查到 Target
- ZIP 內
- 用 word/ 作基底 + Target → 實際路徑 → 讀出圖片 bytes
什麼時候選哪一種查法
- 只要「抓 blip 的 r:embed」這類固定任務:優先 qn + find/findall,簡潔且不需 NS 字典。
- 需要條件化與複合查詢(例如按父節點類型、同時抓多種節點、位置篩選):改用 XPath(前綴版)。
- 需要兼容未知或奇怪前綴:加跑一次無前綴 XPath,與其他結果 union。
推薦hahow線上學習python: https://igrape.net/30afN







![Python爬蟲:BeautifulSoup的 .find_all() 與 .find() 與 .select(‘標籤名[屬性名1=”屬性值1″][屬性名2=”屬性值2″]’) ; from bs4 import BeautifulSoup ; Live Server(可以預覽HTML的VS Code套件) Python爬蟲:BeautifulSoup的 .find_all() 與 .find() 與 .select(‘標籤名[屬性名1=”屬性值1″][屬性名2=”屬性值2″]’) ; from bs4 import BeautifulSoup ; Live Server(可以預覽HTML的VS Code套件)](https://i0.wp.com/savingking.com.tw/wp-content/uploads/2025/03/20250330190318_0_925655.jpg?quality=90&zoom=2&ssl=1&resize=350%2C233)


近期留言