ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Python批量插入图片和表格到Word:python-docx自动化实战

Python批量插入图片和表格到Word:python-docx自动化实战

简介:这份资源面向需要批量处理Word文档的Python学习者与办公自动化从业者,聚焦用python-docx库把图片和表格高效写入.docx文件,解决手工重复排版、逐份编辑效率低的问题,适合具备Python基础、希望提升文档处理效率的读者。压缩包共12个文件,以10个docx成品文档为主,另含1个py脚本和1张jpg示例图片,整体约367KB,体积轻便,便于直接运行与对照学习。脚本演示了遍历图片目录、按行列数据生成表格、设置图片尺寸并统一保存的完整流程,docx文件则给出批量生成后的实际效果,方便读者比对输出结果。目前已有181人学习下载,可作为办公自动化入门到实践的参考样例,帮助读者掌握Document创建、add_picture、add_table及save等核心用法,并迁移到工资通知、报表汇总等批量文档场景中。

1. 批量把图片和表格写进 Word:一个被低估的自动化刚需

上周帮同事处理一份季度报告,需要把 30 多张截图和 12 张数据表按顺序塞进同一个 Word 文档里。手动操作到第 8 张的时候我就放弃了——每张图要调尺寸、每张表要调列宽、标题和正文的字体还得统一。这种活儿在自动化办公的语境里其实非常典型:数据在 Excel 或文件夹里躺着,最终交付物却是一份格式规整的 Word。python-docx这个库很多人听过,但真正用起来会发现,图片插入的尺寸控制、表格样式的继承、段落与表格的交替排列,每一个都是独立的坑。这份资源包围绕的就是这个场景:用 Python 把图片和表格批量写入 Word,省掉重复的复制粘贴和格式调整。适合经常做报告汇总、数据归档、批量生成文档的从业者,新手跟着步骤也能跑通,熟手可以重点看参数控制和异常处理的部分。

2. 环境准备与 python-docx 的核心对象模型

2.1 安装与版本选择

python-docx是操作 Word 文档最常用的第三方库,它不需要本机安装 Office,直接读写.docx文件。安装命令很简单:

pip install python-docx

但这里有个版本坑:python-docx在 0.8.11 之后有一段时间没更新,社区里有人用docx这个包名装到了另一个同名但功能不同的库。我一般会显式确认版本:

pip show python-docx

输出里看到Name: python-docx和Version: 1.1.0以上才算对。如果装错了,先pip uninstall docx再重新装python-docx。另外,处理图片时底层依赖Pillow,通常会自动装上,但如果你用的是精简版 Python 环境,最好手动补一个:

pip install Pillow

提示:不要用pip install docx,那个包已经废弃,导入时会报ModuleNotFoundError或者行为异常。

2.2 Document、Paragraph、Table 的关系

理解python-docx的对象模型,后面写代码才不会乱。一个Document对象代表整个.docx文件,它内部维护一个块级元素序列。块级元素主要有两种:Paragraph(段落)和Table(表格)。图片本身不是块级元素,它必须被放进一个Run对象里,而Run又隶属于某个Paragraph。所以插入图片的路径是:Document→Paragraph→Run→add_picture()。

表格则是直接挂在Document下的,通过add_table(rows, cols)创建。表格里的每个单元格又是一个独立的容器,可以往里写文字、插图片,甚至再嵌套表格。很多人第一次用的时候会困惑:为什么表格不能像图片那样直接add_picture?因为表格是块级元素,图片是行内元素,层级不同。

下面这段代码展示了基本的结构关系:

from docx import Document from docx.shared import Inches doc = Document() # 添加一个段落,段落里加一个 Run,Run 里插图片 para = doc.add_paragraph() run = para.add_run() run.add_picture('chart.png', width=Inches(4.5)) # 添加一个 3 行 4 列的表格 table = doc.add_table(rows=3, cols=4) table.style = 'Table Grid' # 往单元格里写内容 table.cell(0, 0).text = '指标' table.cell(0, 1).text = 'Q1' table.cell(0, 2).text = 'Q2' table.cell(0, 3).text = 'Q3' doc.save('demo.docx')

