Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body

加入好友
加入社群
Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body - 儲蓄保險王

📚 前言:兩種遍歷方式的差異

使用 python-docx 時,有兩種主要的遍歷文檔方式:

  1. 簡單方式doc.paragraphs – 只能看到段落
  2. 進階方式:遍歷 doc.element.body – 能看到完整結構

本文將詳細說明兩種方式的差異、使用場景和注意事項。


📖 第一章:基礎對比

1.1 簡單方式:doc.paragraphs

from docx import Document

# 創建測試文檔
doc = Document()
doc.add_heading('第一章', level=1)
doc.add_paragraph('這是第一段文字')
doc.add_table(rows=2, cols=2)  # 添加表格
doc.add_paragraph('這是表格後的文字')

# 簡單方式只能看到段落
print("使用 doc.paragraphs:")
for i, para in enumerate(doc.paragraphs):
    print(f"段落 {i}: {para.text}")
    print(f"  樣式: {para.style.name}")  # ✅ 可以安全訪問樣式

# 輸出
# 段落 0: 第一章
# 段落 1: 這是第一段文字  
# 段落 2: 這是表格後的文字
# 注意看不到表格

輸出:

Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body - 儲蓄保險王

1.2 進階方式:遍歷 body 元素

# 進階方式能看到所有元素
print("\n遍歷 doc.element.body:")
body = doc.element.body

for i, elem in enumerate(body):
    elem_type = elem.tag.split('}')[-1]  # 獲取元素類型
    
    if elem_type == 'p':  # 段落
        # 獲取段落文字不創建 Paragraph 對象
        text = ''.join(t.text for t in elem.iter() if t.tag.endswith('}t') and t.text)
        print(f"位置 {i}: [段落] {text}")
        
    elif elem_type == 'tbl':  # 表格
        print(f"位置 {i}: [表格]")
        
    elif elem_type == 'sectPr':  # 節屬性
        print(f"位置 {i}: [節屬性/分頁]")

# 輸出
# 位置 0: [段落] 第一章
# 位置 1: [段落] 這是第一段文字
# 位置 2: [表格]  ← 看到表格了
# 位置 3: [段落] 這是表格後的文字

輸出:

Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body - 儲蓄保險王

🎯 第二章:正確的使用方式

2.1 手動創建 Paragraph 對象的正確方法

‘D:\Temp\test_document.docx’ #path
#有段落,也有表格

Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body - 儲蓄保險王

當我們需要遍歷 body 元素並同時訪問段落屬性時,正確的做法是:

from docx import Document
from docx.text.paragraph import Paragraph

doc = Document(path)
body = doc.element.body

for elem in body:
    if elem.tag.endswith('p'):
        # ✅ 正確使用 doc 作為 parent
        para = Paragraph(elem, doc)
        #docx.text.paragraph.Paragraph
        
        # 現在可以訪問所有屬性
        print(f"文字: {para.text}")
        print(f"樣式: {para.style.name}")
        print(f"對齊: {para.alignment}")
        print(f"Runs: {len(para.runs)}")

輸出:

Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body - 儲蓄保險王

2.2 為什麼必須用 doc 作為 parent?

關鍵在於 part 屬性:

# Document 對象有 part 屬性
doc.part  # ✅ <docx.parts.document.DocumentPart object>

# CT_Body 對象沒有 part 屬性
body.part  # ❌ AttributeError: 'CT_Body' object has no attribute 'part'

Paragraph 類的許多屬性都依賴 part

# Paragraph 類內部簡化示意
class Paragraph:
    def __init__(self, p_element, parent):
        self._element = p_element
        self._parent = parent
    
    @property
    def style(self):
        # 需要通過 parent.part 來獲取樣式
        style_id = self._element.style
        return self._parent.part.get_style(style_id)  # 這裡需要 part

2.3 實際應用:帶樣式判斷的章節定位

