乐于分享
好东西不私藏

解决 python-docx 生成的 Word 文档打开时弹出“无法读取内容“警告

解决 python-docx 生成的 Word 文档打开时弹出“无法读取内容“警告

🤟致敬读者

  • 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉

📘博主相关

  • 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息

文章目录

  • 解决 python-docx 生成的 Word 文档打开时弹出”无法读取内容”警告
    • 一、问题现象
    • 二、排查过程
      • 第一步:常规 XML 结构检查
      • 第二步:检查 `customXml` 目录
      • 第三步:确认完整关系链
    • 三、根本原因
    • 四、解决方案
      • 实现代码
      • 调用方式
    • 五、方案要点说明
      • 为什么不直接修改模板文件?
      • ZIP 重打包的注意事项
      • 正则表达式中 `[^>]*` 的选择
      • 只清理 WPS 私有项,保留其他 customXml
    • 六、验证方法
    • 七、总结

📃文章前言

  • 🔷文章均为学习工作中整理的笔记。
  • 🔶如有错误请指正,共同学习进步。



解决 python-docx 生成的 Word 文档打开时弹出”无法读取内容”警告

背景:使用 python-docx 基于 WPS 创建的模板生成 .docx 报告文件,用 Microsoft Word 打开时弹出以下警告:

“Word 在 report_xxx.docx 中发现无法读取的内容。是否恢复此文档的内容?如果您信任此文档的来源,请单击”是”。”


一、问题现象

程序运行正常,生成的 .docx 文件内容完整,但每次用 Microsoft Word 打开都会弹出”无法读取内容”警告弹窗。点击”是”之后文档可以正常显示,但这个警告严重影响用户体验,并暗示文件存在潜在的格式问题。


二、排查过程

第一步:常规 XML 结构检查