逻辑说明:add_paragraph()返回一个Paragraph对象,add_run()在段落里创建一个行内容器,add_picture()把图片文件写入这个容器。add_table(rows, cols)直接创建表格,table.style设置边框样式,table.cell(row, col)定位到具体单元格。参数方面,width和height接受Inches、Cm、Pt等单位对象,只给宽度时高度会按比例缩放,两个都给则强制拉伸——这一点后面避坑章节会展开。

2.3 批量写入的整体流程设计

批量操作的核心思路是:把“数据源”和“写入逻辑”分开。数据源可以是一个文件夹里的图片列表,也可以是一个 Excel 文件里的表格数据。写入逻辑则是一个循环,按顺序往Document里追加内容。

我一般会先把要插入的内容整理成一个列表,每个元素是一个字典,标明类型和路径或数据:

items = [ {'type': 'heading', 'text': '一、整体概况'}, {'type': 'image', 'path': 'imgs/overview.png', 'width': 5.0}, {'type': 'table', 'data': [['区域', '销量'], ['华东', '1200'], ['华南', '980']]}, {'type': 'heading', 'text': '二、分项明细'}, {'type': 'image', 'path': 'imgs/detail.png', 'width': 4.5}, ]

然后写一个分发函数,根据type调用不同的处理逻辑。这样做的好处是,数据源怎么变都不影响写入逻辑,后面要加新类型(比如页眉、页码)也只需要扩展分发函数。这个模式在批量生成报告时非常稳,不容易因为某一条数据格式不对就整个崩掉。

3. 图片批量插入:尺寸、对齐与格式兼容

3.1 按文件夹顺序批量插图

最常见的需求是:一个文件夹里按编号排好了图片,要按顺序全部插进 Word。用os.listdir或pathlib拿到文件列表后,排序是关键。如果文件名是img1.png、img2.png、img10.png,默认字符串排序会把img10排在img2前面。解决办法是用自然排序:

from pathlib import Path import re def natural_key(path): # 把文件名里的数字提取出来作为排序依据 return [int(s) if s.isdigit() else s.lower() for s in re.split(r'(\d+)', path.name)] img_dir = Path('imgs') img_files = sorted(img_dir.glob('*.png'), key=natural_key) doc = Document() for img_path in img_files: doc.add_picture(str(img_path), width=Inches(5.0)) doc.add_paragraph() # 图片之间留一个空段落,避免挤在一起 doc.save('batch_images.docx')

逻辑说明:natural_key把文件名拆成数字和非数字片段,数字转成int参与排序,这样img2就会排在img10前面。doc.add_picture()是Document上的快捷方法,内部自动创建段落和 Run。width=Inches(5.0)统一宽度,高度按原图比例自动算。最后加一个空段落是为了让图片之间有间距,否则连续插入的图片会紧贴在一起。

参数方面,width和height都可以用Inches、Cm、Mm、Pt、Emu。我一般用Inches,因为 Word 里默认的页面宽度是 8.5 英寸(Letter)或 8.27 英寸(A4),减去左右边距各 1 英寸,正文区域大约 6.27 英寸。所以图片宽度设 5.0 到 6.0 英寸比较安全,超过 6.27 会被挤到下一页或者被裁切。

3.2 控制图片尺寸与保持宽高比

只给width时,python-docx会按原图比例自动算高度,这是最省心的做法。但如果你同时给了width和height,它就会强制拉伸,图片可能变形。我见过有人为了统一尺寸,把截图和照片都设成width=4, height=3,结果截图被压扁,照片被拉长。

正确的做法是:先读原图尺寸,再按目标宽度算高度。用Pillow读尺寸:

from PIL import Image from docx.shared import Inches def add_image_keep_ratio(doc, img_path, target_width_inch): with Image.open(img_path) as im: w_px, h_px = im.size ratio = h_px / w_px target_height_inch = target_width_inch * ratio doc.add_picture(img_path, width=Inches(target_width_inch), height=Inches(target_height_inch))

