Python-docx批量写入Word:图片表格自动化脚本实战
简介:这份资源面向需要批量处理Word文档的Python学习者与办公自动化从业者,聚焦用python-docx库把图片和表格高效写入.docx文件,解决重复手工排版、逐份生成文档的痛点,适合具备Python基础、希望提升文档处理效率的初中级开发者。压缩包共12个文件,约367KB,包含10个docx示例文档、1个py脚本和1个jpg图片,docx用于展示批量生成的工资调整通知效果,py脚本承载图片遍历与表格填充的核心逻辑,jpg则作为插入图片的素材。目前已有181人学习下载。读者可从中获得可直接运行的批量处理脚本,理解Document创建、add_picture插图、add_table建表与cell填充、doc.save保存等关键用法,并借助os库遍历目录的思路,把同一套流程迁移到通知、报表、台账等场景,快速搭建属于自己的文档自动化方案。
1. 批量把图片和表格塞进 Word:一份能直接跑的 python-docx 脚本包
手里攒了三十几份巡检记录,每份都要把现场照片和检测数据表贴进对应的 Word 模板里,手动复制粘贴到第三份的时候我就知道这事不能这么干。这份资源就是冲着这个场景来的:一个基于 python-docx 的批量写入脚本包,把散落在文件夹里的图片和表格按规则灌进 .docx,省掉重复劳动。它适合两类人——一类是天天跟报表、台账、检测记录打交道的办公岗,另一类是想拿 python-docx 练手但不知道从哪下手的开发者。核心逻辑不复杂,难的是图片尺寸、表格样式、中文字体这些细节,脚本包里都做了预设,改几个路径就能跑。
2. 拆开脚本包:python-docx 写入图片和表格的底层逻辑
2.1 为什么选 python-docx 而不是直接操作 XML
Word 的 .docx 本质是个 zip 包,里面是一堆 XML。理论上你可以用 zipfile 解压、改 document.xml、再压回去,但这条路走一次就不想走第二次——命名空间、关系映射、rId 分配,任何一处对不上,Word 打开就报“内容有问题”。python-docx 把这层封装掉了,它把 document.xml 里的段落、表格、图片关系都抽象成 Python 对象,你调 add_picture、add_table 就行。
常见做法是用 python-docx 做主体写入,遇到它不支持的冷门样式(比如某些域代码、复杂页眉)再退回 lxml 操作底层 XML。这份脚本包走的就是这个路线:主体用 python-docx,个别地方用 OxmlElement 补了边框和底纹。
选型上还有一个考虑是依赖轻。python-docx 只依赖 lxml 和 typing-extensions,pip 装完就能用,不需要装 Office,也不需要 COM 接口。这意味着它能在 Linux 服务器上跑,也能塞进定时任务里。如果你之前用 win32com 操作 Word,应该体会过那种“必须开着 Word 进程、偶尔卡死、报错信息看不懂”的滋味,python-docx 在这点上干净得多。
2.2 图片写入:尺寸、对齐和路径的三个参数
先看图片写入的核心代码。脚本包里把这段封装成了insert_images(doc, img_dir, width_cm=12):
from docx import Document from docx.shared import Cm from docx.enum.text import WD_ALIGN_PARAGRAPH import os def insert_images(doc, img_dir, width_cm=12): """ 把 img_dir 下的图片按文件名排序后逐张插入 doc width_cm: 图片宽度,单位厘米,高度按比例自动缩放 """ # 只取常见图片格式,避免把缩略图缓存也读进来 exts = ('.png', '.jpg', '.jpeg', '.bmp') files = sorted( f for f in os.listdir(img_dir) if f.lower().endswith(exts) ) for fname in files: path = os.path.join(img_dir, fname) # 先加一个空段落,再往段落里加图片,方便控制对齐 para = doc.add_paragraph() para.alignment = WD_ALIGN_PARAGRAPH.CENTER run = para.add_run() # 只给宽度,高度自动等比缩放,避免图片被拉变形 run.add_picture(path, width=Cm(width_cm)) # 图片下方加一行文件名做标注,不需要可删 cap = doc.add_paragraph(fname) cap.alignment = WD_ALIGN_PARAGRAPH.CENTER逻辑说明:add_picture只传 width 的时候,python-docx 会按图片原始宽高比自动算高度,这是最稳的做法。如果你同时传 width 和 height,图片会被强行拉伸,人像和表格截图会变形。参数width_cm默认 12 厘米,对应 A4 纸正文宽度(约 15.9 厘米)留出边距后的常见值,你可以按模板调整。
路径处理上有个细节:os.listdir返回的顺序在不同系统上不一致,所以脚本里加了sorted。如果你的图片文件名是img1.jpg、img2.jpg、img10.jpg这种,字符串排序会把 img10 排到 img2 前面,这时候需要改成按数字排序:
import re files.sort(key=lambda x: int(re.search(r'\d+', x).group()))这个坑我在第一次批量插巡检照片的时候就踩了,十张照片顺序全乱,后来加了自然排序才对上。
2.3 表格写入:从列表数据到带样式的 Word 表格
表格写入比图片多一层结构。脚本包里insert_table(doc, data, headers=None, style='Table Grid')的实现:
def insert_table(doc, data, headers=None, style='Table Grid'): """ data: 二维列表,如 [['A', 1], ['B', 2]] headers: 表头列表,为 None 时不生成表头行 style: Word 内置表格样式名,'Table Grid' 带全边框 """ rows = len(data) + (1 if headers else 0) cols = len(headers) if headers else len(data[0]) table = doc.add_table(rows=rows, cols=cols) table.style = style if headers: for j, h in enumerate(headers): cell = table.cell(0, j) cell.text = str(h) # 表头加粗 for p in cell.paragraphs: for r in p.runs: r.bold = True offset = 1 if headers else 0 for i, row in enumerate(data): for j, val in enumerate(row): table.cell(i + offset, j).text = str(val) return table逻辑说明:add_table创建的是空表格,行列数必须提前算好。style='Table Grid'是 Word 内置样式名,带全边框,最常用。如果你想要三线表,得手动设边框,python-docx 没有直接的三线表样式,需要用 OxmlElement 改 tblBorders,脚本包里附了这段代码。
参数上要注意data的每一行长度必须一致,否则len(data[0])算出来的列数会对不上,写入时索引越界。我一般会在调用前加一句校验:
assert len(set(len(r) for r in data)) == 1, "每行列数不一致"中文字体是另一个高频问题。python-docx 默认字体是 Calibri,中文会回退到宋体,但如果你在模板里指定了字体,写入的表格可能不继承。稳妥做法是显式设置:
from docx.oxml.ns import qn style = doc.styles['Normal'] style.font.name = 'Times New Roman' style.element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')这段要放在创建文档之后、写入内容之前,否则已经写入的段落不会更新。
2.4 批量循环:一份数据对应一份文档的目录约定
脚本包的主入口是batch_generate(template_path, data_root, out_dir),目录约定是这样的:
data_root/ record_001/ images/ # 该记录的图片 table.csv # 该记录的表格数据 record_002/ ...主循环代码:
import csv from docx import Document def batch_generate(template_path, data_root, out_dir): os.makedirs(out_dir, exist_ok=True) for case in sorted(os.listdir(data_root)): case_dir = os.path.join(data_root, case) if not os.path.isdir(case_dir): continue # 每份文档都从模板复制一份,保留页眉页脚和样式 doc = Document(template_path) img_dir = os.path.join(case_dir, 'images') if os.path.isdir(img_dir): insert_images(doc, img_dir) csv_path = os.path.join(case_dir, 'table.csv') if os.path.exists(csv_path): with open(csv_path, encoding='utf-8-sig') as f: reader = list(csv.reader(f)) insert_table(doc, reader[1:], headers=reader[0]) doc.save(os.path.join(out_dir, f'{case}.docx'))逻辑说明:Document(template_path)是打开模板而不是新建空文档,这样模板里的页眉、页脚、样式、页面设置都会保留。encoding='utf-8-sig'是为了兼容 Excel 导出的 CSV 带 BOM 头的情况,不加这个参数第一列会多一个不可见字符。
参数out_dir建议和data_root分开,避免输出文件被下一轮循环当成输入。如果你要覆盖原文件,记得先备份。
3. 避坑排查:图片不显示、表格错位、字体乱掉的五个血泪经验
3.1 图片插入后 Word 显示红叉或空白
现象:脚本跑完没报错,打开 docx 图片位置是红叉或空白框。
原因:python-docx 插入图片时是把图片二进制写进 docx 包里的,正常不会丢。出现红叉通常是图片本身损坏,或者扩展名和实际格式不符——比如把 webp 改成 jpg 后缀,python-docx 按 jpg 解析失败但没抛异常。
解决:插入前用 PIL 校验一遍:
from PIL import Image try: Image.open(path).verify() except Exception as e: print(f'跳过损坏图片 {path}: {e}') continue3.2 表格列宽在 Word 里被自动调整
现象:脚本里没设列宽,Word 打开后表格列宽忽宽忽窄,跟预期不符。
原因:python-docx 创建表格时如果不设列宽,Word 会按“自动调整”渲染,列宽取决于内容长度。table.autofit = False只是关掉自动调整,还需要显式设每列宽度。
解决:创建表格后逐列设置:
table.autofit = False for j, w in enumerate([Cm(3), Cm(5), Cm(4)]): for cell in table.columns[j].cells: cell.width = w注意cell.width要每个单元格都设,只设column.width在某些 Word 版本上不生效。
3.3 中文字体在表格里变成宋体,标题字体没继承
现象:正文段落字体正常,表格里的中文全变成宋体,和模板不一致。
原因:表格单元格的段落用的是 Normal 样式,但 python-docx 写入时没有显式指定 eastAsia 字体,Word 回退到默认中文字体。
解决:写入表格后遍历单元格统一设字体:
for row in table.rows: for cell in row.cells: for p in cell.paragraphs: for r in p.runs: r.font.name = '微软雅黑' r.element.rPr.rFonts.set(qn('w:eastAsia'), '微软雅黑')3.4 批量跑完发现文档数量对不上
现象:data_root 下有 30 个文件夹,out_dir 只生成了 28 个 docx。
原因:通常是文件夹名排序后中间某个文件夹里没有 images 也没有 table.csv,脚本跳过了;或者文件夹名含特殊字符导致os.path.join路径异常。
解决:在循环里加日志,把跳过的文件夹打出来:
else: print(f'跳过空目录: {case_dir}')另外检查文件夹名有没有前后空格,Windows 下os.listdir会保留空格,os.path.isdir可能判断失败。
3.5 模板里的页眉页脚在输出文档里丢了
现象:模板有页眉页脚,生成的文档页眉页脚是空的。
原因:Document(template_path)会保留页眉页脚,但如果模板的页眉页脚是“链接到前一节”且第一节被修改过,可能出现异常。更常见的原因是模板本身是用旧版 Word 保存的,python-docx 读取时 section 信息不完整。
解决:确认模板是 .docx 格式(不是 .doc 改后缀),用 Word 重新另存为 .docx。如果还不行,在脚本里显式复制 section:
from copy import deepcopy for section in doc.sections: for hf in [section.header, section.footer]: for p in hf.paragraphs: pass # 页眉页脚内容已在模板中,通常不需要额外操作这个问题的根因多半在模板,不在脚本,换一个干净的 .docx 模板通常就好了。
4. 进阶技巧:用配置表驱动不同模板和字段映射
脚本包默认是“一个模板对应一种数据布局”,但实际办公场景里经常是“同一批数据要套不同模板”,或者“不同文件夹的表格列顺序不一样”。这时候硬编码就不够了,我一般会加一层 YAML 配置:
# config.yaml templates: - name: inspection template: templates/inspection.docx image_width_cm: 10 table_style: Table Grid column_map: [0, 2, 1] # 把 CSV 的第0/2/1列映射到表格第0/1/2列 - name: summary template: templates/summary.docx image_width_cm: 14 table_style: Light Shading column_map: null # null 表示不重排读取配置的代码:
import yaml def load_config(path): with open(path, encoding='utf-8') as f: return yaml.safe_load(f) def apply_column_map(row, mapping): if mapping is None: return row return [row[i] for i in mapping]这样改模板和列映射不用动 Python 代码,交给办公岗同事改 YAML 就行。column_map的索引从 0 开始,写的时候注意 CSV 里有没有序号列,有的话要跳过。
另一个进阶用法是给图片加统一水印。python-docx 本身不支持水印,但可以在插入图片前用 PIL 把水印烧进图片:
from PIL import Image, ImageDraw def add_watermark(src, dst, text='内部资料'): img = Image.open(src).convert('RGBA') overlay = Image.new('RGBA', img.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(overlay) draw.text((10, 10), text, fill=(255, 0, 0, 128)) out = Image.alpha_composite(img, overlay).convert('RGB') out.save(dst, quality=85)这个操作会重新编码图片,画质有轻微损失,如果原图是扫描件建议 quality 设 95 以上。
验证方法上,我习惯跑完脚本后随机抽三份文档打开,检查图片顺序、表格边框、字体三处。批量场景下肉眼全检不现实,但抽检能拦住大部分低级错误。另外可以在脚本末尾加一句统计:
print(f'生成 {len(os.listdir(out_dir))} 份,输入 {len(os.listdir(data_root))} 份')数字对不上就回头查 3.4 那条。
从那以后我每次改完脚本,都强制先拿两份样本数据跑一遍,确认输出没问题再上全量。批量操作最怕的就是跑完一百份才发现模板选错了,重跑的时间成本比验证高得多。希望帮到你。
本文还有配套的精品资源,点击获取