📝 建立測試文檔
首先,讓我們創建一個結構完整的測試文檔,包含標題、段落和表格:
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
🎓 總結與最佳實踐
關鍵概念
- 段落索引 vs Body索引:段落索引只計算段落,Body索引包含所有元素
- 表格不佔用段落索引:這是最常見的錯誤來源
- 倒序刪除:避免索引變化影響
使用建議
- 總是使用
dry_run=True
先預覽 - 保存原始文檔的備份
- 使用視覺化函數理解文檔結構
進階應用
- 可以擴展支持 Heading 2, Heading 3 等多層級
- 可以添加更多過濾條件(如日期、作者等)
- 可以整合到自動化文檔處理流程中
這個完整的解決方案解決了技術債問題,提供了準確、可靠的章節刪除功能!
推薦hahow線上學習python: https://igrape.net/30afN