ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python自动化文档生成:基于docxtpl与python-docx的批量流水线

Python自动化文档生成:基于docxtpl与python-docx的批量流水线 简介这是一套基于Python的自动化文档生成系统源码面向需要批量处理Word/Excel文档的办公人员、财务人员及Python初学者。系统通过读取Excel台账数据按设定模板自动生成评审意见、反馈函、审核报告等Word文档可显著提升项目评审、财务对账等场景的文档处理效率。压缩包共20个文件以docx工作模板、xlsx数据表格和py主程序为主附带bat一键运行脚本、requirements依赖清单及README说明整体仅459KB结构紧凑。目前已有58人学习下载。资源包含auto_work.py核心脚本、11个常用文档模板、台账数据样例以及优化方向笔记读者可直接修改模板和数据源快速上手也可参照项目结构学习文件读写与自动化办公思路搭建属于自己的文档生成流水线。1. 先定义一个口径自动化文档生成系统要解决的不是“写”而是“批量编排”一个做项目交付的团队每月要出几十份合同和技术方案一个数据部门每天定时把数据库查询结果拼成日报推送出去。这两件事的共同点是文档本身不难写难在“批量、按模板、定期出”。基于 Python 的自动化文档生成系统本质是把“数据获取 模板渲染 文件输出 任务调度”用代码串成一条流水线而不是靠一个脚本硬编码解决一次性需求。它适合两类人一类是经常被重复性文档耗费时间的一线工程师另一类是想在自己的内部系统里加一个“一键导出报告”能力的开发者。标题里“源码”两个字意味着这套方案不该停留在概念层面而是可以从一个最小骨架直接扩展成能落地的生产工具。下面从技术路线选型讲起先把最容易翻车的地方理清楚再给出可运行代码最后补上批量、调度和校验。2. 先选路线模板渲染和程序化组装怎么搭配Python 的文档生成生态里路线看似很多但绝大多数系统最后都会收敛到两条基于 Jinja2 语法的模板渲染以及按对象模型逐段构建的程序化组装。选错路线的成本通常在项目中期爆发因为文档生成系统的核心维护成本不在代码而在“业务方改版式”和“数据结构变化”这两件事上。2.1 模板渲染把 Word 模板交给业务方维护最常用的模板渲染实现是 docxtpl它是一个在 python-docx 之上运行的 Jinja2 引擎把{{ 字段 }}这样的占位符写进 docx 文件渲染时再替换成实际内容。它的核心好处不是省几十行代码而是把模板的维护权从开发转移给业务方——公司改 Logo、换页边距、调整表头直接在 Word 里操作代码一行不用动。from docxtpl import DocxTemplate doc DocxTemplate(templates/report_template.docx) doc.render({ report_title: 华东区交付周报, writer: 张三, }) doc.save(output/report.docx)render 方法接收一个扁平字典把模板中对应的{{ report_title }}替换为值。需要注意这个字典必须是可被 Jinja2 属性查找的对象直接传 None 会渲染成空字符串。如果模板里出现列表正常建议配合循环语法处理纯赋值解决不了多行数据。2.2 程序化组装模板失效的地方它来接管模板渲染解决不了两类场景一类是表格列数在运行期才能确定比如不同合同类型要动态输出 3 到 8 列另一类是同一份文档要装配十几张图、几十个段落顺序还依赖条件判断。这类场景的通用做法是用 python-docx 直接创建 Document 对象逐行拼装内容。from docx import Document from docx.shared import Pt doc Document() doc.add_heading(数据字典, level1) p doc.add_paragraph(更新时间) run p.add_run(2026-01-15) run.font.size Pt(9) table doc.add_table(rows1, cols3) table.style Light Grid Accent 1add_heading 设置标题层级add_paragraph 增加正文段落run 控制局部字号add_table 创建表格后再填充单元格。这里版式逻辑全在代码中修改页面尺寸用 section.page_width调整行距用 paragraph_format能力上几乎没有死角。但代价是业务方想微调格式就必须找开发改代码。2.3 选型对照表三个维度先定方向对比维度模板渲染docxtpl程序化组装python-docx大多数项目建议版式维护业务方在 Word 里改开发改代码模板渲染优先动态表格循环语法可做复杂后难调原生支持控制灵活看结构复杂度页面细节控制受模板格式限制可精确到磅严格排版用组装单批生成规模适合万份以内对象模型内存占用高大批量换其他方案实际搭建时不需要二选一。常见的做法是固定骨架交给模板动态内容在渲染后追加。docxtpl 本身就是基于 python-docx 的两条路线可以在同一套代码里共存关键是数据在切入不同生成方式时保持同一种结构。2.4 三个问题帮自己定夺第一个问题模板是否被业务方定期改如果每个月都要动一次表头或页脚就选模板渲染并把模板路径放进配置文件。第二个问题输出是否需要严格分页或精确到磅的排版模板能实现但调起来非常费劲这种需求反过来应交给程序化组装。第三个问题单批生成是否超过一万份一旦规模上来Word 类对象本身就会吃掉大量内存更合适的做法是转向流式 PDF 渲染而不是批量攒 docx。3. 最小实现用 docxtpl 从 JSON 数据批量生成合同先跑通一个能用的最小骨架后面所有系统化抽象都在这之上扩展。目录结构和虚拟环境这步不能省文档生成项目依赖 docxtpl、lxml、jinja2 三个核心包版本错乱时执行报错最难排查。3.1 环境准备和目录结构mkdir -p docgen/{templates,data,output} cd docgen python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install docxtpl目录树结构如下docgen/ ├── templates/ # 放 Word 模板文件 ├── data/ # 放 JSON / YAML / Excel 源数据 ├── output/ # 生成的 docx 输出目录 └── gen.py # 生成脚本虚拟环境工具会自动锁定当前 Python 版本推荐使用 3.9 及以上版本。pip 安装 docxtpl 时会把 python-docx 作为依赖一并装好后续程序化组装无需再单独安装。templates 目录保持单一职责只放 Word 模板文件数据目录按来源分 JSON 和 Excel 子目录避免后期批量任务混在一起。3.2 模板里写占位符的标准姿势打开 Word 模板直接在段落中键入{{ customer_name }}不要从别处复制后粘贴再改。原因是 docx 的底层 XML 会把一次输入的文字拆成多个 run 片段从外部复制进来的内容往往把占位符拆散。比如一个 run 存了{{另一个 run 存customer_name}}渲染时正则无法匹配完整占位符原文就会残留到输出文档中。在 Word 表格里做行循环时把{%tr for item in items %}写在行首单元格{%tr endfor %}写在行尾单元格。渲染时这一行会被循环复制表格内其他行不受影响。这个语法和段落循环不通用段落外面用的是{% for %}与{% endfor %}。模板语法使用位置效果说明{{ customer_name }}段落、表格单元格整体替换字段值{%tr for item in items %}/{%tr endfor %}表格行首行尾按列表项循环复制整行{{ item.price }}循环行内单元格拿到循环内字段{%p if cond %}/{%p endif %}段落开头结尾条件满足才保留段落提示占位符一旦跨 rundocxtpl 无法自动修复。最稳妥的做法是删掉后重新手工输入输入完不要回改光标。3.3 生成脚本数据标准化和渲染二段式把生成逻辑拆成两个函数build_context 负责把原始数据整理成模板字段render_doc 负责真正渲染。两者拆分后源数据从 JSON 换成 Excel 或数据库时只需改第一个函数渲染层完全不动。# gen.py import json from datetime import date from pathlib import Path from docxtpl import DocxTemplate def build_context(record: dict) - dict: 原始 JSON 记录 - 模板上下文统一做类型和格式转换 return { contract_no: record[no], cust_name: record[customer], sign_date: date.fromisoformat(record[date]).strftime(%Y年%m月%d日), items: record[items], total_amount: f{record[amount]:,.2f} 元, } def render_doc(template_path: str, context: dict, out_path: Path) - None: doc DocxTemplate(template_path) doc.render(context) doc.save(out_path) if __name__ __main__: data json.loads(Path(data/orders.json).read_text(encodingutf-8)) out_dir Path(output) out_dir.mkdir(exist_okTrue) for rec in data[orders]: ctx build_context(rec) render_doc(templates/contract_template.docx, ctx, out_dir / f{rec[no]}.docx) print(fgenerated {rec[no]}.docx)build_context 里的 strftime 把 ISO 格式日期转成更符合书面文档的中文日期格式f-string 的:,.2f把金额格式化为带千分位的两位小数。render_doc 的 DocxTemplate 接收模板路径render 完成替换save 输出文件。这里有两个关键参数context 必须是 dictsave 路径的父目录要事先存在docxtpl 不会自动建目录。上面代码在输出前先 mkdir否则首次运行会抛 FileNotFoundError。3.4 表格循环和图片插入的完整用法假设 data/orders.json 中一条记录长这样{ no: A-1001, customer: 乙方测试科技有限公司, date: 2026-01-15, items: [ {name: 服务器租用, qty: 3, price: 3200}, {name: 带宽升级, qty: 1, price: 500} ], amount: 10100 }模板表格第一行写列名第二行开头写{%tr for item in items %}单元格分别放{{ item.name }}、{{ item.qty }}、{{ item.price }}行尾写{%tr endfor %}。渲染后循环行会按 items 长度复制并逐行填充。如果模板里要插入图片需要用 InlineImage 包装并且必须绑定 DocxTemplate 实例from docxtpl import InlineImage from docx.shared import Mm def build_context(record: dict, doc: DocxTemplate) - dict: ctx { contract_no: record[no], cust_name: record[customer], } if record.get(photo_path): ctx[photo] InlineImage(doc, record[photo_path], widthMm(80)) return ctxInlineImage 的第一个参数是 doc 对象第二个是图片路径width 控制显示宽度。它必须在 doc.render 之前构造因为模板里的{{ photo }}需要引用图形对象photo_path 字段不存在时先做判断避免单条数据异常导致整批生成中断。4. 扩展成系统数据源接入、配置驱动与定时调度最小 demo 里 JSON 是入口但真实项目的数据源绝不是单一文件。把数据层单独抽象出来是脚本向系统过渡的第一个标志。之后再引入配置文件和调度机制这套生成流水线才能放到生产环境里跑。4.1 数据源统一先转成 list[dict] 再喂给模板自己在做数据接入时习惯加一层归一化无论来自数据库、Excel 还是爬虫接口最终都整理成一条条 dict再交到 build_context。用 pandas 读取 Excel 是最省事的方式import pandas as pd from pathlib import Path def load_records(xlsx_path: str) - list[dict]: df pd.read_excel(xlsx_path, dtype{no: str}) return df.to_dict(records)read_excel 的 dtype 参数把合同号固定为字符串避免 Excel 的数字格式把A-1001变成1001或科学计数法。to_dict 产出字典列表每一条对应一个 dict。如果数据来自爬虫把爬到的 JSON 原样落盘再走同一步转 dict 即可。这属于 python 类型转换里的高频坑字典的值类型可能是 Timestamp、numpy 类型或 Decimal模板直接做格式化会报错。所以在 build_context 这一层把日期、金额、枚举值全部转成最终展示字符串。这样做还有个隐藏好处模板里的格式逻辑被收敛到一个函数里业务方改格式时不会牵扯到渲染代码。4.2 用 YAML 配置描述批量任务当文档类型超过三种模板路径和数据源硬编码在 gen.py 里会失控。通用做法是用一个 YAML 描述每种文档的生成任务代码只读配置新增文档类型不动生成逻辑。# tasks.yaml contract: template: templates/contract_template.docx data: data/orders.json output: output/contracts datadict: template: templates/datadict_template.docx data: data/tables.xlsx output: output/datadict生成器根据任务名找到对应配置后执行import json import sys from pathlib import Path import yaml def run_task(cfg: dict) - None: out_dir Path(cfg[output]) out_dir.mkdir(parentsTrue, exist_okTrue) if cfg[data].endswith(.json): raw json.loads(Path(cfg[data]).read_text(encodingutf-8)) records raw[orders] if orders in raw else raw else: records load_records(cfg[data]) for rec in records: render_doc(cfg[template], build_context(rec), out_dir / f{rec[no]}.docx) if __name__ __main__: task_name sys.argv[1] with open(tasks.yaml, encodingutf-8) as f: tasks yaml.safe_load(f) run_task(tasks[task_name])配置字段类型说明template字符串Word 模板文件相对路径data字符串JSON 或 Excel 输入文件output字符串输出目录脚本会自动创建执行命令变成python gen.py contract比之前硬编码的脚本清晰很多。给每类任务独立 output 子目录备份时直接归档整个 output 目录即可。JSON 数据里如果已经包了一层 orders 字段run_task 里加了兼容判断不会因为数据结构轻微差异而中断。4.3 定时调度cron、系统计划任务还是 API 触发文档生成系统里定时任务是另一半核心能力。Linux 服务器上用 cron 配每天早上八点跑一次报表0 8 * * * cd /workspace/docgen /workspace/docgen/.venv/bin/python gen.py contract /workspace/docgen/logs/contract.log 21cron 前两段分别是分钟和小时0 8 * * *表示每天 8 点执行。命令里用 .venv 下的 Python 绝对路径避免系统 Python 和虚拟环境依赖不一致标准输出和错误都重定向到日志文件否则 cron 报错时只能看到邮件提示。Windows 环境等价的命令schtasks /create /tn DocgenContract /tr C:\docgen\.venv\Scripts\python.exe C:\docgen\gen.py contract /sc daily /st 08:00如果文档生成要嵌入现有内部平台再加一个 HTTP 触发接口会更顺手用 Flask 是最常见的做法from flask import Flask, request app Flask(__name__) app.post(/run) def run(): task request.json.get(task, contract) # 在这里调用 run_task(tasks[task]) return {ok: True, task: task}有了 HTTP 接口后调度逻辑可以交给任何任务编排工具生成系统只负责“收到任务、执行任务”。这个接口只适合同步小批量如果单批要生成几千份文档接口里应该异步入队避免请求超时。5. 收尾技巧生成后校验与两个让自动化翻车的坑当系统进入“每天自动出”的模式后最怕的不是报错而是文档里残留{{ xxx }}或被替换错的内容。渲染完不做校验问题往往要等到下游发现才暴露那时已经批量污染了整个输出目录。所以生成逻辑后面要立刻接一道程序化自检。5.1 生成后回读关键字段import sys from pathlib import Path from docx import Document def verify_docx(path: Path, expected: list[str]) - list[str]: doc Document(str(path)) text \n.join(p.text for p in doc.paragraphs) missing [s for s in expected if s not in text] if missing: print(f{path.name} 缺少: {missing}) return missing # 放在批量生成循环末尾 for out_path in Path(output).glob(*.docx): verify_docx(out_path, [合同编号, out_path.stem])verify_docx 把段落拼成一个大字符串逐个检查必需关键字是否存在。需要注意它只覆盖正文段落表格内容不在 paragraphs 里需要校验表格时再展开 table.rows 逐个 cell 取文本。批量循环里任何一个文件缺字段日志里立刻暴露而不是等业务方打开文档才发现。5.2 高频坑一占位符跨 run 导致原样残留这是 docxtpl 项目里出现频率最高的问题。模板从别处复制粘贴或编辑时撤销回改都会让一个占位符碎成多个 run。判断方法很简单打开生成的文档如果{{ xxx }}原样还在多半就是跨 run。解决办法是回到模板删除占位符后重新输入一遍不要动光标回改。也可以在生成任务末尾挂一个检测函数import zipfile def has_unresolved(path: str) - bool: with zipfile.ZipFile(path) as z: xml z.read(word/document.xml).decode(utf-8) return {{ in xml这个函数直接读取 docx 中最核心的 document.xml只要里面出现{{就说明存在未解析占位符。把它挂到批量循环的最后有残留时直接抛错比事后人工检查可靠得多。5.3 高频坑二同名引用分散多处模板漏改一处一份报告里的客户名称通常同时出现在封面、页眉和落款三处。源数据字段改了模板只改了一处生成结果就是一份残次品。规避习惯是把模板中所有可变字段整理成清单放到页脚注释标注“生成后删除”也可以用脚本定期扫描模板里所有含{{的字段与数据字典做比对。批量生成场景下还可以用 hash 检查输出目录的文件分布。相同内容重复生成会留下完全一样的文件这时问题通常在数据源而不是模板sha256sum output/contracts/*.docx | sort | uniq -c | sort -nr如果同一批文档里出现大量相同 hash说明数据源去重没做好而不是模板问题。把这行命令放进每日任务输出目录里多了哪些重复文件、少了哪些关键文件一眼就能扫出来整个流水线也算真正闭环了。本文还有配套的精品资源点击获取
返回列表