ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python 导出 Word 文档图片与嵌入文件:基于 zip 解析的自动化方案

Python 导出 Word 文档图片与嵌入文件:基于 zip 解析的自动化方案

简介:这份资源面向需要批量处理 Word 文档的 Python 开发者与办公自动化实践者,聚焦于从 .docx 文件中提取图片与 OLE 嵌入对象(如内嵌表格、PDF 等)这一常见却不易实现的场景,适合具备一定 Python 基础、希望掌握文档深度解析技能的中级读者。压缩包共 2033 个文件,约 12.42MB,以 908 个 py 源码与 880 个 pyc 字节码为主体,辅以 C 扩展头文件、pyd 动态库、exe 可执行文件及少量 docx 示例文档、html 与 md 说明,并包含 venv 虚拟环境与 IDE 配置,开箱即可运行调试。已有 1198 人学习下载。资源围绕 docx 库遍历文档部件、解析关系文件定位图片二进制流,并结合 oletools 等工具解析 OLE 结构导出嵌入文件,脚本分工清晰,便于读者直接复用导出逻辑、理解 Word 内部存储机制,并在此基础上扩展批量处理与内容迁移能力。

1. 从一份“图文混排”的 Word 里把资源抠出来

上周同事丢过来一个 200 多页的产品手册,Word 里塞了几十张截图和一堆内嵌的 Excel 附件,要求把图片和附件全部导出来归档。手动右键另存?点到第十张就想砸键盘。这类需求在文档处理、内容迁移、素材归档场景里非常常见,而 Python 配合python-docx和zipfile就能把这件事自动化。这篇笔记拆的是一个「导出 Word 文档中所有图片与嵌入文件」的完整实现思路:它适合需要批量处理 docx 的后端开发者、做文档解析的数据工程师,也适合刚学 Python 想找一个真实小项目练手的人。核心结论先说:docx 本质是个 zip 包,图片和嵌入对象都躺在包内的固定目录里,找对路径就能一锅端。

2. 技术选型与 docx 包结构:为什么不用 COM 而直接拆 zip

2.1 两条技术路线的取舍

处理 Word 文档,Python 圈子里常见两条路。一条是走 Windows COM 接口,调用本机安装的 Word 程序,用win32com.client打开文档再遍历InlineShapes。这条路能拿到渲染后的效果,但强依赖 Windows 和 Office 环境,服务器上跑不了,速度也慢,一个文档开一次 Word 进程,批量处理时资源占用很难看。另一条路是把 docx 当成 zip 包直接解析,纯 Python、跨平台、无外部依赖,速度快几个数量级。

这个项目选的是第二条路。原因很直接:导出图片和嵌入文件这个需求,根本不需要 Word 的渲染能力,只需要知道文件在包里的位置。docx 从 Office 2007 开始就是 OOXML 格式,扩展名换了,骨子里是个 zip。用zipfile打开它,namelist()一列,目录结构清清楚楚。

提示:老式的.doc二进制格式不适用这套方法,必须先转成.docx。批量场景下可以用 LibreOffice 命令行做一次转换,再进入解析流程。

2.2 docx 内部目录长什么样

一个典型的 docx 解压后,和资源导出相关的目录有这么几个:

路径内容是否本项目关注
word/media/文档中插入的图片,如image1.png是
word/embeddings/嵌入的 OLE 对象,如内嵌 Excel、PDF是
word/document.xml正文结构,图片通过关系 ID 引用辅助定位
word/_rels/document.xml.rels关系映射表,ID 对应真实文件路径辅助定位
word/styles.xml样式定义否

图片统一落在word/media/,嵌入对象统一落在word/embeddings/,这是 OOXML 规范定死的。所以最朴素的导出逻辑就是:打开 zip,遍历这两个前缀下的所有条目,逐个读出来写到本地目录。不需要解析 XML,不需要理解关系表,稳得很。

2.3 环境准备与依赖

项目依赖极简,标准库就够,python-docx只在需要读取图片在正文中的顺序或上下文时才引入:

pip install python-docx

zipfile、os、pathlib都是标准库自带。Python 版本建议 3.8 以上,pathlib的用法更顺手。整个项目不需要 Office,不需要 Windows,Linux 服务器上直接跑。

