Python 實戰:Word 圖片提取與佔位符替換術

加入好友
加入社群
Python 實戰:Word 圖片提取與佔位符替換術 - 儲蓄保險王

本教學將展示如何使用 `python-docx` 與 `lxml`,
深入 Word 文件的底層 XML 結構,
將文件中的圖片「完美手術」取出,並在原位置留下標記。

**教學流程:**

1. **環境準備**:自動生成一個包含圖片的 Word 測試檔 (使用 `PIL` 繪圖)。

2. **核心原理**:解析 Word XML 結構,尋找圖片容器。

3. **執行手術**:提取圖片 Blob、寫入硬碟、插入替代文字、移除原圖片容器。

4. **驗證結果**:檢查新生成的檔案。

### 0. 安裝必要套件

若您的環境尚未安裝,請取消註解並執行下方指令:

# !pip install python-docx pillow lxml
# 注意正確的安裝名稱是 "python-docx" (PyPI 標準名稱),而不是 "docx"
# "docx" 是一個已停止維護的舊套件請勿安裝錯誤
# 雖然在程式碼中 import 時是使用 "import docx"但安裝時需指定 "python-docx"

為了確保大家都能執行,我們先用 Python「畫」一張圖,並存成 Word 檔。

這裡我們會用到 `PIL` (Pillow) 來繪製簡單的圖形,並用 `python-docx` 建立文件。

import io
from pathlib import Path
from PIL import Image, ImageDraw
from docx import Document
from docx.shared import Inches

# 設定工作目錄
WORK_DIR = Path("D:/Temp/DocxDemo")
WORK_DIR.mkdir(parents=True, exist_ok=True)

test_docx_path = WORK_DIR / "demo_origin.docx"

def create_sample_docx(path):
    doc = Document()
    doc.add_heading('Word 圖片提取測試', 0)
    
    doc.add_paragraph('這是第一段文字,下面會有一張紅色的圓形圖。')
    
    # 1. 動態畫一張紅色的圓
    img_byte_arr = io.BytesIO()
    img = Image.new('RGB', (200, 200), color='white')
    d = ImageDraw.Draw(img)
    d.ellipse((50, 50, 150, 150), fill='red', outline='black')
    img.save(img_byte_arr, format='PNG')
    
    # 2. 插入圖片到 Word
    doc.add_picture(img_byte_arr, width=Inches(2.0))
    
    doc.add_paragraph('這是圖片下方的文字。接下來是第二張藍色矩形圖。')
    
    # 3. 動態畫一張藍色的矩形
    img_byte_arr2 = io.BytesIO()
    img2 = Image.new('RGB', (300, 100), color='lightblue')
    d2 = ImageDraw.Draw(img2)
    d2.rectangle((10, 10, 290, 90), fill='blue', outline='black')
    img2.save(img_byte_arr2, format='PNG')
    
    doc.add_picture(img_byte_arr2, width=Inches(3.0))
    
    doc.save(path)
    print(f"測試檔案已建立:{path}")

create_sample_docx(test_docx_path)

測試檔案已建立:D:\Temp\DocxDemo\demo_origin.docx

Python 實戰:Word 圖片提取與佔位符替換術 - 儲蓄保險王

### 2. 核心邏輯:XML 解析與替換

這是本教學的精華。Word 的 `.docx` 本質上是 ZIP 壓縮的 XML 檔。

一般的 `python-docx` API 對於「讀取圖片位置」支援有限,
所以我們必須使用 `XPath` 直接操作底層 XML (`_element`)。

**關鍵技術點:**

*   **搜尋 (`xpath`)**:使用 `.//w:drawing | .//w:pict` 找出圖片容器。

*   **提取 (`rels`)**:透過 `r:embed` 屬性找到圖片的實體資料 (Blob)。

*   **替換 (`addprevious` + `remove`)**:在圖片節點前插入文字,然後移除圖片節點。

document.xml:

Python 實戰:Word 圖片提取與佔位符替換術 - 儲蓄保險王
import sys
from docx import Document
from docx.oxml import OxmlElement
from docx.oxml.ns import qn

def extract_image_bytes(doc, rid):
    """從 doc.part.rels 取得圖片 bytes 
    ->bytes|None
    """
    try:
        part = doc.part.rels[rid].target_part
        # 檢查是否為 ImagePart避免抓到其他連結物件
        return part.blob if "ImagePart" in type(part).__name__ else None
    except Exception as e:
        print(f"Warning: Failed to extract rid={rid}, {e}")
        return None

