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

🤟致敬读者

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

📘博主相关


文章目录


📃文章前言

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



解决 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 复制代码
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ds:datastoreItem ds:itemID="{B1977F7D-...}" xmlns:ds="...">
  <ds:schemaRefs>
    <ds:schemaRef ds:uri="http://www.wps.cn/officeDocument/2013/wpsCustomData"/>
  </ds:schemaRefs>
</ds:datastoreItem>

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

xml 复制代码
<s:customData xmlns="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
  └─ <Override PartName="/customXml/itemProps1.xml" ContentType="...customXmlProperties+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 私有数据。

实现代码

python 复制代码
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}')

调用方式

python 复制代码
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].xmldocument.xml.rels 时,需要匹配完整的 XML 元素标签。注意:

python 复制代码
# ❌ 错误:[^/]* 会在 ContentType 属性值中的 / 处提前停止
r'<Override[^>]*PartName="..."[^/]*/>'

# ✅ 正确:[^>]* 只排除 >,允许属性值中包含 /
r'<Override[^>]*PartName="..."[^>]*/>'

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

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

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


六、验证方法

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

python 复制代码
import zipfile

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

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



📜文末寄语

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

相关推荐
2401_832365522 小时前
JavaScript中rest参数(...args)取代arguments的优势
jvm·数据库·python
Sirius.z2 小时前
第J3周:DenseNet121算法详解
python
2301_779622413 小时前
Go语言怎么用信号量控制并发_Go语言semaphore信号量教程【入门】
jvm·数据库·python
2301_766283443 小时前
c++如何将控制台输出保存到文件_cout重定向到txt【详解】
jvm·数据库·python
小康小小涵4 小时前
基于ESP32S3实现无人机RID模块底层源码编译
linux·开发语言·python
lzjava20244 小时前
Python的函数
开发语言·python
Awesome Baron5 小时前
skill、tool calling、MCP区别
开发语言·人工智能·python
测试员周周5 小时前
【AI测试系统】第4篇:告别硬编码!基于 Markdown + Python 的 Skill 引擎设计:让 AI 测试系统拥有无限扩展的“灵魂”
人工智能·python·测试
武帝为此5 小时前
【Selenium 屏幕截图】
python·selenium·测试工具