攝影或3C

Python: Word 文檔章節操作完整指南:從理解結構到精確刪除

📝 建立測試文檔

首先,讓我們創建一個結構完整的測試文檔,包含標題、段落和表格:

from docx import Document
from docx.shared import Inches
from docx.enum.style import WD_STYLE_TYPE

# 創建新文檔
doc = Document()

# 添加文檔標題
doc.add_heading('技術文件:產品測試規範', 0)

# 第1章
doc.add_heading('1. 概述', 1)
doc.add_paragraph('本文檔定義了產品測試的標準流程和規範。')
doc.add_paragraph('適用於所有硬體產品的測試驗證。')

# 第1章的表格
table1 = doc.add_table(rows=3, cols=3)
table1.style = 'Light List Accent 1'
# 填充表格
header_cells = table1.rows[0].cells
header_cells[0].text = '測試項目'
header_cells[1].text = '標準'
header_cells[2].text = '負責人'

table1.rows[1].cells[0].text = '功能測試'
table1.rows[1].cells[1].text = 'IEC 60950'
table1.rows[1].cells[2].text = '張三'

table1.rows[2].cells[0].text = '性能測試'
table1.rows[2].cells[1].text = 'ISO 9001'
table1.rows[2].cells[2].text = '李四'

# 第2章
doc.add_heading('2. PCBA測試計畫', 1)
doc.add_paragraph('印刷電路板組裝(PCBA)的測試流程包括以下步驟:')
doc.add_paragraph('1) 外觀檢查\n2) 電氣測試\n3) 功能驗證')

# 第2章的表格
table2 = doc.add_table(rows=4, cols=2)
table2.style = 'Light Grid Accent 1'
table2.rows[0].cells[0].text = '測試階段'
table2.rows[0].cells[1].text = '所需時間'
table2.rows[1].cells[0].text = 'ICT測試'
table2.rows[1].cells[1].text = '5分鐘'
table2.rows[2].cells[0].text = '功能測試'
table2.rows[2].cells[1].text = '15分鐘'
table2.rows[3].cells[0].text = '燒機測試'
table2.rows[3].cells[1].text = '24小時'

doc.add_paragraph('測試完成後需要生成測試報告。')

# 第3章
doc.add_heading('3. 環境測試要求', 1)
doc.add_paragraph('所有產品必須通過以下環境測試:')
doc.add_paragraph('• 高低溫測試:-40°C 至 +85°C')
doc.add_paragraph('• 濕度測試:95% RH')
doc.add_paragraph('• 振動測試:10-500Hz')

# 第4章包含子章節
doc.add_heading('4. 品質標準', 1)
doc.add_paragraph('本章定義產品品質標準。')

doc.add_heading('4.1 外觀標準', 2)
doc.add_paragraph('產品外觀不得有明顯瑕疵。')

doc.add_heading('4.2 電氣標準', 2)
doc.add_paragraph('所有電氣參數必須符合規格書要求。')

# 第5章
doc.add_heading('5. PCBA返修流程', 1)
doc.add_paragraph('當PCBA測試失敗時,需要進行返修。')
doc.add_paragraph('返修流程包括:故障分析、維修、重新測試。')

# 保存文檔
doc.save(r'D:\Temp\test_document.docx')
print("✅ 測試文檔已創建:test_document.docx")

生成的 test_document.docx:

📊 視覺化文檔結構

讓我們先看看文檔的內部結構:

def visualize_document_structure(doc, max_elements=50):
    """
    可視化文檔結構,幫助理解元素排
    """
    from docx.text.paragraph import Paragraph
    
    body = doc.element.body
    print(f"\n📋 文檔結構分析(共 {len(body)} 個元素)")
    print("=" * 80)
    
    for i, elem in enumerate(list(body)[:max_elements]):
        if elem.tag.endswith('p'):
            para = Paragraph(elem, doc)
            style = para.style.name
            text = para.text[:50] + "..." if len(para.text) > 50 else para.text
            
            # 用不同符號標記不同層級的標題
            if style == 'Heading 1':
                print(f"[{i:3d}] 📌 H1: {text}")
            elif style == 'Heading 2':
                print(f"[{i:3d}]   📎 H2: {text}")
            elif style == 'Title':
                print(f"[{i:3d}] 📑 標題: {text}")
            else:
                print(f"[{i:3d}]   段落: {text}")
                
        elif elem.tag.endswith('tbl'):
            print(f"[{i:3d}] 📊 【表格】")
        else:
            tag_name = elem.tag.split('}')[-1] if '}' in elem.tag else elem.tag
            print(f"[{i:3d}] ❓ 其他: {tag_name}")