def get_ext(image_bytes: bytes) -> str:
    """
    透過 Magic Number 判斷並回傳對應的副檔名
    支援格式: JPG, PNG, BMP, GIF, ICO, TIFF, WEBP
    """
    if not image_bytes: return ".img"
    
    # 1. JPEG/JPG: FF D8 FF
    if image_bytes.startswith(b'\xff\xd8'):
        return ".jpg"
    
    # 2. PNG: 89 50 4E 47 0D 0A 1A 0A
    elif image_bytes.startswith(b'\x89PNG\r\n\x1a\n'):
        return ".png"
    
    # 3. BMP: BM (Windows Bitmap)
    elif image_bytes.startswith(b'BM'):
        return ".bmp"
    
    # 4. GIF: GIF87a or GIF89a
    elif image_bytes.startswith(b'GIF8'):
        return ".gif"
    
    # 5. ICO: 00 00 01 00 (Icon)
    elif image_bytes.startswith(b'\x00\x00\x01\x00'):
        return ".ico"
    
    # 6. TIFF (Intel-little endian): II* (49 49 2A 00)
    elif image_bytes.startswith(b'II*\x00'):
        return ".tiff"
    
    # 7. TIFF (Motorola-big endian): MM\0* (4D 4D 00 2A)
    elif image_bytes.startswith(b'MM\x00*'):
        return ".tiff"

    # 8. WebP: RIFF ... WEBP (需要檢查 offset)
    # 這裡做簡單檢查前四個是 RIFF且第 8-12  WEBP
    elif image_bytes.startswith(b'RIFF') and image_bytes[8:12] == b'WEBP':
        return ".webp"
    
    # 預設
    return ".img"


def process_docx_images(file_path):
    print(f"正在處理: {file_path.name}")
    doc = Document(str(file_path))
    
    # 輸出設定
    base_stem = file_path.stem
    output_dir = file_path.parent / "extracted_images"
    output_dir.mkdir(exist_ok=True, parents=True)
    
    img_counter = 0
    
    # 1. 取得 XML Body (正文區塊)
    body = doc.element.body
    
    # 2. 使用 XPath 搜尋所有 graphic container (w:drawing  w:pict)
    # 這是最穩定的搜尋方式不依賴 namespace map
    image_tags = body.xpath('.//w:drawing | .//w:pict')
    
    for img_node in image_tags:
        # 3. 找出該容器內所有參照的資源 ID (r:embed  r:id)
        # 使用寬鬆搜尋策略適用性最高
        rids = img_node.xpath('.//@r:embed | .//@r:id')
        #找到 <a:blip r:embed="rId9"/> ,取出 "rId9"
        rids = list(dict.fromkeys(rids)) # 去重且保留順序
        
        found_any = False
        placeholder_texts = []
        
        for rid in rids:
            blob = extract_image_bytes(doc, rid)
            if blob:
                img_counter += 1
                ext = get_ext(blob)
                
                # 存檔
                img_filename = f"{base_stem}__img{img_counter:02d}{ext}"
                save_path = output_dir / img_filename
                with open(save_path, "wb") as f:
                    f.write(blob)
                
                print(f"  -> 圖片已匯出: {img_filename}")
                placeholder_texts.append(f"<img>{img_filename}</img>")
                found_any = True

        # 4. 若有提取到圖片則執行替換手術
        if found_any:
            # 取得父節點 (通常是 w:r Run)
            parent = img_node.getparent()
            
            if parent is not None:
                # 建立新的文字節點 <w:t>
                full_text = "\n".join(placeholder_texts)
                t_node = OxmlElement('w:t')
                t_node.text = full_text
                
                # 手術動作 A: 在圖片容器插入文字
                img_node.addprevious(t_node)
                
                # 手術動作 B: 移除圖片容器
                parent.remove(img_node)

    # 5. 儲存結果
    out_docx = output_dir / f"{base_stem}__replaced.docx"
    doc.save(str(out_docx))
    print(f"處理完成!新文件儲存於: {out_docx}")
    return out_docx, output_dir

# 執行函式
replaced_docx, img_dir = process_docx_images(test_docx_path)    

### 3. 結果驗證

檢查輸出的結果。我們應該會看到:

1. 原始圖片已經被存成獨立的 `.png` 檔案。

2. 開啟新的 `.docx` 檔案時,原位置的圖片變成了 `<img…>` 的文字串。

Python 實戰:Word 圖片提取與佔位符替換術 - 儲蓄保險王
# 列出產生的檔案
print("輸出目錄內容:")
for f in img_dir.glob("*"):
    print(f" - {f.name}")

# 簡易驗證 Word 內容 (讀取文字)
new_doc = Document(replaced_docx)
print("\n新文件內容預覽:")
print("-" * 30)
for p in new_doc.paragraphs:
    print(p.text)
print("-" * 30)
Python 實戰:Word 圖片提取與佔位符替換術 - 儲蓄保險王

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

加入好友
加入社群
Python 實戰:Word 圖片提取與佔位符替換術 - 儲蓄保險王

儲蓄保險王

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

You may also like...

發佈留言

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