逻辑说明:Image.open()读取图片的像素宽高,算出高宽比,再用目标宽度乘以比例得到目标高度。这样图片不会变形。参数target_width_inch根据你的页面正文宽度来定,A4 纸常用 5.5 到 6.0。

注意:如果图片本身是竖长图,按宽度缩放后高度可能超过一页,Word 会自动分页,但图片会被截断。这种情况我一般会判断一下,如果算出来的高度超过 8 英寸,就改用按高度缩放。

3.3 图片对齐与段落格式

add_picture()默认是左对齐,而且图片所在的段落没有额外间距。如果你想让图片居中,需要拿到图片所在的段落对象,设置对齐方式:

from docx.enum.text import WD_ALIGN_PARAGRAPH para = doc.add_paragraph() para.alignment = WD_ALIGN_PARAGRAPH.CENTER run = para.add_run() run.add_picture('chart.png', width=Inches(5.0))

逻辑说明:WD_ALIGN_PARAGRAPH.CENTER是段落对齐的枚举值,还有LEFT、RIGHT、JUSTIFY。图片本身没有对齐属性,对齐是段落级别的。另外,如果你想让图片和上下文字有间距,可以设置段落的space_before和space_after:

para.paragraph_format.space_before = Pt(6) para.paragraph_format.space_after = Pt(6)

参数单位是Pt(磅),6 磅大约等于 0.08 英寸,视觉上就是一点点空隙。批量插图时我一般会统一设成 6 到 12 磅,避免图片和标题粘在一起。

4. 表格批量写入:数据源、样式与动态行数

4.1 从 Excel 或 CSV 读取表格数据

表格数据通常来自 Excel 或 CSV。用pandas读进来最方便,但如果你不想引入pandas这个重依赖,用csv模块也够。下面以 CSV 为例:

import csv def read_table_data(csv_path): with open(csv_path, 'r', encoding='utf-8-sig') as f: reader = csv.reader(f) return [row for row in reader] table_data = read_table_data('sales.csv') # table_data 是二维列表,第一行通常是表头

逻辑说明:encoding='utf-8-sig'是为了处理 Excel 导出的 CSV 带 BOM 头的问题,不加的话第一列列名前面会多一个不可见字符。csv.reader返回的是迭代器,转成列表后每一行是一个字符串列表。如果数据里有数字,读进来也是字符串,写进 Word 后如果需要计算,得自己转类型。

如果数据源是 Excel 的.xlsx文件,用openpyxl读:

from openpyxl import load_workbook wb = load_workbook('data.xlsx', data_only=True) ws = wb.active table_data = [[cell.value for cell in row] for row in ws.iter_rows()]

data_only=True表示读公式的计算结果而不是公式本身,这个参数在批量生成报告时很关键,否则表格里会出现=SUM(A1:A10)这种字符串。

4.2 创建表格并写入数据

拿到二维列表后,创建表格并填充:

def add_table_from_data(doc, data, style='Table Grid'): rows = len(data) cols = len(data[0]) if rows > 0 else 0 table = doc.add_table(rows=rows, cols=cols) table.style = style for i, row_data in enumerate(data): for j, cell_value in enumerate(row_data): table.cell(i, j).text = str(cell_value) if cell_value is not None else '' return table

逻辑说明:rows和cols从数据里动态算出来,避免硬编码。table.style = 'Table Grid'给表格加边框,Word 内置的样式名还有'Light Shading Accent 1'、'Medium Shading 1 Accent 1'等,但不同版本的 Word 对样式名的支持有差异,'Table Grid'是最稳的。table.cell(i, j).text直接赋值,如果值是None就写空字符串,否则str()转一下。

参数方面,add_table(rows, cols)的rows和cols必须是整数,创建后不能直接增删行列——要增删得用table.add_row()或table.add_column(),但这两个方法在旧版本里行为不太一致。我一般是一次性算好行列数再创建。