# 載入並分析文檔
doc = Document(r'D:\Temp\test_document.docx')
visualize_document_structure(doc)

輸出:

body_children = list(body)

🔍 理解段落索引 vs Body索引的差異

這是理解文檔操作的關鍵:

def analyze_index_mapping(doc):
    """
    分析段落索引和body索引的對應關
    """
    body = doc.element.body
    body_children = list(body)
    
    print("\n🔗 索引對應關係分析")
    print("=" * 60)
    print("段落索引 → Body索引 | 內容")
    print("-" * 60)
    
    para_idx = 0
    for body_idx, elem in enumerate(body_children):
        if elem.tag.endswith('p'):
            para = doc.paragraphs[para_idx]
            text = para.text[:40] + "..." if len(para.text) > 40 else para.text
            print(f"P[{para_idx:2d}] → B[{body_idx:2d}] | {text}")
            para_idx += 1
        elif elem.tag.endswith('tbl'):
            print(f"      → B[{body_idx:2d}] | 📊 表格(不佔用段落索引)")
    
    print(f"\n📈 統計:")
    print(f"   總段落數:{len(doc.paragraphs)}")
    print(f"   總元素數:{len(body_children)}")
    print(f"   表格數量:{len(doc.tables)}")

analyze_index_mapping(doc)

輸出結果:

段落/表格以外,
其他可能遇到的兄弟元素:

# Word 文檔中常見的 body 子元素類型
COMMON_BODY_ELEMENTS = {
    'p': '段落',
    'tbl': '表格',
    'bookmarkStart': '書籤開始',
    'bookmarkEnd': '書籤結束',
    'sectPr': '節屬性(頁面設置)',
    'sdt': '結構化文檔標籤(內容控制項)',
    'customXml': '自訂 XML',
    'altChunk': '替代內容塊',
    'ins': '插入修訂(追蹤修訂)',
    'del': '刪除修訂(追蹤修訂)',
}

🎯 查找章節邊界(核心功能)

實現一個完整的章節邊界查找功能:

def find_heading_boundaries(doc, target_text="", style_name="Heading 1",
                          exact=False, case_sensitive=False):
    """
    找出章節的真實邊界,包含所有元
    """
    from docx.text.paragraph import Paragraph
    
    body = doc.element.body
    body_children = list(body)
    
    # 標準化函數
    if case_sensitive:
        norm = lambda s: s.strip()
        search_key = target_text.strip() if target_text else ""
    else:
        norm = lambda s: s.strip().lower()
        search_key = target_text.strip().lower() if target_text else ""
    
    # 內部匹配函數
    def matches_search(text):
        normalized_text = norm(text)
        if exact:
            return normalized_text == search_key
        else:
            return search_key in normalized_text
    
    sections = []
    current_section = None
    
    # 遍歷所有 body 元素
    for body_idx, elem in enumerate(body_children):
        if elem.tag.endswith('p'):
            para = Paragraph(elem, doc)
            
            # 檢查是否為目標標題
            if para.style.name == style_name:
                # 完成前一個章節
                if current_section is not None:
                    current_section['body_end'] = body_idx
                    
                    # 檢查是否匹配搜索條件
                    if not target_text or matches_search(current_section['heading_text']):
                        sections.append(current_section)
                
                # 開始新章節
                current_section = {
                    'heading_text': para.text,
                    'body_start': body_idx,
                    'body_end': None,
                    'tables': [],
                    'paragraphs': [body_idx],
                }
            else:
                # 記錄段落位置
                if current_section is not None:
                    current_section['paragraphs'].append(body_idx)
                    
        elif elem.tag.endswith('tbl'):
            # 記錄表格位置
            if current_section is not None:
                current_section['tables'].append(body_idx)
    
    # 處理最後一個章節
    if current_section is not None:
        current_section['body_end'] = len(body_children)
        if not target_text or matches_search(current_section['heading_text']):
            sections.append(current_section)
    
    # 添加統計信息
    for section in sections:
        section['table_count'] = len(section['tables'])
        section['paragraph_count'] = len(section['paragraphs'])
    
    return sections