3. 核心导出逻辑:从 zip 条目到本地文件

3.1 最小可用版本:遍历 media 与 embeddings

先把能跑通的主干写出来。这段代码是项目的核心,后面所有增强都围绕它展开:

import zipfile from pathlib import Path def export_docx_assets(docx_path: str, out_dir: str) -> dict: """导出 docx 中的图片与嵌入文件""" docx_path = Path(docx_path) out_dir = Path(out_dir) # 图片和嵌入对象分目录存放,避免同名覆盖 img_dir = out_dir / "images" emb_dir = out_dir / "embeddings" img_dir.mkdir(parents=True, exist_ok=True) emb_dir.mkdir(parents=True, exist_ok=True) result = {"images": [], "embeddings": []} with zipfile.ZipFile(docx_path, "r") as zf: for name in zf.namelist(): # 只处理两个目标目录,其余 XML 一律跳过 if name.startswith("word/media/"): target = img_dir / Path(name).name target.write_bytes(zf.read(name)) result["images"].append(str(target)) elif name.startswith("word/embeddings/"): target = emb_dir / Path(name).name target.write_bytes(zf.read(name)) result["embeddings"].append(str(target)) return result

逐段说清楚。zipfile.ZipFile以只读模式打开 docx,namelist()返回包内所有条目路径,形如word/media/image1.png。startswith做前缀过滤,只留两个目标目录。Path(name).name取文件名部分,丢掉目录前缀,写到本地对应子目录。write_bytes直接写二进制,图片和 OLE 对象都是二进制,不需要解码。返回值用字典记录导出了哪些文件,方便上层做统计或日志。

参数上,docx_path传绝对或相对路径都行,out_dir不存在会自动创建。这个版本已经能覆盖 90% 的场景,图片和附件都能完整抠出来。

3.2 处理重名与格式识别

word/media/里的文件名是 Word 自动生成的image1.png、image2.jpeg这种,一般不会重名。但word/embeddings/里的 OLE 对象经常叫oleObject1.bin、oleObject2.bin,扩展名是.bin,直接导出来用户根本不知道里面是什么。常见做法是读 OLE 文件头判断真实类型,或者从关系表里反查原始文件名。

判断 OLE 类型可以看文件头魔数,Excel 的 OLE 复合文档头是D0 CF 11 E0,PDF 是%PDF:

def guess_embed_type(data: bytes) -> str: """根据文件头猜测嵌入对象的真实类型""" if data.startswith(b"%PDF"): return ".pdf" if data.startswith(b"\xd0\xcf\x11\xe0"): return ".ole" # 复合文档,可能是 Excel/Word if data.startswith(b"PK\x03\x04"): return ".zip" # 新版 Office 内嵌也是 zip 结构 return ".bin"

这段逻辑放在写文件之前,把oleObject1.bin改名为oleObject1.ole或按魔数改成.pdf,归档时可读性会好很多。注意.ole复合文档内部可能还套着 Excel 工作簿,真要还原成.xlsx得用olefile库进一步解析,这属于进阶需求,基础导出不必做。

重名问题在批量处理多个文档时才会出现。如果一次导出多个 docx 到同一个输出目录,image1.png会互相覆盖。解决办法是在输出路径里带上源文档名做前缀,或者每个文档一个子目录。我一般用后者,结构清晰,回溯方便。

3.3 用 python-docx 拿到图片的正文顺序

纯 zip 遍历有个短板:导出的图片是乱序的,按包内条目顺序来,和文档里实际出现的先后对不上。如果归档时需要保持阅读顺序,就得借助python-docx解析正文,按段落顺序找图片引用。

from docx import Document def iter_images_in_order(docx_path: str): """按正文出现顺序产出图片的关系 ID""" doc = Document(docx_path) for rel in doc.part.rels.values(): # 只挑图片类型的关系 if "image" in rel.reltype: yield rel.rId, rel.target_ref

doc.part.rels是文档级关系表,reltype里含image的就是图片关系,target_ref指向media/image1.png这样的包内路径。拿到这个映射,再和 zip 里的实际文件对应,就能按正文顺序重命名导出文件,比如img_001.png、img_002.png。这一步不是必须的,但做素材归档时很值。