4.3 表格样式与列宽调整

Table Grid只给边框,列宽是平均分的。如果某一列内容特别长,平均分会导致换行很难看。调整列宽需要遍历每一行的单元格,设置cell.width:

from docx.shared import Inches def set_col_widths(table, widths_inch): for row in table.rows: for idx, width in enumerate(widths_inch): row.cells[idx].width = Inches(width) set_col_widths(table, [1.2, 2.0, 1.5, 1.5])

逻辑说明:table.rows遍历每一行,row.cells[idx]定位到该行的第 idx 个单元格,设置width。注意python-docx的列宽设置是“建议值”,Word 打开时会根据内容自动调整,除非你同时设置了表格的autofit属性:

table.autofit = False

把autofit设为False后,列宽才会严格按照你设的值来。这个坑我踩过好几次——明明设了列宽,打开 Word 还是被自动调了,就是因为没关autofit。

提示:如果表格列数很多,总宽度超过页面正文宽度,Word 会强制压缩或者让表格超出页面。A4 纸正文宽度约 6.27 英寸,列宽之和不要超过这个值。

5. 避坑与常见问题排查

5.1 图片插入后显示为红叉或空白

现象:代码跑完没报错,但打开 Word 发现图片位置是一个红叉或者空白框。

原因:图片路径包含中文或特殊字符,python-docx在写入时没有正确处理编码;或者图片文件本身损坏,Pillow能打开但python-docx读取时失败。

解决:把图片路径转成绝对路径,并且确保路径里没有中文。如果必须有中文,用pathlib.Path的resolve()转绝对路径:

from pathlib import Path img_path = Path('图片/图表.png').resolve() doc.add_picture(str(img_path), width=Inches(5.0))

另外,用Pillow先验证图片能否正常打开:

from PIL import Image try: with Image.open(img_path) as im: im.verify() except Exception as e: print(f'图片损坏: {img_path}, {e}')

5.2 表格样式在打开 Word 后丢失

现象:代码里设了table.style = 'Table Grid',但打开 Word 后表格没有边框。

原因:python-docx创建表格时,样式名必须和 Word 内置样式完全匹配。如果你用的样式名在当前 Word 版本里不存在,它会静默失败,不报错但也不生效。

解决:优先用'Table Grid',这是兼容性最好的。如果确实需要其他样式,先在 Word 里手动创建一个带该样式的表格,保存为.docx,然后用python-docx打开这个文件,打印table.style.name看实际样式名是什么。不要凭记忆写样式名。

5.3 批量插入大量图片时内存暴涨

现象:插入几百张图片后,Python 进程内存占用超过 2GB,甚至被系统杀掉。

原因:python-docx在add_picture()时会把图片数据读进内存并保留在Document对象里,直到save()才写入磁盘。图片越多,内存占用越大。

解决:分批处理。每处理 50 张图片就save()一次,然后重新打开继续追加:

from docx import Document def batch_insert_images(img_files, batch_size=50): doc = Document() for idx, img_path in enumerate(img_files): doc.add_picture(str(img_path), width=Inches(5.0)) if (idx + 1) % batch_size == 0: doc.save('output.docx') doc = Document('output.docx') # 重新打开,释放内存 doc.save('output.docx')

逻辑说明:每 50 张保存一次并重新加载,Document对象重建后旧的内存会被回收。代价是每次重新加载会有少量 I/O 开销,但比内存溢出强。

5.4 中文字体在表格和段落里不生效

现象:代码里设了run.font.name = '宋体',但 Word 里中文还是默认字体。

原因:python-docx的font.name只对西文生效,中文字体需要额外设置w:eastAsia属性。

解决:用qn函数设置 East Asian 字体:

from docx.oxml.ns import qn run = para.add_run('中文内容') run.font.name = 'Times New Roman' run._element.rPr.rFonts.set(qn('w:eastAsia'), '宋体')