# 測試查找功能
print("\n🔍 查找所有章節:")
all_sections = find_heading_boundaries(doc)
for s in all_sections:
    print(f"\n📍 {s['heading_text']}")
    print(f"   範圍: body[{s['body_start']}:{s['body_end']}]")
    print(f"   包含: {s['paragraph_count']} 個段落, {s['table_count']} 個表格")

# 查找特定章節
print("\n🔍 查找包含 'PCBA' 的章節:")
pcba_sections = find_heading_boundaries(doc, "PCBA")
for s in pcba_sections:
    print(f"   找到: {s['heading_text']}")

輸出:

🗑️ 刪除章節(最終實現)

def delete_sections_by_heading(doc, remove_texts, 
                              case_sensitive=False, 
                              exact=False,
                              keep_heading=False,
                              dry_run=False):
    """
    刪除指定的章節,包含其中的所有內
    """
    if not remove_texts:
        print("❌ 沒有指定要刪除的章節")
        return doc
    
    # 確保 remove_texts 是列表
    if isinstance(remove_texts, str):
        remove_texts = [remove_texts]
    
    # 收集所有要刪除的章節
    all_targets = []
    for text in remove_texts:
        matches = find_heading_boundaries(doc, text, "Heading 1", exact, case_sensitive)
        all_targets.extend(matches)
    
    # 去重
    unique_targets = []
    seen = set()
    for target in all_targets:
        key = target['body_start']
        if key not in seen:
            seen.add(key)
            unique_targets.append(target)
    
    if not unique_targets:
        print(f"❌ 沒有找到匹配的章節")
        return doc
    
    print(f"\n🎯 找到 {len(unique_targets)} 個要刪除的章節")
    
    if dry_run:
        print("\n📋 預覽模式 - 將會刪除以下章節:")
        for section in unique_targets:
            print(f"\n   章節: {section['heading_text']}")
            print(f"   範圍: body[{section['body_start']}:{section['body_end']}]")
            print(f"   內容: {section['paragraph_count']} 段落, {section['table_count']} 表格")
            if keep_heading:
                print("   (保留標題)")
        return doc
    
    # 執行刪除
    body = doc.element.body
    
    # 按位置倒序刪除避免索引變化
    for section in sorted(unique_targets, key=lambda x: x['body_start'], reverse=True):
        print(f"\n🗑️ 正在刪除: {section['heading_text']}")
        
        # 確定刪除範圍
        start = section['body_start'] + 1 if keep_heading else section['body_start']
        end = section['body_end']
        
        # 收集要刪除的元素
        elements_to_remove = []
        for i in range(start, end):
            if i < len(body):
                elements_to_remove.append(body[i])
        
        # 執行刪除
        removed = {'paragraphs': 0, 'tables': 0}
        for elem in elements_to_remove:
            try:
                body.remove(elem)
                if elem.tag.endswith('tbl'):
                    removed['tables'] += 1
                else:
                    removed['paragraphs'] += 1
            except:
                pass
        
        print(f"   ✅ 已刪除: {removed['paragraphs']} 段落, {removed['tables']} 表格")
    
    return doc

# 測試刪除功能
# 1. 預覽模式
print("\n" + "="*60)
print("測試 1: 預覽刪除包含 'PCBA' 的章節")
delete_sections_by_heading(doc, "PCBA", dry_run=True)

# 2. 實際刪除並保存
print("\n" + "="*60)
print("測試 2: 實際刪除並保存")
doc_copy = Document(r'D:\Temp\test_document.docx')  # 重新載入
delete_sections_by_heading(doc_copy, ["PCBA", "環境"], dry_run=False)
doc_copy.save(r'D:\Temp\test_document_modified.docx')
print("\n✅ 已保存修改後的文檔:test_document_modified.docx")

