简介:这份资源是一款面向行政、人事、财务等日常办公人群的Excel批量生成小工具,用于解决按名单逐条生成独立表格的重复劳动问题。使用者只需维护好名单与模板两个表格,工具便会按每一行数据单独生成一个Excel文件,文件名同样可在名单中自定义,生成结果统一存放在指定文件夹内,适用于考核表、工资单等场景的批量制作。压缩包共8个文件,约27.67MB,包含6个xlsx表格文件(模板、名单及生成结果示例)、1个html使用说明和1个exe可执行程序,结构简洁、开箱即用。目前已有2562人学习下载,说明其在自动办公场景中具备一定实用价值。读者拿到后可直接修改模板内容与名单数据,快速产出成批文件,省去逐份复制粘贴的繁琐,让日常表格处理更轻松高效。
1. 按模板批量生成 Excel 文件:从手工复制到脚本化的一次性解法
如果你每个月都要把同一份 Excel 模板复制几十上百遍,再把姓名、部门、金额这些字段一个个填进去,那你大概率已经体会过那种「复制到手指发麻、改到眼睛发花」的感觉。按模板批量生成 Excel 文件这件事,本质上是把「一份模板 + 一份数据表」映射成「N 份结构一致、内容不同的成品文件」,它解决的不是算法难题,而是重复劳动和人为出错。适合谁?做行政汇总、财务对账、教务名单、销售分区域报表的从业者,只要你的工作里存在「固定格式 + 变化数据」的组合,这套思路就能直接套用。我见过太多人第一反应是录宏,但宏的坑在于模板一改就崩,而脚本化方案改一行配置就能重跑,这才是它值得投入的原因。
2. 先想清楚数据源和模板的对应关系,再动手写代码
2.1 为什么推荐「数据表驱动」而不是「模板里写死占位符」
批量生成最容易翻车的地方,不是代码写不出来,而是数据源和模板的字段对不上。常见做法有两种:一种是在模板单元格里写{{姓名}}这类占位符,脚本读取后替换;另一种是把数据单独放一张表,脚本按列名去模板里找对应位置。我一般会选第二种,原因是占位符方案一旦模板被同事手动调整过,占位符被删掉或者挪位,脚本就会静默失败——它找不到占位符,可能什么都不替换,最后生成一堆空文件,你还得逐个打开检查。
数据表驱动的逻辑是:模板里保留固定的表头文字,脚本用「表头文字 → 列索引」建立映射,再从数据表里按列名取值填入。这样即使模板列顺序变了,只要表头文字没改,脚本依然能定位。代价是模板表头必须规范,不能有合并单元格盖住表头,也不能有重复表头。这个约束在落地时反而帮你把模板标准化了,长期看是好事。
2.2 用 openpyxl 读取模板并建立表头映射
先看读取模板、建立表头到列号映射的代码。这里假设模板第一行是表头,数据从第二行开始写。
from openpyxl import load_workbook from openpyxl.utils import get_column_letter def build_header_map(ws, header_row=1): """读取模板表头行,返回 {表头文字: 列号} 的字典""" header_map = {} for col_idx in range(1, ws.max_column + 1): cell_value = ws.cell(row=header_row, column=col_idx).value if cell_value is not None: # 去掉首尾空格,避免模板里手滑多敲空格导致匹配失败 key = str(cell_value).strip() if key in header_map: raise ValueError(f"模板存在重复表头: {key}") header_map[key] = col_idx return header_map # 使用示例 wb = load_workbook("template.xlsx") ws = wb.active header_map = build_header_map(ws) print(header_map) # {'姓名': 1, '部门': 2, '金额': 3}这段代码的关键点有三个。第一,header_row参数默认是 1,如果你的模板前几行是标题或说明,需要手动改成实际表头所在行。第二,strip()处理空格是血泪经验,模板里表头带空格的情况太常见了,不处理就会匹配不上。第三,重复表头直接抛异常而不是覆盖,因为重复表头意味着你的映射有歧义,早报错比晚出错好。get_column_letter这里没用到,但后面写单元格时如果需要按列字母定位会用到,先导入备用。
2.3 从数据源逐行取值并写入新文件
有了表头映射,接下来就是遍历数据行、复制模板、填值、保存。这里用 pandas 读数据源,因为它对 CSV 和 Excel 都友好。
import pandas as pd from openpyxl import load_workbook import os def batch_generate(template_path, data_path, output_dir, sheet_name=None): """按模板批量生成 Excel 文件""" os.makedirs(output_dir, exist_ok=True) df = pd.read_excel(data_path, dtype=str) # 全部按字符串读,避免数字被科学计数 df = df.fillna("") # 空值统一成空字符串,防止写入 None for idx, row in df.iterrows(): wb = load_workbook(template_path) ws = wb.active if sheet_name is None else wb[sheet_name] header_map = build_header_map(ws) for col_name, col_idx in header_map.items(): if col_name in df.columns: value = row[col_name] ws.cell(row=2, column=col_idx, value=value) # 数据源里没有的列,保持模板原样,不覆盖 # 文件名用第一列的值,做非法字符替换 first_col = df.columns[0] raw_name = str(row[first_col]) safe_name = "".join(c for c in raw_name if c not in r'\/:*?"<>|') out_path = os.path.join(output_dir, f"{safe_name}.xlsx") wb.save(out_path) wb.close() print(f"共生成 {len(df)} 个文件,输出目录: {output_dir}")逻辑说明:每次循环都重新load_workbook,这是为了确保每个文件都从干净的模板开始,避免上一个文件的数据残留。dtype=str很重要,否则像工号「001」这种会被读成整数 1,写进去就丢了前导零。fillna("")处理空单元格,不然写入 None 会让单元格显示异常。文件名过滤非法字符是必须的,Windows 下文件名不能包含\ / : * ? " < > |,不处理就会保存失败。sheet_name参数留出来是因为有些模板有多个 sheet,需要指定往哪个写。
参数方面,template_path和data_path建议用绝对路径,相对路径在定时任务里容易因为工作目录变化而找不到文件。output_dir会自动创建,不用手动建。如果数据量超过几千行,每次重新加载模板会变慢,可以考虑用copy模块深拷贝已加载的 workbook,但要注意 openpyxl 的深拷贝有坑,稳妥起见还是重新加载。
3. 把脚本变成能日常用的工具:参数、日志和异常处理
3.1 用 argparse 暴露关键参数,避免改代码
脚本能跑通只是第一步,要让它变成同事也能用的工具,得把路径、表头行号这些做成命令行参数。
import argparse def parse_args(): parser = argparse.ArgumentParser(description="按模板批量生成Excel文件") parser.add_argument("--template", required=True, help="模板文件路径") parser.add_argument("--data", required=True, help="数据源文件路径") parser.add_argument("--out", required=True, help="输出目录") parser.add_argument("--header-row", type=int, default=1, help="模板表头所在行号") parser.add_argument("--sheet", default=None, help="模板中要写入的sheet名") return parser.parse_args() if __name__ == "__main__": args = parse_args() batch_generate(args.template, args.data, args.out, args.sheet)这样调用就是python gen.py --template t.xlsx --data d.xlsx --out ./output。--header-row暴露出来是因为不同模板表头位置不一样,写死 1 迟早会遇到表头在第 3 行的模板。--sheet默认 None 表示用活动 sheet,多 sheet 模板必须指定。
3.2 加日志和失败续跑,别让一个坏数据毁掉整批
批量任务最怕跑到一半崩了,前面生成的文件不知道哪些成功哪些失败。加一个简单的日志和跳过机制。
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", handlers=[logging.FileHandler("batch.log", encoding="utf-8"), logging.StreamHandler()] ) def batch_generate_safe(template_path, data_path, output_dir, sheet_name=None): os.makedirs(output_dir, exist_ok=True) df = pd.read_excel(data_path, dtype=str).fillna("") success, failed = 0, [] for idx, row in df.iterrows(): try: wb = load_workbook(template_path) ws = wb.active if sheet_name is None else wb[sheet_name] header_map = build_header_map(ws) for col_name, col_idx in header_map.items(): if col_name in df.columns: ws.cell(row=2, column=col_idx, value=row[col_name]) first_col = df.columns[0] safe_name = "".join(c for c in str(row[first_col]) if c not in r'\/:*?"<>|') out_path = os.path.join(output_dir, f"{safe_name}.xlsx") wb.save(out_path) wb.close() success += 1 logging.info(f"第{idx+2}行生成成功: {safe_name}") except Exception as e: failed.append((idx + 2, str(e))) logging.error(f"第{idx+2}行生成失败: {e}") logging.info(f"完成: 成功{success}个, 失败{len(failed)}个") for line_no, err in failed: logging.warning(f"失败行号{line_no}: {err}")这里把每行的异常都捕获住,记录行号和错误信息,继续跑下一行。行号用idx+2是因为 idx 从 0 开始,数据从 Excel 第 2 行开始。日志同时输出到文件和控制台,方便事后排查。失败列表最后统一打印,你可以拿着行号去数据源里定位问题数据。
3.3 输出文件命名和覆盖策略
命名冲突是另一个高频问题。如果数据源里第一列有重复值,后生成的文件会覆盖前面的。常见做法是加序号后缀。
seen = {} base_name = safe_name if base_name in seen: seen[base_name] += 1 safe_name = f"{base_name}_{seen[base_name]}" else: seen[base_name] = 0这段放在保存前。seen字典记录每个基础名出现次数,重复时加_1、_2。如果你希望重复时直接报错而不是自动改名,把这段换成检查os.path.exists然后抛异常即可。两种策略没有优劣,看你的业务是否允许重名。
4. 批量生成 Excel 的避坑清单:这五个坑我踩过
4.1 坑一:模板里的公式和格式被破坏
现象:生成的文件里公式变成了静态值,或者条件格式、数据验证全没了。原因:openpyxl 加载再保存时,对某些复杂格式的支持不完整,尤其是跨 sheet 引用和部分条件格式。解决:如果模板格式复杂,改用xlwings或win32com调用 Excel 本体来操作,它们能保留完整格式,代价是必须装 Excel 且速度慢。或者把格式和公式固化在模板里,脚本只填数据不碰格式区域。
4.2 坑二:数字前导零丢失
现象:工号「001」写进去变成「1」。原因:pandas 默认把数字列推断成 int,前导零被吃掉。解决:pd.read_excel(dtype=str)强制按字符串读,或者在 openpyxl 写入时把单元格格式设成文本ws.cell(...).number_format = '@'。两个一起用最稳。
4.3 坑三:日期变成一串数字
现象:数据源里的日期写进模板后显示成 45000 这种序列号。原因:Excel 内部用序列号存日期,openpyxl 写入字符串时不会自动转换。解决:在数据源里把日期统一格式化成YYYY-MM-DD字符串再写入,或者写入 datetime 对象并设置number_format = 'yyyy-mm-dd'。我一般选前者,因为字符串最不容易出意外。
4.4 坑四:文件被占用导致保存失败
现象:报PermissionError,提示文件被另一个程序占用。原因:生成的文件正被打开,或者上一次循环的 workbook 没关闭。解决:确保每次wb.close(),输出目录不要用正在同步的网盘目录。如果确实需要覆盖已打开的文件,先检测并提示用户关闭,不要硬写。
4.5 坑五:数据源列名和模板表头差一个空格
现象:脚本跑完没报错,但某些列是空的。原因:数据源列名是「姓名」,模板表头是「姓名 」(带尾空格),映射时对不上。解决:读取数据源后统一df.columns = df.columns.str.strip(),模板表头也 strip,两边都规范化。这个坑最隐蔽,因为不报错,只能靠生成后抽查发现。
5. 进阶:把生成结果做一次自动校验,别让错误文件流出去
脚本跑完不代表任务结束。我现在的习惯是生成后加一步校验,用 openpyxl 重新打开每个生成的文件,检查关键字段是否非空、行数是否正确。这一步能拦住 90% 的静默失败。
def verify_output(output_dir, required_cols): """校验输出目录下所有文件的关键列是否非空""" problems = [] for fname in os.listdir(output_dir): if not fname.endswith(".xlsx"): continue fpath = os.path.join(output_dir, fname) wb = load_workbook(fpath, read_only=True) ws = wb.active header_map = build_header_map(ws) for col in required_cols: if col not in header_map: problems.append(f"{fname}: 缺少列 {col}") continue val = ws.cell(row=2, column=header_map[col]).value if val is None or str(val).strip() == "": problems.append(f"{fname}: 列 {col} 为空") wb.close() return problems # 使用 issues = verify_output("./output", ["姓名", "金额"]) for i in issues: print(i)read_only=True让打开速度更快,适合只读校验。required_cols传你业务上不能为空的列名。校验结果打印出来后,你可以决定是人工修数据重跑,还是直接拦截这批文件不发出去。
再进一步,如果你需要把生成的文件按部门分目录存放,可以在保存前根据某一列的值建子目录:
dept = str(row.get("部门", "未分类")).strip() or "未分类" dept_dir = os.path.join(output_dir, dept) os.makedirs(dept_dir, exist_ok=True) out_path = os.path.join(dept_dir, f"{safe_name}.xlsx")这样输出目录就是按部门分好的文件夹,分发时直接整个文件夹发出去,省掉二次整理。参数上注意row.get在 pandas Series 上可用,但更稳妥的是先判断列是否存在。
最后说一个我自己的教训:早期我图省事,把模板路径和数据路径写死在脚本里,结果换台机器就得改代码,后来统一改成命令行参数加一个config.yaml存默认值,才算真正能交付给别人用。批量生成这件事,代码本身不难,难的是把边界情况想全。每次跑完抽查三五个文件,比事后返工便宜得多。希望帮到你。
本文还有配套的精品资源,点击获取