Python解析Word结构实现语义级文档对比

发布时间:2026/9/10 11:32:11
Python解析Word结构实现语义级文档对比 简介这是一套基于Python开发的Word文档.docx智能对比工具面向办公自动化开发者、文档质检工程师及高校计算机专业学生解决多版本Word文件在样式、结构与批注层面难以人工比对的痛点。资源包共22个文件含5个核心Python脚本如main.py、get_comments.py、docx_to_xml.py、7个测试/模板docx文档、6个XML中间格式文件用于样式解析以及README.md说明文档和日志、配置等辅助文件整体压缩包大小为22.33MB。已有421人学习下载体现其在实际文档合规审查、教学材料一致性核验等场景中的实用价值。用户可直接运行脚本完成样式序列化、段落/图表/标题层级统计、样式相似度评分计算并完整导出并结构化存储所有批注内容为JSON代码模块清晰、依赖明确仅python-docx便于二次开发与集成到CI/CD文档质检流程中。1. 用 Python 做 Word 文件对比不是比“谁改了哪行”而是比“语义结构是否一致”你手上有两份 .docx 文件一份是法务部发来的合同终稿一份是你昨天修改后存档的版本。Word 自带的“比较”功能能标出删减和加粗但一旦对方调整了段落顺序、拆分了表格、把一段文字从正文挪到文本框里——它就彻底失灵甚至报错“无法比较”。这不是 Word 的 bug而是它底层根本不按“文档结构”做比对而是依赖编辑历史快照。真正需要的是一个能穿透 .docx ZIP 封装、解析 document.xml 中的w:p段落、w:tbl表格、w:tc单元格等 Open XML 元素再逐节点比对样式、属性、嵌套关系的工具。Python 实现的 Word 对比工具核心价值不在“高亮差异”而在“识别结构性变更”比如某处标题从 Heading 2 变成 Heading 3某张三列表格被拆成两个两列表格某段含超链接的文字被整体替换为纯文本——这些才是业务审核时真正要卡住的点。适合法务、出版、教育内容质检等对格式合规性有硬性要求的场景也适合 CI/CD 流程中自动校验模板填充结果是否符合预设结构。2. 解析 .docx 文件结构从 ZIP 解包到 Open XML 节点提取2.1 理解 .docx 本质一个 ZIP 包裹的 Open XML 文档集.docx 文件不是二进制黑盒而是符合 ECMA-376 标准的 ZIP 归档。其核心内容位于word/document.xml该文件以 XML 描述整个文档的逻辑结构段落w:p、运行w:r即连续同格式文本、文本节点w:t、表格w:tbl、列表项w:ilvl等。样式信息分散在word/styles.xml图片存在word/media/目录下。直接读取.docx二进制会丢失所有结构语义而用python-docx库虽方便但它抽象掉了底层 XML 层级无法获取w:pPrw:spacing w:before240/这类精确间距控制也无法判断某个w:tc是否被设置了w:vMergerestart合并属性——而这恰恰是表格跨页断开的关键信号。提示不要用open(filename, rb)直接读取 .docx那只是读 ZIP 头必须解压或用zipfile模块定位document.xml。2.2 用 zipfile 和 xml.etree.ElementTree 提取原始 XML 结构import zipfile from xml.etree import ElementTree as ET def extract_document_xml(docx_path): 从 .docx 文件中提取 word/document.xml 的 ElementTree 根节点 返回ET.Element 对象代表 w:document 根元素 with zipfile.ZipFile(docx_path, r) as docx: # 读取 document.xml 内容注意需指定 encodingutf-8 xml_content docx.read(word/document.xml).decode(utf-8) # 解析 XML注意命名空间声明 # .docx 使用默认命名空间 http://schemas.openxmlformats.org/wordprocessingml/2006/main # ElementTree 默认不处理前缀需手动注册 ns {w: http://schemas.openxmlformats.org/wordprocessingml/2006/main} root ET.fromstring(xml_content) return root, ns # 示例调用 root_a, ns_a extract_document_xml(v1_contract.docx) root_b, ns_b extract_document_xml(v2_contract.docx)这段代码的关键在于zipfile.ZipFile直接读取内部文件避免临时解压目录decode(utf-8)是必须的因为docx.read()返回 bytes命名空间ns字典用于后续find()、findall()查找例如root.findall(.//w:p, ns)才能正确匹配段落节点返回root而非字符串是为了后续用ET进行节点遍历、属性提取、子树序列化等操作。2.3 提取可比对的结构化节点序列单纯比对整段 XML 字符串毫无意义——空格、换行、属性顺序不同都会导致哈希值变化。真正要对比的是“有意义的节点序列”。我们定义最小可比单元为结构化段落块Structured Paragraph Block它包含段落本身w:p及其所有子节点段落内每个w:r运行的文本内容w:t值与关键格式属性如w:b加粗、w:i斜体、字体名段落级别属性对齐方式w:jc w:valcenter/、缩进w:ind w:firstLine480/、行距w:spacing w:line360/若段落内含表格则递归提取该w:tbl的行列结构行数、列数、每个w:tc的合并状态。def extract_paragraph_blocks(root, ns): 从 document.xml 根节点提取所有结构化段落块列表 每个块是 dict含 text纯文本拼接、attrs段落属性字典、runs运行列表、table_structure若含表 blocks [] # 查找所有 w:p 段落节点 for p in root.findall(.//w:p, ns): block { text: , attrs: {}, runs: [], table_structure: None } # 提取段落属性对齐、缩进、间距 pPr p.find(w:pPr, ns) if pPr is not None: jc pPr.find(w:jc, ns) block[attrs][jc] jc.get({http://schemas.openxmlformats.org/wordprocessingml/2006/main}val) if jc is not None else left ind pPr.find(w:ind, ns) if ind is not None: block[attrs][firstLine] ind.get({http://schemas.openxmlformats.org/wordprocessingml/2006/main}firstLine, 0) block[attrs][left] ind.get({http://schemas.openxmlformats.org/wordprocessingml/2006/main}left, 0) # 提取所有 w:r 运行及其文本和格式 for r in p.findall(w:r, ns): run_text t_nodes r.findall(w:t, ns) for t in t_nodes: run_text t.text or # 获取运行级格式加粗、斜体、字体 rPr r.find(w:rPr, ns) run_attrs {} if rPr is not None: b rPr.find(w:b, ns) run_attrs[bold] b is not None i rPr.find(w:i, ns) run_attrs[italic] i is not None rFonts rPr.find(w:rFonts, ns) if rFonts is not None: run_attrs[font] rFonts.get({http://schemas.openxmlformats.org/wordprocessingml/2006/main}ascii, Times New Roman) block[runs].append({ text: run_text.strip(), attrs: run_attrs }) block[text] run_text # 检查段落内是否嵌套表格 tbl p.find(w:tbl, ns) if tbl is not None: block[table_structure] extract_table_structure(tbl, ns) blocks.append(block) return blocks def extract_table_structure(tbl_node, ns): 提取表格结构行数、列数、每个单元格的合并状态 rows tbl_node.findall(w:tr, ns) structure { rows: len(rows), cols: 0, cells: [] # 列表每项为 (row_index, col_index, is_merged) } for i, tr in enumerate(rows): tcs tr.findall(w:tc, ns) structure[cols] max(structure[cols], len(tcs)) for j, tc in enumerate(tcs): vMerge tc.find(.//w:vMerge, ns) is_merged vMerge is not None and vMerge.get({http://schemas.openxmlformats.org/wordprocessingml/2006/main}val) restart structure[cells].append((i, j, is_merged)) return structure这段提取逻辑的要点在于block[text]是纯文本拼接用于快速初筛如两段文字完全相同则跳过深层比对block[runs]保留了格式粒度能区分“加粗的‘甲方’”和“普通‘甲方’”block[table_structure]不比对表格内容只记录结构特征行/列数、合并位置因为表格内容常因数据填充变动但结构必须稳定所有属性提取都使用.get()并提供默认值如0或left避免因缺失节点导致 KeyError。3. 实现结构化差异比对从节点哈希到语义变更分类3.1 为结构化块生成稳定哈希忽略无关扰动XML 解析后得到的block是 Python 字典但直接hash(block)会失败dict 不可哈希且json.dumps(block)会受字典键序影响Python 3.7 保持插入序但保险起见仍需排序。更关键的是某些属性如时间戳、随机 ID在每次保存时都会变必须剔除。我们定义结构哈希StructHash仅基于业务相关字段生成 SHA256。import hashlib import json def struct_hash(block): 为结构化段落块生成稳定哈希 规则只包含 text、attrs过滤掉时间相关键、runs只取 textbolditalic、table_structure只取 rows/cols/cells # 构建精简字典 safe_block { text: block[text].strip(), attrs: {k: v for k, v in block[attrs].items() if k not in [time_created, time_modified]}, # 实际中需根据真实属性名过滤 runs: [] } for run in block[runs]: safe_run { text: run[text].strip(), bold: run[attrs].get(bold, False), italic: run[attrs].get(italic, False), font: run[attrs].get(font, Times New Roman) } safe_block[runs].append(safe_run) if block[table_structure]: safe_block[table_structure] { rows: block[table_structure][rows], cols: block[table_structure][cols], merged_cells: [cell for cell in block[table_structure][cells] if cell[2]] # 只存合并单元格 } # JSON 序列化时强制排序键确保哈希稳定 json_str json.dumps(safe_block, sort_keysTrue, ensure_asciiFalse) return hashlib.sha256(json_str.encode(utf-8)).hexdigest() # 示例为两个版本的段落块生成哈希 blocks_a extract_paragraph_blocks(root_a, ns_a) blocks_b extract_paragraph_blocks(root_b, ns_b) hashes_a [struct_hash(b) for b in blocks_a] hashes_b [struct_hash(b) for b in blocks_b]此哈希函数的设计原则sort_keysTrue确保字典键序固定ensure_asciiFalse保留中文字符原样避免\uXXXX编码差异过滤掉time_created等元数据字段实际项目中需根据document.xml中真实出现的属性名确认表格只保留结构特征不包含w:t文本因业务上允许表格内容动态填充。3.2 基于哈希的块级比对与变更类型判定哈希比对能快速识别“完全相同”、“完全新增”、“完全删除”的段落块。但更多情况是“相似但有微调”例如段落文字相同但对齐方式从left→center表格行数不变但某单元格从vMergerestart→vMergecontinue运行文本相同但字体从Arial→Calibri。此时需进行深度属性比对Deep Attribute Diffdef deep_diff_block(block_a, block_b, ns): 深度比对两个结构化段落块返回变更类型列表 返回示例[paragraph_alignment_changed, table_cell_merge_changed, run_font_changed] diffs [] # 1. 段落属性比对 attrs_a block_a[attrs] attrs_b block_b[attrs] if attrs_a.get(jc) ! attrs_b.get(jc): diffs.append(paragraph_alignment_changed) if attrs_a.get(firstLine) ! attrs_b.get(firstLine): diffs.append(paragraph_first_line_indent_changed) # 2. 运行级比对遍历 runs 列表需考虑增删 runs_a block_a[runs] runs_b block_b[runs] # 简单策略按索引比对假设 runs 顺序不变Word 通常如此 for i in range(min(len(runs_a), len(runs_b))): r_a runs_a[i] r_b runs_b[i] if r_a[text] r_b[text]: # 文本相同才比格式 if r_a[attrs].get(bold) ! r_b[attrs].get(bold): diffs.append(run_bold_changed) if r_a[attrs].get(font) ! r_b[attrs].get(font): diffs.append(run_font_changed) # 3. 表格结构比对 tbl_a block_a[table_structure] tbl_b block_b[table_structure] if tbl_a and tbl_b: if tbl_a[rows] ! tbl_b[rows] or tbl_a[cols] ! tbl_b[cols]: diffs.append(table_dimension_changed) # 比较合并单元格集合 merged_a set([(c[0], c[1]) for c in tbl_a[cells] if c[2]]) merged_b set([(c[0], c[1]) for c in tbl_b[cells] if c[2]]) if merged_a ! merged_b: diffs.append(table_cell_merge_changed) elif tbl_a and not tbl_b: diffs.append(table_removed) elif not tbl_a and tbl_b: diffs.append(table_added) return diffs # 主比对流程 def compare_docx_files(file_a, file_b): root_a, ns_a extract_document_xml(file_a) root_b, ns_b extract_document_xml(file_b) blocks_a extract_paragraph_blocks(root_a, ns_a) blocks_b extract_paragraph_blocks(root_b, ns_b) hashes_a [struct_hash(b) for b in blocks_a] hashes_b [struct_hash(b) for b in blocks_b] # 构建哈希到索引的映射 hash_to_idx_a {h: i for i, h in enumerate(hashes_a)} hash_to_idx_b {h: i for i, h in enumerate(hashes_b)} report { added: [], # file_b 中有file_a 中无 deleted: [], # file_a 中有file_b 中无 modified: [] # 两者都有但 deep_diff 发现变更 } # 找出共同哈希 common_hashes set(hashes_a) set(hashes_b) for h in common_hashes: idx_a hash_to_idx_a[h] idx_b hash_to_idx_b[h] diffs deep_diff_block(blocks_a[idx_a], blocks_b[idx_b], ns_a) # ns_a/ns_b 相同任选其一 if diffs: report[modified].append({ index_in_a: idx_a, index_in_b: idx_b, changes: diffs, text_preview: blocks_a[idx_a][text][:50] ... }) # 找出新增和删除 for h in set(hashes_b) - set(hashes_a): idx_b hash_to_idx_b[h] report[added].append({ index_in_b: idx_b, text_preview: blocks_b[idx_b][text][:50] ... }) for h in set(hashes_a) - set(hashes_b): idx_a hash_to_idx_a[h] report[deleted].append({ index_in_a: idx_a, text_preview: blocks_a[idx_a][text][:50] ... }) return report # 执行比对 result compare_docx_files(v1_contract.docx, v2_contract.docx) print(json.dumps(result, indent2, ensure_asciiFalse))此比对逻辑输出的是语义变更类型而非原始 XML 差异。例如paragraph_alignment_changed比w:jc w:valcenter/ in xml_a but not in xml_b更易理解table_cell_merge_changed直接指向业务风险点跨页表格可能被错误拆分run_font_changed提示品牌规范可能被违反合同要求统一使用“思源黑体”。这种分类结果可直接对接企业知识库例如当table_dimension_changed出现时自动触发法务复核流程。4. 输出可读报告与集成 CLI支持命令行调用与 HTML 可视化4.1 生成结构化 JSON 报告与简洁终端输出比对结果应同时满足两种需求自动化系统消费返回标准 JSON含added/deleted/modified数组每个元素带index_in_a/index_in_b便于定位人工快速浏览在终端打印摘要用颜色区分变更类型需colorama库。from colorama import init, Fore, Style init(autoresetTrue) # 支持 Windows 终端颜色 def print_terminal_report(report): 在终端打印简洁可读的比对摘要 print(f\n{Fore.CYAN} Word 文件结构比对报告 {Style.RESET_ALL}) print(f文件 A: {Fore.YELLOW}v1_contract.docx{Style.RESET_ALL}) print(f文件 B: {Fore.YELLOW}v2_contract.docx{Style.RESET_ALL}\n) total_changes len(report[added]) len(report[deleted]) len(report[modified]) if total_changes 0: print(f{Fore.GREEN}✓ 两份文档结构完全一致{Style.RESET_ALL}) return print(f{Fore.RED}⚠ 发现 {total_changes} 处结构性变更{Style.RESET_ALL}) if report[added]: print(f\n{Fore.GREEN}➕ 新增段落 ({len(report[added])} 处){Style.RESET_ALL}) for item in report[added][:3]: # 只显示前3条 print(f • {item[text_preview]}) if len(report[added]) 3: print(f ... 还有 {len(report[added])-3} 处新增) if report[deleted]: print(f\n{Fore.RED}➖ 删除段落 ({len(report[deleted])} 处){Style.RESET_ALL}) for item in report[deleted][:3]: print(f • {item[text_preview]}) if len(report[deleted]) 3: print(f ... 还有 {len(report[deleted])-3} 处删除) if report[modified]: print(f\n{Fore.BLUE}✏️ 修改段落 ({len(report[modified])} 处){Style.RESET_ALL}) for item in report[modified][:3]: changes 、.join(item[changes]) print(f • {item[text_preview]} → {changes}) if len(report[modified]) 3: print(f ... 还有 {len(report[modified])-3} 处修改) # 调用示例 report compare_docx_files(v1_contract.docx, v2_contract.docx) print_terminal_report(report) # 同时保存 JSON 报告 with open(diff_report.json, w, encodingutf-8) as f: json.dump(report, f, indent2, ensure_asciiFalse)终端输出效果示例 Word 文件结构比对报告 文件 A: v1_contract.docx 文件 B: v2_contract.docx ⚠ 发现 5 处结构性变更 ➕ 新增段落 (2 处) • 第八条 争议解决方式本合同履行过程中发生的争议... • 附件一技术规格参数表共12页 ➖ 删除段落 (1 处) • 第五条 付款方式甲方应在收到发票后30日内支付... ✏️ 修改段落 (2 处) • 第三条 服务内容乙方应提供... → paragraph_alignment_changed、run_font_changed • 附件二保密协议 → table_dimension_changed4.2 构建命令行接口CLI支持python word-diff.py a.docx b.docx将工具封装为可直接调用的脚本遵循 Unix 哲学一个工具一个职责输入输出清晰。#!/usr/bin/env python3 # word-diff.py import argparse import sys def main(): parser argparse.ArgumentParser( descriptionPython 实现的 Word (.docx) 文件结构对比工具, formatter_classargparse.RawDescriptionHelpFormatter, epilog 用法示例 python word-diff.py old.docx new.docx # 终端输出摘要 python word-diff.py old.docx new.docx --json report.json # 保存 JSON 报告 python word-diff.py old.docx new.docx --html report.html # 生成 HTML 可视化报告 ) parser.add_argument(file_a, help基准文件路径 (.docx)) parser.add_argument(file_b, help待比对文件路径 (.docx)) parser.add_argument(--json, help输出 JSON 报告到指定文件) parser.add_argument(--html, help输出 HTML 可视化报告到指定文件) args parser.parse_args() # 验证文件存在且为 .docx if not args.file_a.lower().endswith(.docx) or not args.file_b.lower().endswith(.docx): print(f{Fore.RED}错误输入文件必须为 .docx 格式{Style.RESET_ALL}) sys.exit(1) try: report compare_docx_files(args.file_a, args.file_b) except FileNotFoundError as e: print(f{Fore.RED}错误找不到文件 {e.filename}{Style.RESET_ALL}) sys.exit(1) except Exception as e: print(f{Fore.RED}错误解析文件失败 — {str(e)}{Style.RESET_ALL}) sys.exit(1) # 输出到终端 print_terminal_report(report) # 输出 JSON if args.json: with open(args.json, w, encodingutf-8) as f: json.dump(report, f, indent2, ensure_asciiFalse) print(f\n{Fore.GREEN}✓ JSON 报告已保存至{args.json}{Style.RESET_ALL}) # 输出 HTML下一节实现 if args.html: generate_html_report(report, args.html) print(f{Fore.GREEN}✓ HTML 报告已保存至{args.html}{Style.RESET_ALL}) if __name__ __main__: main()使用方式# 安装依赖首次 pip install colorama # 直接运行 python word-diff.py contract_v1.docx contract_v2.docx # 生成 JSON 报告 python word-diff.py contract_v1.docx contract_v2.docx --json diff.json # 生成 HTML 报告需额外实现 generate_html_report 函数 python word-diff.py contract_v1.docx contract_v2.docx --html diff.html4.3 生成 HTML 可视化报告用 Jinja2 渲染结构化差异HTML 报告需突出显示结构性变更而非行级文本差异。设计原则左右分栏左侧file_a右侧file_b段落块用卡片展示新增/删除/修改分别用绿色/红色/蓝色边框修改块内用details折叠显示具体变更类型如paragraph_alignment_changed表格变更单独渲染为 mini-table 图形标出合并单元格。from jinja2 import Template def generate_html_report(report, output_path): 生成 HTML 可视化报告 html_template !DOCTYPE html html langzh-CN head meta charsetUTF-8 titleWord 结构比对报告/title style body { font-family: Segoe UI, sans-serif; margin: 40px; background: #f9f9f9; } .header { text-align: center; margin-bottom: 30px; } .summary { background: white; padding: 15px; border-radius: 5px; margin-bottom: 20px; } .block { margin: 15px 0; padding: 12px; border-radius: 4px; } .added { border-left: 4px solid #4CAF50; background: #f1f8e9; } .deleted { border-left: 4px solid #f44336; background: #ffebee; } .modified { border-left: 4px solid #2196F3; background: #e3f2fd; } .changes { margin-top: 8px; font-size: 0.9em; color: #555; } .details { margin-top: 8px; } table.mini { border-collapse: collapse; font-size: 0.8em; } table.mini td { border: 1px solid #ddd; padding: 3px 6px; } .merged { background: #ffeb3b; } /style /head body div classheader h1 Word 文件结构比对报告/h1 p基于 Open XML 结构解析聚焦语义级变更/p /div div classsummary h2 概览/h2 p✅ 基准文件strong{{ file_a }}/strong/p p✅ 待比对文件strong{{ file_b }}/strong/p p 总变更数strong{{ total_changes }}/strong 处/p p➕ 新增strong{{ report.added|length }}/strong 处 | ➖ 删除strong{{ report.deleted|length }}/strong 处 | ✏️ 修改strong{{ report.modified|length }}/strong 处/p /div {% if report.added %} h2 新增段落{{ report.added|length }} 处/h2 {% for item in report.added %} div classblock added strong位置 {{ item.index_in_b 1 }}/strong{{ item.text_preview }} /div {% endfor %} {% endif %} {% if report.deleted %} h2 删除段落{{ report.deleted|length }} 处/h2 {% for item in report.deleted %} div classblock deleted strong位置 {{ item.index_in_a 1 }}/strong{{ item.text_preview }} /div {% endfor %} {% endif %} {% if report.modified %} h2 修改段落{{ report.modified|length }} 处/h2 {% for item in report.modified %} div classblock modified strong位置 A{{ item.index_in_a 1 }} → B{{ item.index_in_b 1 }}/strong{{ item.text_preview }} div classchanges details classdetails summary▸ 查看具体变更/summary ul {% for change in item.changes %} li{{ change }}/li {% endfor %} /ul /details /div /div {% endfor %} {% endif %} /body /html template Template(html_template) html_content template.render( file_acontract_v1.docx, file_bcontract_v2.docx, total_changeslen(report[added]) len(report[deleted]) len(report[modified]), reportreport ) with open(output_path, w, encodingutf-8) as f: f.write(html_content) # 在 CLI 中调用即可 # generate_html_report(report, args.html)此 HTML 报告的特点零依赖纯静态 HTML无需服务器双击即可在浏览器打开语义聚焦不渲染原始 XML只展示“段落位置变更类型”避免信息过载可扩展details标签支持折叠未来可加入点击跳转到原始 Word 位置需结合python-docx定位合规友好所有样式内联无外部 CSS/JS满足内网审计要求。5. 处理 Word 关闭卡顿与 .docx 解压异常的实战技巧5.1 当zipfile.ZipFile报错 “Bad CRC-32”修复损坏的 .docx 文件Word 关闭卡顿、异常退出常导致 .docx 文件写入不完整表现为 ZIP 校验失败。此时zipfile.ZipFile(docx_path, r)会抛出zipfile.BadZipFile: Bad CRC-32。这不是 Python 的问题而是文件物理损坏。不要重试或忽略错误而应主动修复import zipfile import shutil def repair_corrupted_docx(corrupted_path, backup_pathNone): 尝试修复损坏的 .docx 文件基于 ZIP 修复原理 策略复制未损坏的 ZIP 中央目录重建文件结构 if backup_path: shutil.copy2(corrupted_path, backup_path) # 先备份 # 尝试用 zip -FF 强制修复需系统安装 unzip import subprocess try: result subprocess.run( [zip, -FF, corrupted_path, --out, corrupted_path .fixed], capture_outputTrue, textTrue, timeout30 ) if result.returncode 0 and repaired in result.stdout: # 替换原文件 shutil.move(corrupted_path .fixed, corrupted_path) print(f✅ 已修复损坏的 .docx 文件{corrupted_path}) return True except (subprocess.TimeoutExpired, FileNotFoundError): pass # 纯 Python 回退方案尝试读取并跳过损坏条目 # 实际生产环境建议优先用 unzip -FF print(f⚠ 无法自动修复请用 WinRAR 或 7-Zip 手动修复 {corrupted_path}) return False # 在 extract_document_xml 前调用 def safe_extract_document_xml(docx_path): try: return extract_document_xml(docx_path) except zipfile.BadZipFile: if repair_corrupted_docx(docx_path): return extract_document_xml(docx_path) else: raise RuntimeError(f文件损坏且无法修复{docx_path})注意unzip -FF是 Linux/macOS 下最可靠的 ZIP 修复命令Windows 用户可下载7z.exe并调用7z x -y corrupted.docx尝试解压再重新打包。5.2 规避 Word 关闭慢导致的文件锁强制释放句柄在 Windows 上若 Word 进程未完全退出.docx文件可能被系统锁定Python 读取时报PermissionError: [WinError 32] 另一个程序正在使用此文件。这不是代码问题而是 OS 文件锁机制。不能靠time.sleep()等待而应主动检测并释放import os import time def wait_for_file_unlock(filepath, timeout10): 等待文件解锁超时则抛出异常 start_time time.time() while time.time() - start_time timeout: try: # 尝试以只读模式打开不写入 with open(filepath, rb): return True except PermissionError: time.sleep(0.5) raise RuntimeError(f文件仍被占用超时 {timeout} 秒{filepath}) # 在调用 extract_document_xml 前 def robust_compare(file_a, file_b): wait_for_file_unlock(file_a) wait_for_file_unlock(file_b) return compare_docx_files(file_a, file_b)5.3 针对.docx解压后document.xml的特殊规则处理命名空间与空白Open XML 规范允许 document.xml本文还有配套的精品资源点击获取