# 3. 驗證結果
print("\n" + "="*60)
print("驗證刪除結果:")
doc_modified = Document(r'D:\Temp\test_document_modified.docx')
remaining_sections = find_heading_boundaries(doc_modified)
print(f"\n剩餘章節數:{len(remaining_sections)}")
for s in remaining_sections:
    print(f"   ✓ {s['heading_text']}")

刪除表格的方式:

# 方法1直接從父元素移除
table_element.getparent().remove(table_element)

# 方法2 body 移除
body.remove(table_element)

輸出結果:

test_document_modified.docx:

📚 完整的測試腳本

將所有功能整合在一起:

# 完整測試流程
def run_complete_test():
    """執行完整的測試流程"""
    print("🚀 開始完整測試流程\n")
    
    # 1. 創建測試文檔
    print("步驟 1: 創建測試文檔")
    from docx import Document
    doc = Document()
    doc.add_heading('測試文檔', 0)
    
    # 添加多個章節
    sections_data = [
        ("1. 產品概述", ["這是產品介紹", "包含基本信息"], True),
        ("2. PCBA測試規範", ["測試流程說明", "測試標準定義"], True),
        ("3. 軟體測試", ["軟體功能測試", "性能測試"], False),
        ("4. PCBA維修指南", ["維修流程", "注意事項"], True),
        ("5. 品質保證", ["品質標準", "檢驗流程"], False),
    ]
    
    for title, paragraphs, add_table in sections_data:
        doc.add_heading(title, 1)
        for p in paragraphs:
            doc.add_paragraph(p)
        if add_table:
            table = doc.add_table(rows=2, cols=2)
            table.style = 'Light List Accent 1'
            table.rows[0].cells[0].text = '項目'
            table.rows[0].cells[1].text = '說明'
    
    doc.save('complete_test.docx')
    print("   ✅ 文檔已創建\n")
    
    # 2. 分析文檔結構
    print("步驟 2: 分析文檔結構")
    doc = Document('complete_test.docx')
    sections = find_heading_boundaries(doc)
    print(f"   找到 {len(sections)} 個章節\n")
    
    # 3. 測試不同的刪除場景
    print("步驟 3: 測試各種刪除場景\n")
    
    # 場景1: 刪除包含特定關鍵字的章節
    print("   場景 1: 刪除所有包含 'PCBA' 的章節")
    doc1 = Document('complete_test.docx')
    delete_sections_by_heading(doc1, "PCBA", dry_run=False)
    doc1.save('test_result_1.docx')
    print("   ✅ 已保存到 test_result_1.docx\n")
    
    # 場景2: 精確匹配刪除
    print("   場景 2: 精確匹配刪除 '3. 軟體測試'")
    doc2 = Document('complete_test.docx')
    delete_sections_by_heading(doc2, "3. 軟體測試", exact=True, dry_run=False)
    doc2.save('test_result_2.docx')
    print("   ✅ 已保存到 test_result_2.docx\n")
    
    # 場景3: 保留標題只刪除內容
    print("   場景 3: 刪除 '品質保證' 但保留標題")
    doc3 = Document('complete_test.docx')
    delete_sections_by_heading(doc3, "品質保證", keep_heading=True, dry_run=False)
    doc3.save('test_result_3.docx')
    print("   ✅ 已保存到 test_result_3.docx\n")
    
    print("🎉 測試完成!請查看生成的文檔文件。")

# 執行測試
run_complete_test()

輸出:

test_result_1.docx

🎓 總結與最佳實踐

關鍵概念

  1. 段落索引 vs Body索引:段落索引只計算段落,Body索引包含所有元素
  2. 表格不佔用段落索引:這是最常見的錯誤來源
  3. 倒序刪除:避免索引變化影響

使用建議

  • 總是使用 dry_run=True 先預覽
  • 保存原始文檔的備份
  • 使用視覺化函數理解文檔結構

進階應用

  • 可以擴展支持 Heading 2, Heading 3 等多層級
  • 可以添加更多過濾條件(如日期、作者等)
  • 可以整合到自動化文檔處理流程中

這個完整的解決方案解決了技術債問題,提供了準確、可靠的章節刪除功能!

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

儲蓄保險王

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