逻辑说明:run._element.rPr.rFonts拿到字体的 XML 元素,qn('w:eastAsia')生成 Word 命名空间下的属性名,设置为'宋体'。这样西文用 Times New Roman,中文用宋体。表格里的单元格文字同理,需要拿到单元格的段落和 Run 再设置。

5.5 插入的表格行数太多导致跨页断行混乱

现象:表格行数超过一页,跨页后表头没有重复,第二页的表格看起来像没有表头的新表格。

原因:python-docx默认不重复表头行。

解决:设置表格首行为表头重复:

from docx.oxml.ns import qn from docx.oxml import OxmlElement def set_repeat_header(row): tr = row._tr trPr = tr.get_or_add_trPr() tblHeader = OxmlElement('w:tblHeader') tblHeader.set(qn('w:val'), 'true') trPr.append(tblHeader) set_repeat_header(table.rows[0])

逻辑说明:table.rows[0]._tr拿到第一行的 XML 元素,get_or_add_trPr()获取或创建行属性容器,然后插入w:tblHeader元素并设为true。这样 Word 在跨页时会自动重复表头行。这个操作在批量生成多页表格时几乎是必加的,否则打印出来很难读。

6. 进阶技巧:用模板 + 占位符实现可复用的报告生成

批量插入图片和表格只是第一步。实际工作中更常见的是:有一份固定的 Word 模板,里面用占位符标好了哪里放图片、哪里放表格,然后根据数据源替换占位符。这样做的好处是格式完全由模板控制,代码只负责填数据,不用在代码里写一堆字体、间距、对齐的设置。

具体做法是:在 Word 里手动做好模板,占位符用{{image_1}}、{{table_1}}这种格式。然后用python-docx打开模板,遍历所有段落,找到占位符所在的 Run,替换成图片或表格。

from docx import Document from docx.shared import Inches def replace_placeholder_with_image(doc, placeholder, img_path, width_inch): for para in doc.paragraphs: if placeholder in para.text: # 清空占位符所在的 Run for run in para.runs: if placeholder in run.text: run.text = run.text.replace(placeholder, '') # 在段落里插入图片 run = para.add_run() run.add_picture(img_path, width=Inches(width_inch)) return True return False doc = Document('template.docx') replace_placeholder_with_image(doc, '{{image_1}}', 'chart1.png', 5.5) doc.save('report.docx')

逻辑说明:遍历doc.paragraphs,找到包含占位符的段落,先把占位符文本从 Run 里删掉,再在同一个段落里新增一个 Run 来放图片。这样图片就出现在占位符原来的位置。表格的替换稍微复杂一点,因为表格不能直接塞进段落,需要先找到占位符段落,然后在它后面插入表格,再删掉占位符段落。

def replace_placeholder_with_table(doc, placeholder, data): for para in doc.paragraphs: if placeholder in para.text: # 在占位符段落后面插入表格 table = doc.add_table(rows=len(data), cols=len(data[0])) table.style = 'Table Grid' for i, row_data in enumerate(data): for j, val in enumerate(row_data): table.cell(i, j).text = str(val) # 把表格移动到占位符段落的位置 para._element.addnext(table._element) # 删除占位符段落 para._element.getparent().remove(para._element) return True return False

逻辑说明:doc.add_table()创建的表格默认追加在文档末尾,用para._element.addnext(table._element)把表格的 XML 元素移动到占位符段落后面,然后删掉占位符段落。这样表格就出现在原来占位符的位置。这个“先创建再移动”的模式在python-docx里很常用,因为库本身没有提供“在指定位置插入表格”的 API。

模板方案的优势在于:格式调整不用改代码,改模板就行。我一般会把模板里的样式、页眉页脚、页码都设好,代码只负责填内容。这样即使后面报告格式要改,也只需要让设计同事改模板,不用重新跑代码。

从那以后我每次做批量文档生成,都会先花十分钟做一个模板,把占位符和样式定好,再写替换逻辑。这样后面不管数据怎么变,格式都不会跑偏。希望帮到你。

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

RELATED READING

延伸阅读

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