def find_sections_with_style(doc, style_name="Heading 1"):
    """
    找出所有特定樣式的章
    """
    from docx.text.paragraph import Paragraph
    
    body = doc.element.body
    sections = []
    
    for idx, elem in enumerate(body):
        if elem.tag.endswith('p'):
            # 使用 doc 作為 parent
            para = Paragraph(elem, doc)
            
            if para.style.name == style_name:
                sections.append({
                    'index': idx,
                    'text': para.text,
                    'style': para.style.name,
                    'alignment': para.alignment,
                    'font_name': para.style.font.name,
                    'font_size': para.style.font.size.pt if para.style.font.size else None
                })
    
    return sections

# 使用範例
sections = find_sections_with_style(doc, "Heading 1")
for s in sections:
    print(f"\n📍 {s['text']}")
    print(f"   位置: body[{s['index']}]")
    print(f"   字型: {s['font_name']} {s['font_size']}pt")

輸出:

Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body - 儲蓄保險王

2.4 處理複雜文檔結構:包含表格

def find_all_headings(doc, heading_styles=["Heading 1", "Heading 2"]):
    """
    找出所有標題,包括表格內的標
    """
    from docx.text.paragraph import Paragraph
    
    body = doc.element.body
    headings = []
    
    for idx, elem in enumerate(body):
        if elem.tag.endswith('p'):
            para = Paragraph(elem, doc)
            if para.style.name in heading_styles:
                headings.append({
                    'index': idx,
                    'text': para.text,
                    'style': para.style.name,
                    'type': 'paragraph'
                })
        #因為標題不可能在表格中,以下是在做白工:
        elif elem.tag.endswith('tbl'):
            # 處理表格內的段落
            for cell in elem.iter('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tc'):
                for p in cell.iter('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p'):
                    para = Paragraph(p, doc)
                    if para.style.name in heading_styles:
                        headings.append({
                            'index': idx,
                            'text': para.text,
                            'style': para.style.name,
                            'type': 'table'
                        })
    
    return headings

輸出:

Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body - 儲蓄保險王

2.5 實戰案例:提取Heading 1 標題的邊界並刪除其下的整個內容:

def get_chapter_range(doc, chapter_keyword, style_name=None):
    """
    獲取章節的起始和結束索引,以及所有相關元素的詳細資
    
    Args:
        doc: Document 對象
        chapter_keyword: 章節關鍵字
        style_name: 指定的樣式名稱如果為 None則收集所有 Heading 樣式
    
    Returns:
        dict: 包含章節範圍和元素詳細資訊
    """
    from docx.text.paragraph import Paragraph
    from docx.table import Table
    
    body = doc.element.body
    heading_indices = []
    all_elements = []  # 記錄所有元素的類型和資訊
    
    # 第一遍收集所有元素資訊
    for idx, elem in enumerate(body):
        elem_info = {
            'index': idx,
            'element': elem,
            'tag': elem.tag
        }
        
        if elem.tag.endswith('p'):
            para = Paragraph(elem, doc)
            elem_info['type'] = 'paragraph'
            elem_info['text'] = para.text
            elem_info['style'] = para.style.name
            elem_info['paragraph_obj'] = para
            
            # 判斷是否為標題
            if style_name is None:
                if para.style.name.startswith("Heading"):
                    elem_info['is_heading'] = True
                    elem_info['level'] = int(para.style.name.split()[-1])
                    heading_indices.append(elem_info)
            else:
                if para.style.name == style_name:
                    elem_info['is_heading'] = True
                    heading_indices.append(elem_info)
                    
        elif elem.tag.endswith('tbl'):
            elem_info['type'] = 'table'
            elem_info['table_obj'] = Table(elem, doc)
            
        else:
            elem_info['type'] = 'other'
        
        all_elements.append(elem_info)
    
    # 找到目標章節
    for i, heading in enumerate(heading_indices):
        if chapter_keyword in heading['text']:
            start_idx = heading['index']
            
            # 找到結束點
            if style_name is None:
                target_level = heading['level']
                end_idx = len(body)
                
                for j in range(i + 1, len(heading_indices)):
                    if heading_indices[j]['level'] <= target_level:
                        end_idx = heading_indices[j]['index']
                        break
            else:
                end_idx = heading_indices[i + 1]['index'] if i + 1 < len(heading_indices) else len(body)
            
            # 收集範圍內的所有元素
            elements_in_range = []
            for elem_info in all_elements:
                if start_idx <= elem_info['index'] < end_idx:
                    elements_in_range.append(elem_info)
            
            return {
                'found': True,
                'title': heading['text'],
                'style': heading['style'],
                'start': start_idx,
                'end': end_idx,
                'level': heading.get('level', None),
                'elements': elements_in_range,
                'element_count': len(elements_in_range),
                'paragraph_count': sum(1 for e in elements_in_range if e['type'] == 'paragraph'),
                'table_count': sum(1 for e in elements_in_range if e['type'] == 'table')
            }
    
    return {'found': False}


