批量生成Word文档与邮件合并

Python 办公自动化专题 · 从模板批量生成个性化文档的完整方案

专题:Python 自动化办公系统学习

关键词:Python, 自动化办公, 批量生成Word, 邮件合并, 模板替换, 文档自动化, 数据驱动, python-docx

一、批量生成概述

在现代办公中,频繁面临需要批量生成大量相似文档的场景:HR部门每月要发出数百份劳动合同、学校教务处每学期要打印数千份成绩通知书、企业行政需要批量制作荣誉证书、项目团队要生成统一的会议纪要。如果每份文档都手工编辑,不仅效率低下,而且极易出现错漏。批量生成Word文档技术正是为了解决这一痛点而生。

批量生成的核心思想是"模板+数据=文档"。我们预先设计好文档模板,在其中标记需要动态替换的位置,然后编写程序从数据源(Excel表格、数据库、CSV文件等)读取每一行数据,逐条填充到模板中,最终生成一系列个性化文档。这种方法将重复性劳动交给计算机处理,人力只需要审核模板和最终结果即可。

常见应用场景

场景典型文档数据源批量规模
人力资源劳动合同、录用通知书、离职证明HR系统/Excel花名册数百份
教育行业成绩单、录取通知书、毕业证书教务系统/成绩Excel数千份
企业行政荣誉证书、邀请函、会议纪要参会名单/获奖名单数十至数百份
金融保险保单、对账单、催缴通知业务系统/数据库数万份
政府机关公文、批复文件、公示材料政务系统数百份

技术方案对比

方案优势劣势适用场景
python-docx + 原生替换灵活度高,无额外依赖处理复杂模板需手写大量逻辑简单文本替换、纯代码生成
docxtpl (Jinja2引擎)模板语法强大,支持循环/条件对模板有特定格式要求复杂模板业务文档
VBA邮件合并 (Mail Merge)Office原生支持,无需编程批量规模受限制、跨平台差小批量、普通用户快速操作
comtypes/win32com完全控制Word程序仅Windows、速度慢、依赖Office需调用Word高级功能

工作流程设计

一个标准的批量生成工作流包含以下环节:第一步,准备模板文档,在Word中设计好排版和占位符标记;第二步,准备数据源,将变量数据整理为结构化表格(每列对应一个变量,每行对应一份文档);第三步,编写生成脚本,读取数据源并逐行填充模板;第四步,输出管理,将生成的文档按规则命名并保存到指定目录;第五步,质量验证,抽样检查生成的文档内容是否正确。

# 批量生成工作流核心框架 import os from typing import List, Dict class BatchGenerator: """批量文档生成器基类""" def __init__(self, template_path: str, output_dir: str): self.template_path = template_path self.output_dir = output_dir os.makedirs(output_dir, exist_ok=True) def load_data(self, data_source: str) -> List[Dict]: """加载数据源:支持Excel/CSV/JSON格式""" raise NotImplementedError def render_document(self, record: Dict) -> str: """根据单条数据渲染文档,返回输出路径""" raise NotImplementedError def run(self, data_source: str) -> List[str]: """执行批量生成,返回所有输出文件路径列表""" records = self.load_data(data_source) results = [] total = len(records) for i, record in enumerate(records): print(f"[{i+1}/{total}] 正在生成 {record.get('name', '')}...") output_path = self.render_document(record) results.append(output_path) print(f"完成!共生成 {len(results)} 份文档") return results
# 安装必要的依赖库 # pip install python-docx docxtpl openpyxl pandas python-dateutil # 验证安装 import docx import docxtpl import openpyxl import pandas as pd print(f"python-docx 版本: {docx.__version__}") print(f"openpyxl 版本: {openpyxl.__version__}") print(f"pandas 版本: {pd.__version__}") print("依赖库安装完整,准备就绪!")

二、占位符模板

模板是批量生成文档的核心资产。一个好的模板应该遵循"内容与样式分离"的原则:样式在模板中固定好,内容通过占位符动态替换。模板设计的好坏直接决定了批量生成的效率和最终文档的质量。设计模板时,首先要确定哪些内容是固定不变的(如公司名称、条款文本、表格结构),哪些内容是需要动态替换的(如姓名、日期、金额、编号)。

模板设计原则

占位符规范

推荐使用{{字段名}}双花括号风格作为占位符,这是目前最通用的方案。字段名应采用驼峰命名或下划线命名,如{{employeeName}}{{employee_name}}。避免使用中文作为字段名,因为部分库在处理中文占位符时可能出现编码问题。占位符应单独占据一个独立的文本Run(Word文档中的最小文本单元),这样替换时不会破坏原有格式。

