简介这份资源面向需要用Python自动化处理Word文档的开发者与办公自动化爱好者聚焦于从.docx文件中批量导出内嵌图片与OLE嵌入文件这一常见却不易实现的场景。包内共2033个文件以908个py源码、880个pyc字节码为主辅以C扩展头文件、依赖元数据、可执行程序与说明文档压缩包约12.42MB其中docx_export.py与main.py承担核心导出逻辑虚拟环境与IDE配置也已一并打包开箱即可运行。资源围绕docx库解析文档关系、定位图片二进制数据并结合oletools等工具解析OLE结构提取嵌入的表格、PDF等对象形成完整的导出工作流。已有1197人学习下载适合希望掌握文档内容自动化提取、迁移与整理技巧的读者参考实践。1. 从一份 200 页的 Word 报告里把图抠出来为什么我最后选了 python-docx上周帮某实验室整理一份 200 多页的验收报告对方要我把里面所有插图和嵌入的附件单独导出来归档。我第一反应是手动另存为网页结果图片全糊了嵌入的 Excel 附件直接消失。后来试了 python-docx才发现 Word 的图片和嵌入文件根本不是存在文档正文里的而是藏在word/media/和word/embeddings/这两个目录下。这个资源就是围绕这个场景来的用 Python 把 .docx 里的图片和嵌入对象批量导出适合需要做文档归档、素材提取、报告拆解的从业者。它不处理 .doc 老格式也不做 OCR就是纯粹的解包和落盘。如果你手上有一堆 docx 要拆素材这套思路能省掉大量重复劳动。2. 拆开 docx 之前先搞懂它到底是个什么结构2.1 docx 本质是个 zip不是二进制黑匣子很多人以为 .docx 是 Word 专属的二进制格式其实从 Office 2007 开始它就是一个标准的 ZIP 压缩包里面按目录结构存放 XML 和资源文件。你把任意一个 .docx 的后缀改成 .zip用解压工具打开就能看到完整的内部结构。这个认知很关键因为它决定了后面所有操作的本质我们不是在“解析 Word”而是在“解压一个特定结构的压缩包再按需读取里面的资源”。常见的内部目录长这样路径存放内容word/document.xml正文内容段落、表格、文字都在这里word/media/所有嵌入的图片通常命名为 image1.png、image2.jpegword/embeddings/嵌入的 OLE 对象比如内嵌的 Excel、PDF、另一个 docxword/_rels/document.xml.rels关系映射表记录每个资源被谁引用[Content_Types].xml声明各扩展名对应的 MIME 类型图片和嵌入文件不会出现在 document.xml 的正文里正文里只有引用 ID真正的二进制数据在 media 和 embeddings 目录下。所以导出图片这件事本质上就是遍历这两个目录把文件复制出来。2.2 为什么不用 python-docx 的 API 直接取图python-docx 提供了document.inline_shapes可以拿到内联图片但它有两个硬伤第一它只能拿到内联图片浮动图片比如设置了文字环绕的拿不到第二它返回的是图片的引用关系不是原始二进制流遇到 EMF、WMF 这类矢量格式时经常报错。我一开始也图省事用 API结果一份报告里 30 张图只导出了 18 张剩下的全是浮动排版。后来改成直接操作 zip 包问题一次性解决。常见做法是用zipfile模块打开 docx先列出word/media/下所有文件再逐个读取字节流写出去。嵌入文件同理遍历word/embeddings/即可。这样做的好处是不依赖 python-docx 的解析逻辑只要文件是合法的 docx就能把资源完整抠出来。2.3 环境准备与最小依赖这个方案对环境的依赖极低Python 3.6 以上即可不需要装 Word也不需要 COM 组件。唯一用到的就是标准库里的zipfile和os如果你还想顺便解析一下图片在文档里的位置可以加装 python-docx但导出这一步完全不需要。# 建议用虚拟环境避免污染全局 python -m venv docx_env source docx_env/bin/activate # Windows 下用 docx_env\Scripts\activate # 核心依赖只有标准库python-docx 是可选的 pip install python-docx这里要说明一下zipfile是 Python 内置的不用 pip 装。我列 python-docx 是因为后面排查图片归属时会用到纯导出的话可以不装。虚拟环境这一步别省我见过有人直接在系统 Python 里跑结果和已有的 docx 库版本冲突报了一堆ImportError排查半天。3. 动手导出图片和嵌入文件的完整脚本3.1 导出所有图片遍历 media 目录先写最核心的图片导出逻辑。思路很简单打开 docx 作为 zip找到所有以word/media/开头的条目逐个读出来写到目标目录。import zipfile import os def export_images(docx_path, output_dir): 从 docx 中导出所有图片 :param docx_path: docx 文件路径 :param output_dir: 图片输出目录 :return: 导出的图片文件名列表 # 确保输出目录存在exist_okTrue 避免重复创建报错 os.makedirs(output_dir, exist_okTrue) exported [] # 以只读方式打开 zipdocx 本质就是 zip with zipfile.ZipFile(docx_path, r) as z: # namelist() 返回压缩包内所有文件路径 for name in z.namelist(): # 只处理 media 目录下的文件其余跳过 if name.startswith(word/media/): # 取原始文件名如 image1.png filename os.path.basename(name) # 跳过目录条目本身有些 zip 会包含目录项 if not filename: continue # 读取二进制数据并写入目标文件 data z.read(name) out_path os.path.join(output_dir, filename) with open(out_path, wb) as f: f.write(data) exported.append(filename) return exported # 调用示例 if __name__ __main__: imgs export_images(report.docx, ./output/images) print(f共导出 {len(imgs)} 张图片) for i in imgs: print(i)逻辑说明zipfile.ZipFile以只读模式打开 docxnamelist()拿到内部所有路径。判断前缀word/media/是过滤关键因为 docx 里还有word/theme/等目录也可能有图片但那些是主题资源不是正文插图。os.path.basename取文件名是为了去掉目录前缀避免写出word/media/image1.png这种嵌套路径。写入用wb二进制模式文本模式会损坏图片。参数说明docx_path传相对或绝对路径都行但路径里如果有中文Windows 下建议用原始字符串rC:\某目录\report.docx否则反斜杠会被当转义符。output_dir会自动创建不用提前手动建。3.2 导出嵌入文件embeddings 目录与扩展名还原嵌入文件的导出逻辑和图片几乎一样但有个坑word/embeddings/下的文件通常叫oleObject1.bin、oleObject2.bin扩展名全是.bin你根本不知道它原本是 Excel 还是 PDF。这时候需要读word/_rels/document.xml.rels这个关系文件里面记录了每个资源的真实类型。import zipfile import os import re def export_embeddings(docx_path, output_dir): 导出嵌入的 OLE 对象并尝试还原真实扩展名 os.makedirs(output_dir, exist_okTrue) exported [] with zipfile.ZipFile(docx_path, r) as z: # 先读关系文件建立 rId 到目标路径的映射 rels_content if word/_rels/document.xml.rels in z.namelist(): rels_content z.read(word/_rels/document.xml.rels).decode(utf-8) # 解析 Relationship 节点提取 Id 和 Target rel_map {} for match in re.finditer(rId([^])[^]*Target([^]), rels_content): rel_map[match.group(1)] match.group(2) for name in z.namelist(): if name.startswith(word/embeddings/): filename os.path.basename(name) if not filename: continue data z.read(name) # 尝试从关系映射里找真实扩展名 real_ext .bin for rid, target in rel_map.items(): if filename in target: # target 形如 embeddings/oleObject1.xlsx if . in target: real_ext . target.rsplit(., 1)[-1] break # 如果原名已有非 bin 扩展名保留 if . in filename and not filename.endswith(.bin): out_name filename else: base filename.rsplit(., 1)[0] out_name base real_ext out_path os.path.join(output_dir, out_name) with open(out_path, wb) as f: f.write(data) exported.append(out_name) return exported逻辑说明关系文件document.xml.rels是 XML 格式每个Relationship节点有Id和Target属性Target指向实际资源路径。用正则提取后建立映射再拿 embeddings 里的文件名去匹配就能还原出.xlsx、.pdf这类真实扩展名。如果匹配不上就保留.bin至少文件内容不丢。参数说明正则里的[^]*是为了跳过中间的其他属性因为 Relationship 节点属性顺序不固定。decode(utf-8)是必须的关系文件是文本不解码会得到 bytes 导致正则匹配失败。3.3 合并成一个可复用脚本把两个函数合到一起加个命令行入口就能当工具用了。import argparse def main(): parser argparse.ArgumentParser(description导出 docx 中的图片和嵌入文件) parser.add_argument(docx, help输入的 docx 文件路径) parser.add_argument(-o, --output, default./docx_output, help输出根目录) args parser.parse_args() img_dir os.path.join(args.output, images) emb_dir os.path.join(args.output, embeddings) imgs export_images(args.docx, img_dir) embs export_embeddings(args.docx, emb_dir) print(f图片导出 {len(imgs)} 个 - {img_dir}) print(f嵌入文件导出 {len(embs)} 个 - {emb_dir}) if __name__ __main__: main()用法就是python export_docx.py report.docx -o ./out。我一般会把输出目录按文档名再分一层避免多个文档混在一起这个可以在调用前自己拼路径。4. 避坑与排查那些让我返工的细节4.1 导出的图片打不开提示文件损坏现象导出的 image1.png 双击打不开或者显示一半就断。原因写入时用了文本模式w而不是wb二进制数据被当字符串处理遇到0x1A之类的字节会被截断。解决所有写文件的地方强制用wb读的时候z.read()返回的就是 bytes不要做任何 decode。4.2 嵌入文件全是 .bin不知道是什么现象导出一堆oleObject1.bin用 Excel 打开提示格式不对。原因没有读关系文件或者关系文件里的 Target 用了相对路径没匹配上。解决先确认word/_rels/document.xml.rels存在再检查正则是否匹配到了Target。有些文档的关系文件里 Target 是embeddings/oleObject1.xlsx直接包含扩展名匹配上就能还原。4.3 图片数量对不上少了浮动图现象文档里明明有 20 张图只导出 15 张。原因用了 python-docx 的inline_shapes浮动图片不在这个集合里。解决回到 zip 层面遍历word/media/这个目录下是所有图片不分内联还是浮动。如果 media 目录下数量也不对那可能是图片以链接方式插入而非嵌入这种情况文件本身就不在 docx 里导不出来。4.4 中文文件名乱码现象导出的文件名变成image1.png没问题但如果是嵌入的中文名附件解出来是乱码。原因zip 内部文件名编码可能是 GBK而 Python 默认按 UTF-8 解码。解决在ZipFile打开时指定metadata_encodinggbkPython 3.11 支持或者用name.encode(cp437).decode(gbk)手动转。我一般先试默认乱码了再加这个参数。4.5 大文档内存爆掉现象处理 500MB 的 docx 时脚本卡死或 MemoryError。原因z.read(name)一次性把整个文件读进内存。解决改用流式读取with z.open(name) as src, open(out_path, wb) as dst: dst.write(src.read())或者用shutil.copyfileobj分块拷贝。对于超大文件分块是必须的。5. 进阶按文档顺序重命名图片并生成一份对照表基础导出只能拿到image1.png这种名字归档时根本不知道哪张图对应文档里哪个位置。我后来加了一步解析document.xml按图片在正文中出现的顺序把 rId 和 media 文件对应起来重命名成fig_001.png、fig_002.png同时生成一个 CSV 对照表记录原始文件名、新文件名和所在段落序号。import zipfile import re import csv import os def export_with_order(docx_path, output_dir): os.makedirs(output_dir, exist_okTrue) with zipfile.ZipFile(docx_path, r) as z: # 读关系文件建立 rId - media 文件名映射 rels z.read(word/_rels/document.xml.rels).decode(utf-8) rid_to_media {} for m in re.finditer(rId(rId\d)[^]*Targetmedia/([^]), rels): rid_to_media[m.group(1)] m.group(2) # 读正文按出现顺序提取 r:embed 的 rId doc_xml z.read(word/document.xml).decode(utf-8) ordered_rids re.findall(rr:embed(rId\d), doc_xml) rows [] seq 1 for rid in ordered_rids: media_name rid_to_media.get(rid) if not media_name: continue src_path fword/media/{media_name} if src_path not in z.namelist(): continue ext os.path.splitext(media_name)[1] new_name ffig_{seq:03d}{ext} data z.read(src_path) with open(os.path.join(output_dir, new_name), wb) as f: f.write(data) rows.append([seq, media_name, new_name, rid]) seq 1 # 写对照表 csv_path os.path.join(output_dir, mapping.csv) with open(csv_path, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([序号, 原始文件名, 新文件名, 关系ID]) writer.writerows(rows) return rows这段代码的关键在于r:embed的提取顺序。document.xml里图片引用是按正文流出现的re.findall返回的列表就是阅读顺序。rId和 media 文件的映射从关系文件里拿注意 Target 是media/xxx这种相对路径正则里要带上media/前缀。encodingutf-8-sig是为了 Excel 打开 CSV 不乱码这个细节我踩过坑普通 utf-8 在 Windows Excel 里中文会变问号。验证方法很简单打开导出的fig_001.png对照文档第一张图看是否一致再看mapping.csv里的序号是否连续。如果序号跳号说明有图片的 rId 没在关系文件里找到通常是图片被删除但关系残留可以忽略。从那以后我每次做文档归档都强制走一遍“先导出、再对照、后校验”的流程再也没出现过图片对不上号的情况。希望帮到你。本文还有配套的精品资源点击获取