最近在研究一批历史 Word 文档的自动化迁移。我原来以为这件事很直接:把 .doc 转成 .docx,后面的脚本统一处理新格式就行。可是真正开始写脚本查文件时,我发现问题并不简单。
先搞懂两者的差别
.doc 通常指 Word 97–2003 二进制文档,内容分布在 OLE/CFB 的多个流里。 .docx 是 Office Open XML 文档,里面有 XML、关系文件和媒体资源,正常未加密时通常使用 ZIP 容器。微软对 Open XML 的说明也是按这种包、部件和关系来组织的。
DOCX 的好处在于,包里的各个部分能拆开看。脚本可以先确认里面有什么,再决定只取正文,还是继续处理样式、关系、图片、批注和页眉页脚。

DOC 转 DOCX的常见方法
旧 DOC 的常见做法还是先转换,再统一处理,但转换器要按环境和保真要求选。Windows 上如果有 Word COM,通常最接近 Word 自己保存的结果;macOS 或 Linux 批处理一般会用 LibreOffice headless。只想提取纯文本时,antiword、Apache Tika 或 Apache POI HWPF 也能派上用场,但它们不能替代真正的版式转换。
宏和嵌入对象要单独看。普通 .docx 不能保存 VBA 宏;如果旧文档的宏必须保留,目标格式就要重新评估 .docm,不能硬塞进 .docx。公式、OLE 对象、域、修订和字体也可能在转换时变化。

虽然都是.docx 后缀,但可能是三种不同的文件
有些 .docx 打开后是正常 ZIP 包,有些仍是 OLE/CFB 文件头,还有些只是旧 .doc 换了个名字。麻烦的是,看到二进制外壳也不能马上判定转换失败,因为密码加密的 .docx 仍然可能使用 OLE/CFB 容器。总之,就是两者傻傻分不清楚。
第一种是正常 OOXML。它通常能被 ZIP 工具识别,包内应有 [Content_Types].xml、_rels/.rels,Word 文档还要能沿关系找到主文档部件。仅仅“是 ZIP”还不够,因为任意 ZIP 文件都可以改名为 .docx。
第二种是加密或受保护的 OOXML。微软的 MS-OFFCRYPTO 规范要求相关 Data Spaces 结构使用 OLE compound file,并通过 EncryptionInfo、EncryptedPackage 等流保存加密信息与内容。这类文件是合法的 Office 文档,只是自动化处理前需要密码和解密步骤。
第三种是“伪转换”:旧 DOC 只改成了 .docx 后缀。它仍可能包含 WordDocument 与 0Table / 1Table 等旧二进制流。脚本如果直接交给 python-docx 或 zipfile,得到的只会是格式错误。

还有一个容易混淆的点:OOXML 确实存在 Strict 与 Transitional 两种一致性类别。ISO/IEC 29500 同时定义了这两类概念,Transitional 还包含帮助旧二进制文档高质量迁移的兼容特性。Strict/Transitional 说的是 OOXML 的一致性,不是两种不同的 DOCX 容器。
利用脚本判断文件类型
from pathlib import Path
from posixpath import normpath
from zipfile import ZipFile, is_zipfile
from xml.etree import ElementTree as ET
CFB_MAGIC = bytes.fromhex("D0 CF 11 E0 A1 B1 1A E1")
def has_word_main_part(package):
names = set(package.namelist())
if not {"[Content_Types].xml", "_rels/.rels"} <= names:
return False
root_rels = ET.fromstring(package.read("_rels/.rels"))
for rel in root_rels:
if rel.attrib.get("Type", "").endswith("/officeDocument"):
target = normpath(rel.attrib.get("Target", "").lstrip("/"))
return target.startswith("word/") and target in names
return False
def probe_word_file(path):
path = Path(path)
if is_zipfile(path):
with ZipFile(path) as package:
return "docx_ooxml" if has_word_main_part(package) else "zip_not_word_docx"
with path.open("rb") as file:
header = file.read(8)
if header == CFB_MAGIC:
return "ole_cfb_needs_stream_inspection"
return "unknown"
这段代码只负责把正常 ZIP 包、OLE/CFB 容器和其他文件分开,不会一次解决所有情况。进入 OLE/CFB 分支后,还要继续看内部流:有 EncryptedPackage 和 EncryptionInfo,先走解密;有 WordDocument 和 Table 流,走 DOC 转换;两组特征都没有,再放进异常队列。Apache POI 的 FileMagic 也是按文件头区分 OLE2 与 OOXML/ZIP,并不相信文件名。
DOCX 自动化处理 Tips
确认它是正常 OOXML 后,脚本才知道该去哪些部件里拿内容。正文通常在 word/document.xml,样式在 word/styles.xml,编号在 word/numbering.xml,图片在 word/media/。超链接、图片和嵌入对象通过 .rels 文件串起来。

而且还有最常见的两个坑,一个是把 document.xml 当成整份文档,另一个是直接对 XML 做字符串替换。页眉、页脚、脚注、尾注、批注和文本框可能都在别的部件里。肉眼看到的一个词,也可能被拆进多个 w:r / w:t 节点。微软的 Open XML SDK 文档也是按 package、parts 和 relationships 这套结构来讲 Word 文档的,段落内部再由 Run 和 Text 组成。
工具可以按需求往上加:只做包检查和简单 XML,就用 zipfile 加 xml.etree.ElementTree;常规段落、表格、样式和图片读写,用 python-docx;需要精细操作 OOXML、处理大文件或特殊节点,就换 lxml、Open XML SDK、docx4j。需要接受修订、更新域、验证分页或尽量保持版式,还是 Word 或 LibreOffice 更稳。

夜雨聆风