
简介这份资源面向需要批量处理 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 而直接拆 zip2.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-docxzipfile、os、pathlib都是标准库自带。Python 版本建议 3.8 以上pathlib的用法更顺手。整个项目不需要 Office不需要 WindowsLinux 服务器上直接跑。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(parentsTrue, exist_okTrue) emb_dir.mkdir(parentsTrue, exist_okTrue) 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以只读模式打开 docxnamelist()返回包内所有条目路径形如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 E0PDF 是%PDFdef 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(bPK\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_refdoc.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 的 docxzf.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, helpdocx 文件或包含 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扩展名交付时对方打开一堆无后缀文件直接懵了。从那以后我每次做资源导出都强制先跑一遍「类型识别 数量核对」再交付。希望这个思路帮到你拿到源码后先跑最小版本再按需往上叠功能别一上来就追求大而全。本文还有配套的精品资源点击获取