# Word模板中的占位符设计示例(在Word中编辑) # 不要在代码中执行,仅供查看模板设计规范 """ 模板文件名:contract_template.docx 文档内容设计: -------------------------------------------------- 劳动合同 甲方(用人单位):{{company_name}} 乙方(员工姓名):{{employee_name}} 身份证号:{{id_number}} 合同期限:{{start_date}} 至 {{end_date}} 试用期:{{probation_months}}个月 岗位:{{position}} 基本工资:¥{{base_salary}}/月 一、合同期限 本合同为{{contract_type}}合同... 二、工作内容 乙方在{{department}}部门担任{{position}}岗位... ... -------------------------------------------------- 注意事项: 1. 占位符前后可加空格以便在Word中清晰识别 2. 数字型字段(如金额)需在程序中格式化 3. 日期字段需统一为"YYYY年MM月DD日"格式 """

动态段落与表格扩展

在实际业务中,经常需要处理动态行数的表格。例如员工的培训经历可能有多条,合同中涉及的多条条款可能因岗位不同而变化。对于这种情况,模板中只需保留一行作为"模板行",程序读取数据后复制该行并填充内容。表格扩展需要精确操作Word XML层面,推荐使用docxtpl的Jinja2循环语法,比手动操作更可靠。

# 手动处理动态表格行(使用python-docx) from docx import Document def populate_table_rows(doc: Document, table_index: int, data_rows: list): """ 向指定表格填充动态行数据 :param doc: Document对象 :param table_index: 表格索引(0开始) :param data_rows: 数据行列表,每行为dict """ table = doc.tables[table_index] # 保留表头(第0行),复制模板行(第1行)作为基准 template_row = table.rows[1] for i, row_data in enumerate(data_rows): if i == 0: target_row = template_row else: # 复制模板行 target_row = table.add_row() # 填充每列数据 for j, key in enumerate(row_data.keys()): cell = target_row.cells[j] cell.text = str(row_data[key]) # 如果数据行数为0,隐藏模板行 if not data_rows: table._tbl.remove(template_row._tr) # 使用示例 training_data = [ {"course": "新员工入职培训", "date": "2025-01-15", "hours": 8}, {"course": "信息安全培训", "date": "2025-03-20", "hours": 4}, {"course": "管理技能培训", "date": "2025-06-10", "hours": 16}, ] doc = Document("template.docx") populate_table_rows(doc, 0, training_data) doc.save("output.docx")

三、数据驱动生成

数据驱动是批量生成文档的核心思想。每一份个性化文档本质上都是一条数据记录的呈现。数据源可以是多种形式:企业最常用的是Excel表格,因为它直观易编辑;开发项目倾向于使用JSON,因为它结构清晰便于程序处理;数据量较小时CSV也是一种轻量选择。无论何种形式,数据源的每一列对应模板中的一个占位符字段,每一行对应最终的一份文档。

数据质量直接决定生成结果的正确性。在批量生成前,务必对数据进行充分的验证和清洗。常见的验证包括:必填字段是否为空、日期格式是否正确、数字金额是否合法、身份证号/手机号格式是否符合规范等。数据验证做在前面,可以避免生成数百份文档后发现大量错误需要返工的情况。

Excel数据源读取

pandas库提供了最便捷的Excel读取方式。使用pd.read_excel()可以将工作表直接读取为DataFrame,然后通过to_dict('records')转换为字典列表,每条字典对应一份文档所需的数据。对于包含多个工作表的复杂Excel,可以指定sheet_name参数读取指定工作表。需要注意,Excel中合并单元格会导致部分行数据为空,建议在读取前先取消合并或使用fillna(method='ffill')填充。

# 从Excel读取数据源 import pandas as pd from typing import List, Dict def read_data_from_excel(file_path: str, sheet_name: str = 0) -> List[Dict]: """ 从Excel文件读取数据源 :param file_path: Excel文件路径 :param sheet_name: 工作表名称或索引 :return: 字典列表,每项对应一份文档的数据 """ df = pd.read_excel(file_path, sheet_name=sheet_name, dtype=str) # 去除首尾空白字符 df = df.map(lambda x: x.strip() if isinstance(x, str) else x) # 填充空值 df = df.fillna('') records = df.to_dict('records') print(f"成功读取 {len(records)} 条数据记录") print(f"字段列表: {list(df.columns)}") return records def read_data_from_csv(file_path: str, encoding: str = 'utf-8') -> List[Dict]: """从CSV文件读取数据源""" df = pd.read_csv(file_path, dtype=str, encoding=encoding) df = df.fillna('') records = df.to_dict('records') print(f"成功读取 {len(records)} 条数据记录") return records # 数据验证函数 def validate_records(records: List[Dict], required_fields: List[str]) -> List[str]: """ 验证数据记录完整性 :return: 错误信息列表,空列表表示全部通过 """ errors = [] for i, record in enumerate(records): for field in required_fields: if not record.get(field, '').strip(): errors.append(f"第{i+1}行:必填字段 '{field}' 为空") if not errors: print("数据验证通过!") else: print(f"数据验证发现 {len(errors)} 个问题:") for err in errors: print(f" - {err}") return errors