首先怀疑是 python-docx 生成的 XML 本身存在格式问题,逐一检查:

  • word/document.xml
     的 body 结构
  • 颜色值格式(#000000 vs 000000
  • 图片关系引用
  • 样式引用完整性
  • 书签配对完整性
  • 表格 XML 结构(tblPr/tblGrid/tblW

结果:全部通过,未发现异常。

插曲:检查过程中确实发现了一个 #000000 颜色值格式问题——python-docx 要求颜色值不含 # 前缀(应为 000000),修复后警告依然存在,说明这不是根本原因。

第二步:检查 customXml 目录

将 .docx 文件作为 ZIP 包解压,进入 customXml/ 目录检查:

customXml/├── item1.xml├── item2.xml├── _rels/│   ├── item1.xml.rels│   └── item2.xml.rels├── itemProps1.xml   ← 关键文件└── itemProps2.xml

打开 customXml/itemProps1.xml,发现了问题所在:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><ds:datastoreItemds:itemID="{B1977F7D-...}"xmlns:ds="...">  <ds:schemaRefs>    <ds:schemaRefds:uri="http://www.wps.cn/officeDocument/2013/wpsCustomData"/>  </ds:schemaRefs></ds:datastoreItem>

以及 customXml/item1.xml 中的私有数据:

<s:customDataxmlns="http://www.wps.cn/officeDocument/2013/wpsCustomData">  <!-- WPS 私有扩展信息:形状、段落属性等 --></s:customData>

第三步:确认完整关系链

word/_rels/document.xml.rels  └─ rId12 → ../customXml/item1.xml   (WPS 私有数据)  └─ rId14 → ../customXml/item2.xml   (APA 书目,正常)[Content_Types].xml  └─ 

三、根本原因

模板文件由 WPS(金山 Office)创建,WPS 会在 .docx 文件中嵌入私有的 wpsCustomData XML 数据,其 schema URI 为:

http://www.wps.cn/officeDocument/2013/wpsCustomData

当 python-docx 加载该模板并保存新文档时,这份 WPS 私有 customXml 数据会被完整保留到输出文件中。

Microsoft Word 打开文档时会尝试验证所有 customXml 的 schema 引用。由于 wpsCustomData 是 WPS 的私有 schema,Microsoft Word 无法找到或识别它,因此触发”无法读取内容”警告。


四、解决方案

在 doc.save() 之后,直接对生成的 ZIP 包(即 .docx 文件)进行处理,移除 WPS 私有数据。

实现代码

def clean_wps_custom_data(docx_path):    """    从生成的 docx 文件中移除 WPS 私有 customXml 数据。    模板由 WPS 创建时会嵌入 wpsCustomData 私有 XML,其 schema URI 为    http://www.wps.cn/officeDocument/2013/wpsCustomData,Microsoft Word    无法识别此私有 schema,导致打开时弹出"无法读取内容"警告。    此函数在 doc.save() 之后运行,直接对 ZIP 包进行手术,移除该私有数据。    """    import zipfile, re, io    buffer = io.BytesIO()    try:        with zipfile.ZipFile(docx_path, 'r'as z_in:            with zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_DEFLATED) as z_out:                file_list = z_in.namelist()                # 第一步:找出含有 WPS schema 引用的 itemProps 文件编号                wps_item_nums = set()                for fname in file_list:                    if fname.startswith('customXml/itemProps'and fname.endswith('.xml'):                        with z_in.open(fname) as f:                            content = f.read().decode('utf-8', errors='replace')                        if 'wps.cn' in content or 'wpsCustomData' in content:                            m = re.search(r'itemProps(\d+)\.xml', fname)                            if m:                                wps_item_nums.add(m.group(1))                # 第二步:构建需要整体删除的文件集合                files_to_delete = set()                for num in wps_item_nums:                    files_to_delete.add(f'customXml/item{num}.xml')                    files_to_delete.add(f'customXml/itemProps{num}.xml')                    files_to_delete.add(f'customXml/_rels/item{num}.xml.rels')                if not wps_item_nums:                    print('clean_wps_custom_data: 未发现 WPS 私有数据,无需清理')                    return                print(f'clean_wps_custom_data: 发现 WPS 私有数据(item 编号 {wps_item_nums}),正在清理...')                # 第三步:逐文件处理,重新打包 ZIP                for fname in file_list:                    if fname in files_to_delete:                        continue  # 跳过 WPS 私有文件,不写入新 zip                    with z_in.open(fname) as f:                        data = f.read()                    if fname == '[Content_Types].xml':                        # 删除 WPS itemProps 的 Override 声明                        content = data.decode('utf-8')                        for num in wps_item_nums:                            content = re.sub(                                r'<Override[^>]*PartName="/customXml/itemProps' + num + r'\.xml"[^>]*/>''',                                content                            )                        z_out.writestr(fname, content.encode('utf-8'))                    elif fname == 'word/_rels/document.xml.rels':                        # 删除指向 WPS item 文件的 Relationship 条目                        content = data.decode('utf-8')                        for num in wps_item_nums:                            content = re.sub(                                r'<Relationship[^>]*Target="\.\./customXml/item' + num + r'\.xml"[^>]*/>''',                                content                            )                        z_out.writestr(fname, content.encode('utf-8'))                    else:                        z_out.writestr(fname, data)        # 将处理后的内容写回原文件        buffer.seek(0)        with open(docx_path, 'wb'as f:            f.write(buffer.read())        print('clean_wps_custom_data: WPS 私有数据清理完成')    except Exception as e:        print(f'clean_wps_custom_data: 清理失败(不影响文档内容): {e}')

调用方式

doc = Document(template_path)# ... 填充内容 ...doc.save(output_path)clean_wps_custom_data(output_path)   # ← 紧接在 save 之后调用

五、方案要点说明

为什么不直接修改模板文件?

可以手动用 Microsoft Word 打开模板并重新保存,以去除 WPS 私有数据。但这种方式存在风险:

  • 每次模板更新后都需要手动处理
  • 在自动化流水线中无法保证

因此选择在代码层面自动清理,更加健壮。

ZIP 重打包的注意事项

.docx 本质上是一个 ZIP 文件。修改其内容的正确方式是:

  1. 读取原 ZIP 到内存(io.BytesIO
  2. 创建新 ZIP,逐文件写入(跳过或修改目标文件)
  3. 将新 ZIP 的内容写回原路径

不要直接在原 ZIP 上增删文件(Python 的 zipfile 模块不支持原地删除),否则会损坏文件结构。

正则表达式中 [^>]* 的选择

处理 [Content_Types].xml 和 document.xml.rels 时,需要匹配完整的 XML 元素标签。注意:

# ❌ 错误:[^/]* 会在 ContentType 属性值中的 / 处提前停止r'<Override[^>]*PartName="..."[^/]*/>'# ✅ 正确:[^>]* 只排除 >,允许属性值中包含 /r'<Override[^>]*PartName="..."[^>]*/>'

ContentType="application/vnd.openxmlformats-officedocument.customXmlProperties+xml" 中包含 /,必须使用 [^>]* 才能正确匹配到 />

只清理 WPS 私有项,保留其他 customXml

文档中可能存在合法的 customXml 数据(如 APA 书目数据),清理时通过检测 wps.cn 域名精确定位,只删除 WPS 私有项,不影响其他数据。


六、验证方法

用 Python 对清理前后的文件进行验证:

import zipfiledef verify_clean(docx_path):    with zipfile.ZipFile(docx_path, 'r'as z:        file_list = z.namelist()        # 1. 检查是否还有 WPS schema 引用        for fname in file_list:            if fname.startswith('customXml/itemProps'and fname.endswith('.xml'):                with z.open(fname) as f:                    content = f.read().decode('utf-8', errors='replace')                if 'wps.cn' in content or 'wpsCustomData' in content:                    print(f"⚠ 仍有 WPS schema: {fname}")                    return False        # 2. 检查 document.xml.rels        with z.open('word/_rels/document.xml.rels'as f:            rels = f.read().decode('utf-8')        if 'wpsCustomData' in rels or ('customXml' in rels and 'wps.cn' in rels):            print("⚠ document.xml.rels 中仍有 WPS 引用")            return False        print("✓ 清理验证通过,无 WPS 私有数据残留")        return True

七、总结

项目
内容
问题根因
WPS 创建的模板内嵌私有 wpsCustomData schema,Microsoft Word 无法识别
触发条件
用 python-docx 加载 WPS 模板并保存,私有数据被保留到输出文件
修复方式 doc.save()

 后对 ZIP 包进行后处理,删除 WPS 私有文件并清理引用
清理范围 customXml/item{n}.xml

customXml/itemProps{n}.xmlcustomXml/_rels/item{n}.xml.rels[Content_Types].xml 中的 Override 声明、word/_rels/document.xml.rels 中的 Relationship 条目
副作用
无,不影响文档内容和其他合法 customXml 数据

适用场景:所有使用 python-docx 基于 WPS 创建的模板生成 Word 文档的场景,均可能遇到此问题,使用本文的清理方案可彻底解决。



📜文末寄语

  • 🟠关注我,解锁更多优质内容
  • 🟡技术前沿 | 实战干货 | 疑难解答,持续更新中
  • 🟢加入《全栈知识库》,与各领域开发者共创技术盛宴
  • 🔵进入《专属社群》,技术路上结伴同行,共同成长
  • 🟣点击下方名片,获取更多精彩内容👇