攝影或3C

Python實戰教學: yield 與 yield from 解析,遞迴遍歷docx所有Paragraph(包含儲存格_Cell 中的Paragraph) ; from docx.document import Document as DocxDocument #類別,非function ; from docx.table import _Cell, Table #儲存格/表格 類別

本筆記本將帶您了解如何使用 yieldyield from 來處理資料流,
最終應用於將 Word 文件中的巢狀結構(表格內的段落)「拉平」成單一序列。

1. 基礎暖身:什麼是 yield

yield 讓函式變成一個生成器 (Generator)。
它不會一次回傳所有資料,而是「一次產生一個」,
省記憶體且適合處理串流資料。

def make_bread_gen():
    yield "麵包 1 (熱騰騰)"
    yield "麵包 2 (剛出爐)"
    yield "麵包 3 (最後一個)"

# 驗證 List 一樣跑迴圈但它是惰性運算的
for bread in make_bread_gen():
    print(f"收到: {bread}")

2. 進階關鍵:為什麼需要 yield from

當我們需要「函式呼叫函式」來產生資料時(例如:巢狀結構),
如果只用 yield,你必須寫迴圈去接值再傳出去。
yield from 建立了「直通管道」,
讓子生成器的產出直接傳給最外層,不需要寫繁雜的轉接迴圈。

模擬情境:拆解多層清單 (Nested Lists)

# 模擬一個巢狀結構清單裡面還有清單
nested_data = [
    "外層 A",
    ["內層 1", "內層 2", ["深層 X", "深層 Y"]], # 這裡有多層
    "外層 B"
]

def flatten_list(data):
    for item in data:
        if isinstance(item, list):
            # 【關鍵遇到 List委派給自己 (遞迴) 去處理
            # yield from 會自動把內層吐出來的每個元素直接傳到最外面
            yield from flatten_list(item)
        else:
            yield item

print("拉平後的結果:")
print(list(flatten_list(nested_data)))

3. 實戰應用:解決 Word 表格的巢狀遍歷

Word 文件 (docx) 的結構也是巢狀的:

  • 文件 (Document)
    • 段落 (Paragraph)
    • 表格 (Table)
      • 列 (Row)
        • 儲存格 (Cell) -> 這裡面又可以包含段落或表格!

我們要寫一個函數,依照視覺順序把所有文字抓出來。

首先,我們先製作一個測試用 Word 檔,裡面包含主文和表格。

import os
from docx import Document

# 準備測試檔案路徑
DEMO_PATH = r"D:\Temp\docx_notebook_demo.docx"
os.makedirs(os.path.dirname(DEMO_PATH), exist_ok=True)

def create_demo_docx(path):
    doc = Document()
    doc.add_paragraph("【1. 主文開頭】Start")
    
    # 建立 2x2 表格
    table = doc.add_table(rows=2, cols=2)
    table.style = 'Table Grid'
    
    table.cell(0, 0).text = "【2. 表格 R0C0】Cell 1"
    
    # 這一格塞兩段
    cell_01 = table.cell(0, 1)
    cell_01.paragraphs[0].text = "【3. 表格 R0C1】Cell 2 - Para 1"
    cell_01.add_paragraph("【4. 表格 R0C1】Cell 2 - Para 2")
    
    table.cell(1, 1).text = "【5. 表格 R1C1】End of Table"
    
    doc.add_paragraph("【6. 主文結尾】End")
    doc.save(path)
    print(f"測試檔案已建立: {path}")

create_demo_docx(DEMO_PATH)

docx_notebook_demo.docx

4. 核心解法:遞迴遍歷生成器

我們定義 iter_all_paragraphs(parent)

  • parent 可以是 文件 也可以是 儲存格
  • 利用 yield from,當遇到表格時,
    我們就鑽進去每一格,把那一格當作新的 parent 進行遞迴。
from typing import Union, Iterable
from docx.document import Document as DocxDocument
from docx.table import _Cell, Table
from docx.text.paragraph import Paragraph
from docx.oxml.ns import qn