逐条生成与进度显示

当批量规模达到数百上千份时,进度反馈非常重要。使用tqdm库可以显示美观的进度条,让操作者了解当前进度和预估剩余时间。对于大规模批量生成,建议添加异常处理机制:某一条数据出错时,记录错误信息后继续处理下一条,而不是中断整个流程。生成完成后汇总所有错误,便于统一排查修正。

# 批量生成引擎(含进度显示和异常处理) from tqdm import tqdm from docx import Document import traceback class BatchDocxEngine: """批量Word文档生成引擎""" def __init__(self, template_path: str, output_dir: str): self.template_path = template_path self.output_dir = output_dir self.errors = [] def generate_all(self, records: List[Dict], filename_field: str = 'name') -> List[str]: """ 批量生成所有文档 :param records: 数据记录列表 :param filename_field: 用于命名的字段 :return: 成功生成的文件路径列表 """ outputs = [] for i, record in enumerate(tqdm(records, desc="生成文档")): try: doc = Document(self.template_path) self.fill_template(doc, record) filename = f"{record.get(filename_field, f'doc_{i}')}.docx" # 清理文件名中的非法字符 filename = "".join(c for c in filename if c.isalnum() or c in '._-()() ') output_path = f"{self.output_dir}/{filename}" doc.save(output_path) outputs.append(output_path) except Exception as e: error_msg = f"第{i+1}条({record.get('name','未知')})失败: {str(e)}" self.errors.append(error_msg) traceback.print_exc() print(f"\n生成完成:成功 {len(outputs)} 份,失败 {len(self.errors)} 份") if self.errors: print("错误详情:") for err in self.errors: print(f" - {err}") return outputs def fill_template(self, doc: Document, data: Dict): """填充模板内容(子类可重写)""" for paragraph in doc.paragraphs: for key, value in data.items(): placeholder = f"{{{{{key}}}}}" if placeholder in paragraph.text: paragraph.text = paragraph.text.replace(placeholder, str(value))

四、python-docx模板替换

python-docx是Python操作Word文档最基础也最常用的库。它能够读取、创建和修改.docx文件,支持段落、表格、图片、页眉页脚等几乎所有Word元素的处理。在不引入额外依赖的情况下,使用原生python-docx进行模板替换是最轻量级的方案。然而,python-docx的文本替换并不像我们想象的那么简单,这其中最大的"坑"就是Run对象的拆分问题。

在Word中,一个段落(Paragraph)由多个Run组成,每个Run是一段具有相同格式(字体、字号、颜色、加粗等)的文本。当你设置一个占位符{{name}}后,Word可能会将这个占位符拆分为多个Run,比如{{在一个Run中,name在另一个Run中,}}又在第三个Run中。直接简单的字符串替换无法处理这种情况,需要遍历段落中的所有Run,拼接文本后才能正确替换。

正确处理Run对象拆分

# 处理Run对象拆分的正确替换方法 from docx import Document from docx.oxml.ns import qn import re def smart_replace(doc: Document, placeholder: str, value: str) -> int: """ 智能替换文档中的占位符,处理Run拆分问题 :param doc: Document对象 :param placeholder: 占位符文本(如 '{{name}}') :param value: 替换值 :return: 替换次数 """ count = 0 placeholder_lower = placeholder.lower() for paragraph in doc.paragraphs: # 将段落中所有Run的文本拼接起来 full_text = paragraph.text if placeholder not in full_text and placeholder_lower not in full_text.lower(): continue # 查找所有Run中的占位符片段 runs = paragraph.runs for i in range(len(runs)): # 收集从i开始的连续Run组合文本 combined = '' run_indices = [] for j in range(i, len(runs)): combined += runs[j].text run_indices.append(j) if placeholder in combined: # 找到完整的占位符,进行替换 before = combined[:combined.index(placeholder)] after = combined[combined.index(placeholder) + len(placeholder):] # 第一个Run设置替换后的文本 runs[i].text = before + value # 中间Run清空 for k in run_indices[1:]: runs[k].text = '' # 更新最后一个Run为after部分 if after: runs[run_indices[-1]].text = after count += 1 break return count def replace_in_tables(doc: Document, placeholder: str, value: str) -> int: """替换表格单元格中的占位符""" count = 0 for table in doc.tables: for row in table.rows: for cell in row.cells: for paragraph in cell.paragraphs: if placeholder in paragraph.text: for run in paragraph.runs: if placeholder in run.text: run.text = run.text.replace(placeholder, str(value)) count += 1 return count

段落与表格保留