注意:python-docx对嵌入的 OLE 对象支持有限,embeddings目录还是得靠 zip 遍历拿,别指望python-docx的 API 全覆盖。

4. 避坑与排查:那些让我返工的细节

4.1 导出后图片打不开,文件大小是 0

现象:脚本跑完没报错,但导出的图片全是 0 字节。原因多半是用了zf.open(name)拿到文件对象后没读完就关了,或者用文本模式读了二进制。解决:统一用zf.read(name)一次性读全量再write_bytes,别在 zip 条目上做流式文本操作。二进制资源一律走 bytes 路径。

4.2 某些文档根本没有 media 目录

现象:对一份纯文字文档跑脚本,namelist()里找不到word/media/,代码逻辑没错但结果为空,容易误判成 bug。原因:文档里确实没插图片,zip 包不会创建空目录。解决:遍历前先判断目录是否存在,结果为空时正常返回,日志里标注「无图片资源」,不要抛异常。

4.3 嵌入的 Excel 导出来是 .bin 打不开

现象:embeddings里导出的oleObject1.bin双击没反应。原因:OLE 复合文档是容器格式,.bin只是原始字节,Windows 靠注册表关联才能打开。解决:按 3.2 的魔数判断改扩展名,或者用olefile解析出内部流再另存。归档场景下改成.ole并附一份说明文件,比硬还原更省事。

4.4 中文路径在 Windows 上写入失败

现象:输出目录带中文,write_bytes报FileNotFoundError。原因:老版本 Python 在 Windows 上对非 ASCII 路径处理有坑,或者路径拼接用了字符串加法导致分隔符错乱。解决:全程用pathlib.Path做路径运算,别手拼字符串;Python 3.8+ 基本没这问题,但团队里混用版本时要注意统一。

4.5 大文档导出慢、内存飙高

现象:几百 MB 的 docx,zf.read()一次性读进内存,进程内存直接翻倍。原因:全量读入。解决:图片和附件通常单个体积不大,逐个read写完即释放引用即可;如果确实有超大嵌入对象,改用zf.open(name)配合shutil.copyfileobj分块写,控制峰值内存。

5. 二次开发:把它做成可复用的导出工具

基础脚本跑通后,真正落地时我会把它包成一个带命令行入口的小工具,而不是每次改路径。用argparse接参数,支持单文件和目录批量:

import argparse from pathlib import Path def main(): parser = argparse.ArgumentParser(description="导出 docx 中的图片与嵌入文件") parser.add_argument("src", help="docx 文件或包含 docx 的目录") parser.add_argument("-o", "--out", default="./exported", help="输出根目录") args = parser.parse_args() src = Path(args.src) targets = [src] if src.is_file() else list(src.glob("*.docx")) for doc in targets: # 每个文档独立子目录,避免重名覆盖 sub = Path(args.out) / doc.stem stats = export_docx_assets(str(doc), str(sub)) print(f"{doc.name}: 图片 {len(stats['images'])} 个, 附件 {len(stats['embeddings'])} 个") if __name__ == "__main__": main()

这个入口的价值在于批量。src.glob("*.docx")扫目录,每个文档一个子目录,统计信息直接打印,跑完一眼看出哪份文档资源多。改造方向上还有几个值得做的点:一是加--rename-by-order开关,用 3.3 的顺序信息重命名图片;二是把结果写成 JSON 清单,记录源文档、导出路径、文件类型,方便后续入库;三是接一个简单的 Web 上传接口,用 Flask 包一层,非技术同事也能用。

验证方法很朴素:拿一份自己造的测试文档,里面故意放 PNG、JPEG 各一张,内嵌一个 Excel 和一个 PDF,跑完对比导出数量和文件能否正常打开。我吃过亏——早期版本没处理.bin扩展名,交付时对方打开一堆无后缀文件直接懵了。从那以后我每次做资源导出,都强制先跑一遍「类型识别 + 数量核对」再交付。希望这个思路帮到你,拿到源码后先跑最小版本,再按需往上叠功能,别一上来就追求大而全。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进