← 返回自动化办公目录
← 返回学习笔记首页
专题: 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("依赖库安装完整,准备就绪!")
二、占位符模板
模板是批量生成文档的核心资产。一个好的模板应该遵循"内容与样式分离"的原则:样式在模板中固定好,内容通过占位符动态替换。模板设计的好坏直接决定了批量生成的效率和最终文档的质量。设计模板时,首先要确定哪些内容是固定不变的(如公司名称、条款文本、表格结构),哪些内容是需要动态替换的(如姓名、日期、金额、编号)。
模板设计原则
样式一致性: 模板中预设好字体、字号、颜色、段落间距、页眉页脚等格式,确保所有生成文档风格统一
占位符可识别: 使用明显且唯一的标记符(如{{name}}),避免与正文中自然出现的花括号或类似符号冲突
预留扩展空间: 考虑未来可能增加的字段,模板设计时留有余地
表格和列表模板化: 模板中的表格应包含一行占位符行,程序据此复制扩展
图片占位处理: 头像、签名等图片位置用特定占位符标记,程序中替换为实际图片
占位符规范
推荐使用{{字段名}}双花括号风格作为占位符,这是目前最通用的方案。字段名应采用驼峰命名或下划线命名,如{{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}")