替换时需要特别注意保留原有格式。直接设置paragraph.text会清空该段落中所有Run的格式信息。推荐按Run级别逐个替换,保留每个Run的字体、字号、颜色等属性。表格单元格的替换同理,需要深入到每个单元格的段落和Run中操作。对于页眉页脚中的占位符,需要单独访问doc.sections进行处理。

# 完整模板替换(保留格式) from docx import Document def full_template_replace(doc: Document, data: dict) -> None: """ 全文档模板替换,保留格式 :param doc: Document对象 :param data: 替换映射 {占位符: 值} """ # 1. 替换正文段落 for paragraph in doc.paragraphs: for placeholder, value in data.items(): for run in paragraph.runs: if placeholder in run.text: run.text = run.text.replace(placeholder, str(value)) # 2. 替换表格 for table in doc.tables: for row in table.rows: for cell in row.cells: for paragraph in cell.paragraphs: for placeholder, value in data.items(): for run in paragraph.runs: if placeholder in run.text: run.text = run.text.replace(placeholder, str(value)) # 3. 替换页眉页脚 for section in doc.sections: # 页眉 for paragraph in section.header.paragraphs: for placeholder, value in data.items(): for run in paragraph.runs: if placeholder in run.text: run.text = run.text.replace(placeholder, str(value)) # 页脚 for paragraph in section.footer.paragraphs: for placeholder, value in data.items(): for run in paragraph.runs: if placeholder in run.text: run.text = run.text.replace(placeholder, str(value)) # 4. 替换文本框(Word Art对象中的文本) # 需要访问底层XML,略复杂,但基础场景不需要 print(f"模板替换完成,共处理 {len(data)} 个占位符字段")

五、docxtpl库

docxtpl是目前Python生态中最成熟的Word模板引擎库。它在python-docx的基础上引入了Jinja2模板引擎语法,让模板的编写更加简洁强大。使用docxtpl时,你直接在Word文档中编写Jinja2语法标记,比如{{ name }}用于简单替换,{% for item in items %}{{ item.name }}{% endfor %}用于循环生成表格行或列表项,{% if gender == '男' %}先生{% else %}女士{% endif %}用于条件内容控制。

相比纯python-docx的手动替换,docxtpl的优势非常明显:模板逻辑放在Word文档中而非代码中,业务人员可以直接修改模板而无需触碰代码;支持循环(for)和条件(if)等高级语法,可以处理复杂多变的文档结构;内置过滤器(如{{ date | datetimeformat('YYYY年MM月DD日') }})让数据格式化更加便捷。

安装与基本使用

# 安装docxtpl # pip install docxtpl from docxtpl import DocxTemplate from datetime import datetime # 加载模板 doc = DocxTemplate("contract_template.docx") # 准备上下文数据 context = { "company_name": "上海佼艾科技有限公司", "employee_name": "张三", "id_number": "310101199001011234", "start_date": "2025年01月01日", "end_date": "2028年12月31日", "probation_months": 3, "position": "高级Python开发工程师", "department": "技术研发部", "base_salary": "15000.00", "contract_type": "固定期限", "sign_date": datetime.now().strftime("%Y年%m月%d日"), } # 渲染模板 doc.render(context) # 保存文档 doc.save("output/张三_劳动合同.docx") print("合同生成完成!")

循环与条件模板

# Word模板中的Jinja2语法示例 # 以下标记直接写在Word文档中 """ 员工培训记录表 | 序号 | 培训课程 | 培训日期 | 学时 | |------|---------|---------|------| {% for item in training_records %} | {{ loop.index }} | {{ item.course_name }} | {{ item.training_date }} | {{ item.hours }} | {% endfor %} ---------------------------------------- {% if position_level == '高级' or position_level == '资深' %} 享受额外住房补贴:¥{{ housing_allowance }}/月 {% elif position_level == '中级' %} 享受交通补贴:¥{{ transport_allowance }}/月 {% else %} 享受餐饮补贴:¥{{ meal_allowance }}/月 {% endif %} ---------------------------------------- 签发日期:{{ issue_date | default('待定', true) }} 备注:{{ remark | default('无', true) }} """ # Python代码 from docxtpl import DocxTemplate doc = DocxTemplate("report_template.docx") context = { "training_records": [ {"course_name": "Python高级编程", "training_date": "2025-01-15", "hours": 16}, {"course_name": "数据库优化", "training_date": "2025-03-20", "hours": 8}, {"course_name": "项目管理", "training_date": "2025-06-10", "hours": 24}, ], "position_level": "高级", "housing_allowance": 3000, "issue_date": "2025年07月01日", } doc.render(context) doc.save("output/培训报告.docx")

自定义过滤器