def delete_chapter(doc, chapter_keyword, style_name=None, dry_run=True):
    """
    刪除指定章節的所有內
    
    Args:
        doc: Document 對象
        chapter_keyword: 章節關鍵字
        style_name: 指定的樣式名稱
        dry_run: 如果為 True只顯示將要刪除的內容不實際刪除
    
    Returns:
        int: 刪除的元素數量
    """
    # 獲取章節範
    range_info = get_chapter_range(doc, chapter_keyword, style_name)
    
    if not range_info['found']:
        print(f"找不到包含 '{chapter_keyword}' 的章節")
        return 0
    
    print(f"\n找到章節: {range_info['title']} ({range_info['style']})")
    print(f"範圍: [{range_info['start']} - {range_info['end']})")
    print(f"包含: {range_info['paragraph_count']} 個段落, {range_info['table_count']} 個表格")
    
    if dry_run:
        print("\n[預覽模式] 將要刪除的內容:")
        for elem in range_info['elements'][:10]:  # 只顯示前10個
            if elem['type'] == 'paragraph':
                print(f"  [{elem['index']}] 段落: {elem['text'][:50]}...")
            elif elem['type'] == 'table':
                print(f"  [{elem['index']}] 表格")
        if len(range_info['elements']) > 10:
            print(f"  ... 還有 {len(range_info['elements']) - 10} 個元素")
        return 0
    
    # 實際刪除操作
    body = doc.element.body
    deleted_count = 0
    
    # 從後往前刪除避免索引變化的問題
    for elem in reversed(range_info['elements']):
        try:
            body.remove(elem['element'])
            deleted_count += 1
        except Exception as e:
            print(f"刪除元素 {elem['index']} 時出錯: {e}")
    
    print(f"\n成功刪除 {deleted_count} 個元素")
    return deleted_count


# 使用範例

# 1. 查看章節資訊
range_info = get_chapter_range(doc, "PCBA測試計畫")
if range_info['found']:
    print(f"章節資訊:")
    print(f"  標題: {range_info['title']}")
    print(f"  範圍: [{range_info['start']} - {range_info['end']})")
    print(f"  元素數: {range_info['element_count']}")
    print(f"  段落數: {range_info['paragraph_count']}")
    print(f"  表格數: {range_info['table_count']}")

# 2. 預覽要刪除的內容不實際刪除
delete_chapter(doc, "PCBA測試計畫", dry_run=False)

# 3. 實際刪除章節
# delete_chapter(doc, "PCBA測試計畫", dry_run=False)
# doc.save("modified_document.docx")

# 4. 更精確的刪除保留標題只刪除內容
def delete_chapter_content_only(doc, chapter_keyword, style_name=None):
    """只刪除章節內容,保留章節標題"""
    range_info = get_chapter_range(doc, chapter_keyword, style_name)
    
    if not range_info['found']:
        return 0
    
    body = doc.element.body
    deleted_count = 0
    
    # 跳過第一個元素標題),從第二個開始刪除
    for elem in reversed(range_info['elements'][1:]):
        try:
            body.remove(elem['element'])
            deleted_count += 1
        except Exception as e:
            print(f"刪除時出錯: {e}")
    
    return deleted_count

刪除後vs原始:

Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body - 儲蓄保險王

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

加入好友
加入社群
Python-docx 完整教學:正確理解文檔元素遍歷; doc.paragraphs vs doc.element.body - 儲蓄保險王

儲蓄保險王

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

You may also like...

發佈留言

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