def iter_all_paragraphs(parent: Union[DocxDocument, _Cell]) -> Iterable[Paragraph]:
    """
    遞迴走訪 parent 內的節點
    如果遇到表格就使用 yield from iter_all_paragraphs(cell) 深入挖掘
    """
    
    # 1. 取得底層 XML 容器
    # 我們只處理這兩種類型的容器因為它們是 Paragraph 的直接父節點
    # 雖然 Table 也是物件但它的子節點是 Row -> Cell而不是 Paragraph
    # 所以我們不是遍歷 Table」,而是穿過 Table直接找它下面的 Cell
    if isinstance(parent, DocxDocument):
        # DocxDocument 的內容段落表格都存放在
        # .element.body 這個 <w:body> XML 節點下
        parent_elm = parent.element.body
    elif isinstance(parent, _Cell):
        # _Cell (表格儲存格) 的內容都存放在 ._tc 這個 <w:tc> XML 節點下
        parent_elm = parent._tc
    else:
        # 如果傳入的是其他不可預期的物件 (例如 Header/Footer ),暫不處理
        return

    # 2. 遍歷 XML 子節點
    for child in parent_elm.iterchildren():
        
        # 情況 A: 是段落 -> 直接產出
        if child.tag == qn('w:p'):
            yield Paragraph(child, parent)
            
        # 情況 B: 是表格 -> 深入每一格 -> 委派(yield from)
        elif child.tag == qn('w:tbl'):
            table = Table(child, parent)
            for row in table.rows:
                for cell in row.cells:
                    yield from iter_all_paragraphs(cell)

5. 驗證成果

現在我們可以把它當作一個普通的平面 list 來讀取了!
請觀察下列輸出的順序,是否為我們預期的視覺順序(包含插入在表格中間的文字)。

# 讀取剛剛建立的檔案
doc = Document(DEMO_PATH)

print("--- 開始依序讀取 ---")
for i, p in enumerate(iter_all_paragraphs(doc), 1):
    print(f"段落 {i}: {p.text}")
print("--- 結束 ---")

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

from docx.document import Document as DocxDocument #類別
from docx import Document #function

這是一個常見的命名衝突問題,在 python-docx 中特別明顯。

取別名(Aliasing)是最簡潔且明確的解決方案。

為什麼這會發生?

在 python-docx 函式庫的設計中:

  1. docx.Document (函式):
    • 這是一個工廠函式 (Factory Function)
    • 用途是:開啟一個舊檔案或建立一個新檔案。
    • 我們通常這樣用:doc = Document(“file.docx”)。
  2. docx.document.Document (類別):
    • 這才是真正的文件物件類別 (Class)
    • isinstance(doc, …) 需要用到這個「型別」。
    • 我們通常只在寫 Type Hinting (: Document) 或 isinstance 檢查時才會用到它。

為什麼取別名是最好的?

如果不取別名,我們就必須寫得很長,或者容易搞混:

方案 A:不取別名 (容易混淆)

from docx import Document  # 工廠函式
from docx.document import Document  # 壞消息這裡覆蓋了上面那個變數

# 結果你現在不能用 Document("file.docx") 因為它變成了類別

方案 B:寫全名 (太長)

import docx.document

def process(doc):
    if isinstance(doc, docx.document.Document):  # 寫起來很冗長
        pass

方案 C:取別名 (最簡潔 ✅)

from docx import Document  # 用來開檔案的 (主要入口)
from docx.document import Document as DocxDocument  # 用來做型別檢查的 (只在少數地方用)

def process(parent: DocxDocument):  # 清楚明瞭這是一個文件物件型別
    pass

所以在 iter_all_paragraphs 這個函式中,
為了同時支援 Type Hinting (parent: DocxDocument) 和 isinstance 檢查,
取別名 DocxDocument 是最標準且語意最清晰的做法。

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

儲蓄保險王

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