# docxtpl自定义过滤器 from docxtpl import DocxTemplate import re # 定义自定义过滤器 def money_format(value): """金额格式化:加千分位分隔符""" try: num = float(value) return f"{num:,.2f}" except (ValueError, TypeError): return str(value) def id_mask(value): """身份证号脱敏:只显示前6位和后4位""" if len(value) == 18: return value[:6] + "********" + value[-4:] return value def chinese_number(value): """阿拉伯数字转中文数字""" mapping = {'0': '零', '1': '一', '2': '二', '3': '三', '4': '四', '5': '五', '6': '六', '7': '七', '8': '八', '9': '九'} return ''.join(mapping.get(c, c) for c in str(value)) # 注册并使用过滤器 doc = DocxTemplate("invoice_template.docx") # 将过滤器添加到Jinja2环境 doc.jinja_env.filters['money'] = money_format doc.jinja_env.filters['id_mask'] = id_mask doc.jinja_env.filters['chinese'] = chinese_number context = { "invoice_amount": 1234567.89, "customer_id": "310101199001011234", "quantity": 5, } doc.render(context) doc.save("output/invoice.docx") # 在Word模板中使用: # 金额:¥{{ invoice_amount | money }} # 客户身份证:{{ customer_id | id_mask }} # 数量(大写):{{ quantity | chinese }}份

六、邮件合并高级

邮件合并(Mail Merge)源自Microsoft Word的一项经典功能,它允许用户从一个数据源批量生成格式相同的文档。Python实现邮件合并不仅可以摆脱对Microsoft Office的依赖,还能实现比原生Mail Merge更灵活的功能:支持更复杂的数据处理逻辑、可以集成外部API获取实时数据、能够跨平台运行(Linux/macOS均可使用)。

高级邮件合并方案需要处理几个典型难题:嵌套表格的生成(如合同中的付款计划表包含在条款表格中)、多语言模板(同一模板根据用户语言偏好生成不同语言版本)、条件内容(根据数据值决定显示或隐藏某段文本)、以及图片的动态插入(如电子签名、产品照片、证件照等)。

嵌套表格处理

# 嵌套表格模板处理 from docxtpl import DocxTemplate # 假设Word模板中包含以下结构: """ 付款条款: {% for term in payment_terms %} {{ term.term_name }}: 付款金额:¥{{ term.amount | money }} 付款截止日:{{ term.due_date }} 付款方式:{{ term.payment_method }} {% endfor %} 开票信息: | 发票类型 | 税率 | 金额 | |---------|------|------| {% for invoice in invoices %} | {{ invoice.type }} | {{ invoice.tax_rate }} | {{ invoice.amount | money }} | {% endfor %} """ doc = DocxTemplate("payment_contract_template.docx") context = { "payment_terms": [ {"term_name": "首付款", "amount": 50000, "due_date": "2025-02-01", "payment_method": "银行转账"}, {"term_name": "中期款", "amount": 30000, "due_date": "2025-05-01", "payment_method": "银行转账"}, {"term_name": "尾款", "amount": 20000, "due_date": "2025-08-01", "payment_method": "银行转账"}, ], "invoices": [ {"type": "增值税专用发票", "tax_rate": "13%", "amount": 50000}, {"type": "增值税专用发票", "tax_rate": "13%", "amount": 30000}, {"type": "增值税专用发票", "tax_rate": "13%", "amount": 20000}, ], } doc.render(context) doc.save("output/payment_contract.docx") print("含嵌套表格的合同生成完成!")

图片动态插入

# 模板中动态插入图片 from docxtpl import DocxTemplate, InlineImage from docx.shared import Inches, Mm, Emu def generate_with_images(template_path: str, output_path: str, context: dict, image_mappings: dict): """ 模板动态插入图片 :param template_path: docxtpl模板路径 :param output_path: 输出路径 :param context: 文本数据上下文 :param image_mappings: 图片映射 {模板中的占位名: 图片文件路径} """ doc = DocxTemplate(template_path) # 将图片作为InlineImage对象传入上下文 for placeholder, img_path in image_mappings.items(): try: # 控制图片尺寸 context[placeholder] = InlineImage(doc, img_path, width=Mm(60)) except Exception as e: print(f"图片加载失败 {img_path}: {e}") context[placeholder] = "" # 图片加载失败时留空 doc.render(context) doc.save(output_path) # 使用示例 generate_with_images( template_path="certificate_template.docx", output_path="output/张三_荣誉证书.docx", context={ "name": "张三", "award": "年度优秀员工", "date": "2025年01月", "company": "上海佼艾科技有限公司", }, image_mappings={ "company_logo": "resources/logo.png", "stamp_image": "resources/stamp.png", } )

七、批量输出管理

当批量生成的文档数量很大时,输出管理就变得至关重要。良好的输出管理应该包括:合理的目录结构组织、规范的文件命名规则、以及必要时的格式转换。想象一下,一次性生成了500份劳动合同,如果所有文件都平铺在一个目录中,查找和管理将是一场噩梦。按部门、按日期、按类型建立子目录可以极大提升后期的检索效率。

