为什么 python-docx 打不开加密的 .docx,Word COM 却能打开
一、根本原因:两者打开文件的层级不同
| 工具 | 打开方式 | 看到的是什么 |
|---|---|---|
| python-docx | 直接按 ZIP + XML 解析 | 文件在磁盘上的原始字节 |
| Word COM | 调用 Word 应用打开 | Word 解密后内存中的文档对象 |
.docx 加密后,磁盘上的字节不再是标准 ZIP 包 ,而是一个 OLE 复合文档 (Compound File Binary Format),内部包含加密流。python-docx 直接把它当 ZIP 打开,自然读不到 word/document.xml,所以报 Package not found。
Word COM 走的是 Word 的完整加载流程:先识别 OLE 容器 → 找到加密流 → 提示输入密码(或使用已保存的凭据)→ 解密 → 在内存中构建文档对象 → 你的代码才能操作 doc.Tables。它根本不关心中间字节是什么格式。
二、加密 .docx 的真实结构
未加密 .docx:
PK\x03\x04 ... (ZIP 本地文件头)
├── [Content_Types].xml
├── _rels/.rels
├── word/document.xml
├── word/styles.xml
└── ...
加密 .docx(Office 标准加密):
D0 CF 11 E0 A1 B1 1A E1 ... (OLE Compound File 签名)
├── EncryptionInfo (加密算法、盐值、加密参数)
├── EncryptedPackage (被加密的原始 docx ZIP 字节流)
└── DataSpaces/...
所以:
- 文件头不是
PK,而是D0 CF 11 E0,这是判断加密与否最直接的标志。 - 整个原始 docx 被包在
EncryptedPackage里,AES 加密。 - 解密需要的密钥由用户密码派生,只有 Word/WPS 这类完整办公软件才知道怎么解。
python-docx 完全不知道 OLE 容器、加密算法、密钥派生这套机制,它只会在开头读 4 个字节,发现不是 PK 就报错。
三、python-docx 的加载流程
Document(path) 内部大致做这些事:
- 用
zipfile.ZipFile(path)打开文件。 - 检查是否为有效 ZIP。
- 读取
[Content_Types].xml。 - 读取
word/document.xml构建Document对象。
只要第 1 步失败,就会抛出 PackageNotFoundError(旧版是 BadZipFile 或 KeyError)。它没有"解密"这一步,也没有密码接口。
官方文档明确说:python-docx 只处理 Office Open XML 格式,不支持加密文档。
四、Word COM 为什么可以
win32com.client.DispatchEx("Word.Application") 启动的是完整的 Word 进程,你调的 Documents.Open() 是 Word 自己的加载逻辑:
- 识别文件格式(OLE 容器 vs 普通 ZIP)。
- 如果是加密文档,弹出密码对话框,或用已保存的凭据。
- 解密
EncryptedPackage。 - 把解密后的字节当作 docx 处理。
- 构建内存中的
Document对象。
你拿到的 doc 是 Word 内存模型,和磁盘上的加密文件无关。保存时 Word 再把内存模型序列化回磁盘,是否加密取决于 SaveAs 的参数。
WPS 的 COM 接口也是同样的机制,只是 ProgID 换成 kwps.Application。
五、实操中的判断与处理
1. 判断文件是否加密
读前 4 个字节:
PK\x03\x04→ 标准 ZIP,python-docx 可用。D0 CF 11 E0→ OLE 容器,可能是加密 docx、也可能是老式.doc。- 其他 → 损坏或非 Word 文件。
可以进一步检查 OLE 容器中是否含 EncryptionInfo 和 EncryptedPackage 流,来区分"加密 docx"和"老 .doc"。
2. 处理方式
| 场景 | 方案 |
|---|---|
| 有密码,且环境有 Word | 用 Word COM,Documents.Open(path, PasswordDocument="...") |
| 有密码,环境无 Word | 用 msoffcrypto-tool 解密后临时文件,再交给 python-docx |
| 无密码 | 只能人工处理,或跳过 |
| 批量 | 优先 Word COM;无 Word 时用 msoffcrypto-tool;都失败则跳过并记录 |
3. 用 msoffcrypto-tool 解密
bash
pip install msoffcrypto-tool
python
import msoffcrypto
from io import BytesIO
with open("encrypted.docx", "rb") as f:
office = msoffcrypto.OfficeFile(f)
office.load_key(password="your_password")
buf = BytesIO()
office.decrypt(buf)
with open("decrypted.docx", "wb") as f:
f.write(buf.getvalue())
# 之后再用 python-docx 打开 decrypted.docx
4. 结合到你的工具
现有代码里 is_valid_docx_package 已经检测到"文件头不是 PK",可以在此基础上:
- 若头是
D0 CF 11 E0,进一步区分是加密 docx 还是老 .doc:- 用
olefile列出流名,含EncryptionInfo→ 加密 docx; - 否则 → 老 .doc 或未知 OLE。
- 用
- 加密 docx 时:
- 若有 Word COM 且 UI 提供了密码输入,走 COM 打开;
- 否则尝试 msoffcrypto-tool;
- 都失败则标记
skipped,提示用户"文件已加密,请提供密码或用 Word 另存为未加密版本"。
六、为什么之前 Word COM 修复反而能成功
之前 try_repair_with_word_com 里调用的就是 Word 自己的 Documents.Open:
python
doc = word.Documents.Open(path)
doc.SaveAs2(tmp_path, FileFormat=WD_FORMAT_XML_DOCUMENT)
如果打开时 Word 记忆了密码(例如文档来自你最近用 Word 打开过),或密码为空,它就能解密并另存为未加密 docx。然后 python-docx 再打开这个临时文件就没问题了。
但如果:
- Word 没记忆密码;
- 或弹出的密码对话框被
DisplayAlerts=0抑制,Documents.Open直接失败; - 或文档用的是 WPS 的私有加密算法,Word 不认;
那修复就会失败,此时会走到 InvalidDocxError,提示用户手动处理。
七、一句话总结
| python-docx | Word COM | |
|---|---|---|
| 打开层级 | 直接解析磁盘字节(ZIP + XML) | 调用完整 Office 应用,含解密 |
| 加密 docx 表现 | 不是 ZIP,直接报 Package not found |
识别 OLE → 解密 → 提供 Document 对象 |
| 能否读密码 | 不能 | 能(PasswordDocument 参数或对话框) |
| 能否解密 | 不能 | 能 |
| 替代方案 | msoffcrypto-tool 先解密 |
有 Word 环境时首选 |
所以:python-docx 只处理未加密的 OOXML 文件;加密文档必须交给能解密的一方------Word/WPS 的 COM,或 msoffcrypto-tool 这类专用解密库------处理完之后再交回 python-docx。