文件命名是输出管理的另一个关键环节。一个好的文件名应该包含足够的信息来唯一标识该文档,比如"姓名_合同类型_日期.docx"。需要特别注意文件名中不能包含非法字符(Windows下禁止\ / : * ? " < > |等),程序必须做转义处理。另外,如果同一批次中包含不同模板生成的文档,建议在文件名中加入模板标识前缀。

输出目录组织与文件命名

# 科学的输出目录管理 import os import re from datetime import datetime class OutputManager: """输出文件管理器""" def __init__(self, base_dir: str, batch_name: str = None): self.base_dir = base_dir self.batch_name = batch_name or datetime.now().strftime("%Y%m%d_%H%M%S") self.batch_dir = f"{base_dir}/{self.batch_name}" os.makedirs(self.batch_dir, exist_ok=True) def get_sub_dir(self, sub_name: str) -> str: """获取或创建子目录""" path = f"{self.batch_dir}/{sub_name}" os.makedirs(path, exist_ok=True) return path @staticmethod def sanitize_filename(filename: str) -> str: """清理文件名中的非法字符""" # Windows非法字符: \ / : * ? " < > | sanitized = re.sub(r'[\\/:*?"<>|]', '_', filename) # 限制长度(通常文件名不超过200字符) max_len = 200 if len(sanitized) > max_len: name, ext = os.path.splitext(sanitized) sanitized = name[:max_len - len(ext)] + ext return sanitized def build_filename(self, record: dict, template_type: str = "", ext: str = ".docx") -> str: """ 根据数据构建文件名 :param record: 数据记录 :param template_type: 模板类型标识 :param ext: 文件扩展名 :return: 安全的文件名 """ name_parts = [] if template_type: name_parts.append(template_type) for key in ['name', 'employee_name', 'student_name', 'customer_name']: if key in record and record[key]: name_parts.append(str(record[key]).strip()) break name_parts.append(datetime.now().strftime("%Y%m%d")) raw_name = "_".join(name_parts) + ext return self.sanitize_filename(raw_name) def summary(self) -> dict: """生成批处理摘要信息""" total_files = 0 by_ext = {} for root, dirs, files in os.walk(self.batch_dir): total_files += len(files) for f in files: ext = os.path.splitext(f)[1].lower() by_ext[ext] = by_ext.get(ext, 0) + 1 return { "batch_dir": self.batch_dir, "total_files": total_files, "by_extension": by_ext, } # 使用示例 manager = OutputManager("output", "contracts_202501") sub_dir_tech = manager.get_sub_dir("技术部") sub_dir_market = manager.get_sub_dir("市场部") # 生成文件时指定路径 filename = manager.build_filename( {"employee_name": "张三", "department": "技术部"}, template_type="劳动合同" ) print(f"生成文件名: {filename}") print(f"保存路径: {sub_dir_tech}/{filename}")

格式转换(docx转PDF)

# docx转PDF格式转换 import os import subprocess def convert_docx_to_pdf(docx_path: str, pdf_path: str = None) -> str: """ 将docx文件转换为PDF 方案一:使用 LibreOffice 命令行(跨平台) 方案二:使用 win32com(仅Windows,需安装Microsoft Office) """ if pdf_path is None: pdf_path = docx_path.replace('.docx', '.pdf') # 方案一:LibreOffice(推荐,免费跨平台) try: cmd = [ 'soffice', '--headless', '--convert-to', 'pdf', '--outdir', os.path.dirname(pdf_path), docx_path ] subprocess.run(cmd, check=True, capture_output=True, timeout=60) print(f"转换成功: {pdf_path}") return pdf_path except (subprocess.CalledProcessError, FileNotFoundError) as e: print(f"LibreOffice转换失败: {e}") print("尝试备用方案...") # 方案二:win32com(仅Windows) try: import win32com.client word = win32com.client.Dispatch("Word.Application") word.Visible = False doc = word.Documents.Open(os.path.abspath(docx_path)) doc.SaveAs(os.path.abspath(pdf_path), FileFormat=17) # 17=PDF doc.Close() word.Quit() print(f"转换成功: {pdf_path}") return pdf_path except Exception as e: print(f"win32com转换也失败: {e}") raise def batch_convert_to_pdf(docx_dir: str, pdf_dir: str = None) -> list: """批量转换目录下所有docx文件为PDF""" if pdf_dir is None: pdf_dir = docx_dir + "_pdf" os.makedirs(pdf_dir, exist_ok=True) converted = [] for f in os.listdir(docx_dir): if f.endswith('.docx'): docx_path = f"{docx_dir}/{f}" pdf_name = f.replace('.docx', '.pdf') pdf_path = f"{pdf_dir}/{pdf_name}" try: convert_docx_to_pdf(docx_path, pdf_path) converted.append(pdf_path) except Exception as e: print(f"转换失败 {f}: {e}") print(f"批量转换完成:成功 {len(converted)}/{len(os.listdir(docx_dir))} 份") return converted

八、实战案例

理论知识最终要落实到实际业务中才有价值。本章通过四个典型的实战案例,展示批量生成Word文档技术在不同场景下的完整应用。每个案例都包含完整的代码实现和关键设计思路,可以直接参考修改后投入生产使用。这些案例涵盖了HR、教育、行政和项目管理等部门最常见的文档批量生成需求。

案例一:批量生成劳动合同

HR部门每月需要为新入职员工生成劳动合同。每份合同包含员工个人信息、岗位信息、薪资福利、合同期限、保密条款等内容。数据来源于HR系统导出的Excel花名册。关键设计要点包括:合同编号自动生成规则(部门代码+入职年份+序号)、试用期工资按正式工资的80%自动计算、不同岗位类型对应不同的合同模板条款、合同到期日期根据合同类型自动推算。

# 批量生成劳动合同完整案例 from docxtpl import DocxTemplate import pandas as pd from datetime import datetime, timedelta import os def generate_labor_contracts(excel_path: str, template_path: str, output_dir: str) -> list: """ 批量生成劳动合同 :param excel_path: 员工信息Excel :param template_path: 合同模板 :param output_dir: 输出目录 :return: 生成的文件路径列表 """ os.makedirs(output_dir, exist_ok=True) # 读取员工数据 df = pd.read_excel(excel_path, dtype=str) df = df.fillna('') # 合同编号计数器 counter = 1 outputs = [] for _, row in df.iterrows(): employee = row.to_dict() # 自动生成合同编号 dept_code = employee.get('department_code', '00') year = datetime.now().strftime("%Y") contract_no = f"HT-{dept_code}-{year}-{counter:04d}" employee['contract_no'] = contract_no counter += 1 # 计算试用期工资 base_salary = float(employee.get('base_salary', 0)) probation_salary = base_salary * 0.8 employee['probation_salary'] = f"{probation_salary:.2f}" # 计算合同到期日 if employee.get('contract_type') == '固定期限': start = datetime.strptime(employee['start_date'], "%Y-%m-%d") duration_years = int(employee.get('contract_years', 3)) end = start + timedelta(days=duration_years * 365) employee['end_date'] = end.strftime("%Y年%m月%d日") # 渲染合同 doc = DocxTemplate(template_path) doc.render(employee) # 输出文件 filename = f"{employee.get('employee_name', '未知')}_{contract_no}.docx" output_path = f"{output_dir}/{filename}" doc.save(output_path) outputs.append(output_path) print(f"已生成: {filename}") print(f"\n全部完成!共生成 {len(outputs)} 份劳动合同") return outputs

案例二:学生成绩通知书

学校教务处每学期要向家长发送成绩通知书。每份通知书包含学生基本信息、各科成绩、班级排名、教师评语、下学期开学安排等内容。数据来源于教务系统导出的成绩Excel。关键设计要点包括:根据总分自动评定等级(优秀/良好/及格/不及格)、不及格科目用红色标记、根据成绩变化生成鼓励性评语、家长会通知根据校区不同生成不同的时间和地点。

# 批量生成成绩通知书 from docxtpl import DocxTemplate import pandas as pd def generate_report_cards(excel_path: str, template_path: str, output_dir: str): """ 批量生成学生成绩通知书 """ # 读取成绩数据 df = pd.read_excel(excel_path, dtype=str) df = df.fillna('') for _, row in df.iterrows(): student = row.to_dict() # 计算总分 subjects = ['语文', '数学', '英语', '物理', '化学', '生物', '历史', '地理', '政治'] total_score = 0 subject_scores = [] for subj in subjects: score = student.get(subj, '0') try: score_val = float(score) if score else 0 total_score += score_val except ValueError: score_val = 0 subject_scores.append({ "name": subj, "score": score, "is_fail": score_val < 60 }) student['subject_scores'] = subject_scores student['total_score'] = total_score # 评定等级 if total_score >= 450: level = "优秀" elif total_score >= 360: level = "良好" elif total_score >= 240: level = "及格" else: level = "需努力" student['level'] = level # 生成评语 fail_subjects = [s['name'] for s in subject_scores if s['is_fail']] if fail_subjects: comment = (f"该生本学期学习态度端正,但在{fail_subjects}等科目上" f"尚有提升空间,建议加强课后复习和练习。") else: comment = (f"该生本学期表现优秀,各科成绩均衡发展," f"望继续保持。") student['teacher_comment'] = comment # 渲染通知书 doc = DocxTemplate(template_path) doc.render(student) filename = f"{student.get('student_name', '未知')}_成绩通知书.docx" output_path = f"{output_dir}/{filename}" doc.save(output_path) print(f"已生成: {filename}") print("全部成绩通知书生成完成!")

案例三:荣誉证书批量打印

企业年会或季度评优时,需要批量打印荣誉证书。荣誉证书通常有固定格式,只有获奖人姓名、奖项名称、颁发日期不同。证书的排版要求非常严格,字体和位置必须精确。使用Word模板配合docxtpl可以精确控制每个元素的位置。对于证书类文档,建议在模板中使用文本框或表格来固定文字位置,避免因文字长度不同导致排版错乱。

# 批量生成荣誉证书 from docxtpl import DocxTemplate, InlineImage from docx.shared import Mm, Pt import pandas as pd from datetime import datetime def generate_certificates(excel_path: str, template_path: str, output_dir: str): """ 批量生成荣誉证书 """ df = pd.read_excel(excel_path, dtype=str) df = df.fillna('') for _, row in df.iterrows(): recipient = row.to_dict() recipient['year'] = datetime.now().year recipient['month'] = datetime.now().month recipient['day'] = datetime.now().day doc = DocxTemplate(template_path) doc.render(recipient) filename = f"{recipient.get('name', '未知')}_{recipient.get('award', '荣誉证书')}.docx" output_path = f"{output_dir}/{filename}" doc.save(output_path) print(f"已生成: {filename}") print("所有荣誉证书生成完成!") # 数据源Excel格式示例: """ | name | award | department | date | |------|--------------------------|-------------|------------| | 张三 | 2024年度优秀员工 | 技术研发部 | 2025-01-15 | | 李四 | 最佳团队贡献奖 | 市场部 | 2025-01-15 | | 王五 | 技术创新奖 | 产品部 | 2025-01-15 | """ # Word模板中放置占位符: """ 荣誉证书 {{ name }} 同志: 鉴于您在{{ department }}的卓越表现, 特授予您{{ award }}。 以资鼓励! {{ company_name }} {{ year }}年{{ month }}月{{ day }}日 """

案例四:会议纪要模板

项目团队每周召开例会,需要生成格式统一的会议纪要。会议纪要的特点是:部分内容固定(如会议标题、参会人员框架)、部分内容动态变化(如议题、决议事项、待办任务清单)。使用模板生成会议纪要的优点是保证格式统一、减少录入工作、自动归档管理。每次会议只需填写数据表格,程序自动生成格式规范的纪要文档。

# 批量生成会议纪要 from docxtpl import DocxTemplate from datetime import datetime def generate_meeting_minutes(meeting_data: dict, template_path: str, output_dir: str): """ 生成会议纪要 :param meeting_data: 会议数据字典 """ # 准备模板数据 context = { "meeting_title": meeting_data.get("title", "工作例会"), "meeting_date": meeting_data.get("date", datetime.now().strftime("%Y年%m月%d日")), "meeting_time": meeting_data.get("time", "14:00-16:00"), "location": meeting_data.get("location", "会议室A"), "chairman": meeting_data.get("chairman", ""), "attendees": meeting_data.get("attendees", []), # 列表 "absentees": meeting_data.get("absentees", []), # 列表 "agenda_items": meeting_data.get("agenda_items", []), # 每项含: subject, discussion, conclusion "action_items": meeting_data.get("action_items", []), # 每项含: task, owner, deadline "next_meeting": meeting_data.get("next_meeting", "待定"), "recorder": meeting_data.get("recorder", ""), } # 渲染模板 doc = DocxTemplate(template_path) doc.render(context) # 输出 filename = f"会议纪要_{meeting_data.get('date', datetime.now().strftime('%Y%m%d'))}.docx" output_path = f"{output_dir}/{filename}" doc.save(output_path) return output_path # 使用示例 meeting = { "title": "2025年1月第2周项目例会", "date": "2025年1月13日", "time": "14:00-15:30", "location": "3楼大会议室", "chairman": "王经理", "attendees": ["张三", "李四", "王五", "赵六"], "absentees": ["钱七(请假)"], "agenda_items": [ { "subject": "上周工作回顾", "discussion": "各成员汇报上周工作进展,项目整体进度正常。", "conclusion": "继续保持当前进度,重点关注测试环节。" }, { "subject": "本周任务分配", "discussion": "根据项目计划分解本周任务,确认各模块负责人。", "conclusion": "张三负责前端模块开发,李四负责后端接口。" } ], "action_items": [ {"task": "完成用户模块接口开发", "owner": "李四", "deadline": "2025-01-15"}, {"task": "编写API文档", "owner": "赵六", "deadline": "2025-01-17"}, {"task": "部署测试环境", "owner": "王五", "deadline": "2025-01-14"}, ], "next_meeting": "2025年1月20日 14:00", "recorder": "张三", } output = generate_meeting_minutes(meeting, "meeting_template.docx", "output/meetings") print(f"会议纪要已生成: {output}")