🚀 批量Excel复制填充工具:一键告别重复劳动!!
大家好!今天我要分享一个我自己开发的实用小工具——批量Excel复制填充工具。如果你经常需要根据一份数据源(如CSV或Excel)为每个人/每条记录生成一份独立的Excel文件(比如成绩单、合同、通知单等),那么这个工具绝对能帮你省下大量时间!
💡 为什么开发这个工具?
在日常办公中,我们常常遇到这样的场景:
- 需要为100个学生生成个性化的成绩单
- 为500个客户制作专属的报价单
- 为每个部门生成定制化的报表
传统做法是手动复制模板,然后逐个填写数据,不仅效率低下还容易出错。于是我开发了这个自动化工具,只需简单配置,就能一键批量生成所有文件!
🛠️ 工具功能特点
✅ 支持多种数据源:CSV和Excel文件都可以作为数据源
✅ 可视化操作界面:无需编程基础,图形界面操作简单直观
✅ 灵活字段映射:可以将数据源中的任意列映射到模板的任意单元格
✅ 智能文件命名:支持自定义文件名前缀,并可从数据源中选择列作为文件名后缀
✅ 实时进度反馈:处理过程中显示进度条和详细日志,随时了解处理状态
✅ 错误处理机制:即使部分文件生成失败,也不会影响其他文件的处理
📥 使用方法
1. 准备工作
- 数据源文件:包含所有需要填充的数据(CSV或Excel格式)
- 模板文件:设计好的Excel模板,留出需要填充数据的单元格位置
- 输出文件夹:指定生成文件的保存位置
2. 操作步骤
- 选择文件:分别选择数据源文件、模板文件和输出文件夹
- 配置文件名:
- 设置文件名前缀(可选)
- 选择用于生成文件名后缀的数据列
- 设置字段映射:
- 选择数据源中的列
- 输入模板中对应的单元格地址(如A1、B2等)
- 点击"添加映射"按钮
- 开始处理:点击"开始处理"按钮,等待批量生成完成!
2.0版
3.4版
3.4版本
🎯 实际应用场景
场景1:学生成绩单生成
- 数据源:包含学生姓名、各科成绩的Excel表格
- 模板:设计好的成绩单模板
- 结果:为每个学生生成一份以姓名命名的成绩单文件
场景2:客户报价单制作
- 数据源:客户信息和产品报价数据
- 模板:公司标准报价单模板
- 结果:为每个客户生成个性化的报价单
场景3:员工绩效考核表
- 数据源:员工基本信息和考核数据
- 模板:绩效考核表模板
- 结果:为每位员工生成专属的考核表
💻 技术实现
工具基于Python开发,使用了以下技术栈:
- GUI框架:
ttkbootstrap - 提供现代化的界面外观
- Excel处理:
openpyxl - 用于读写Excel文件
- CSV处理:内置
csv模块 - 支持CSV文件读取
- 文件操作:
os模块 - 处理文件路径和目录操作
🔧 安装依赖
pip install ttkbootstrap openpyxl
🔧 打包参数
pyinstaller --onefile --noconsole --upx-dir=upx-5.0.2-win64 --ico=excel_batch_filler.ico --clean excel_batch_filler.py
📝 注意事项
- Excel文件必须是
.xlsx格式(不支持.xls)
- 数据源文件的第一行必须是列标题
- 单元格地址输入时不区分大小写(会自动转换为大写)
- 文件名会自动过滤掉非法字符,确保生成的文件名合法
🎁 结语
这个工具已经在我自己的工作中发挥了巨大作用,希望能帮助到更多有类似需求的朋友!如果你有任何建议或遇到问题,欢迎在评论区留言讨论。
告别重复劳动,让自动化为你工作!
适用人群:行政人员、HR、教师、财务人员、数据分析人员等需要批量处理Excel的用户
小贴士:使用前建议先用少量数据测试,确保映射配置正确后再进行大批量处理哦!
✅下载地址
2.0版 支持完整保留 模板Excel 单元格格式(包括数字格式如 ¥###0.00、边框、颜色、公式等)
感谢19楼朋友@tz_being的反馈{:1_921:}
3.2版 1.生成的文件名可以是列标题组合;2.增加导入导出配置3.左右布局,将日志区放到右边
感谢46楼朋友 @alex178、55楼朋友@zainali的反馈
https://wwbpf.lanzout.com/i2V5U3ev6zkf 密码:52pj
3.4版 适配14寸小屏(1366x768);增加选择模版工作表
https://wwbpf.lanzout.com/iOh5b3f24ecb 密码:52pj
包含:
excel_batch_filler_3.4.py
excel_batch_filler_3.4-win32.exe
excel_batch_filler_3.4-win64.exe
📄3.4版代码源码如下
import csv
import json
import os
import tkinter as tk
from copy import copy
from tkinter import filedialog, messagebox
from typing import Dict, List, Tuple
import ttkbootstrap as ttk
from openpyxl import load_workbook
from ttkbootstrap.constants import *
class ExcelBatchFillerApp:
"""
Excel批量填充工具主应用类
实现从CSV/XLSX文件读取数据,填充到Excel模板并生成多个文件的功能
"""
def __init__(self, root: ttk.Window):
"""初始化应用"""
self.root = root
self.root.geometry("1366x768") # 适配14寸小屏
# 创建主框架
self.main_frame = ttk.Frame(root)
self.main_frame.pack(fill=tk.BOTH, expand=True)
# 左侧功能区
self.left_frame = ttk.Frame(self.main_frame, width=900) # 减小左侧宽度
self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5, pady=3)
self.left_frame.pack_propagate(False) # 保持固定宽度
# 右侧日志区
self.right_frame = ttk.Frame(self.main_frame)
self.right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=3)
self.right_frame.pack_propagate(False) # 占据剩余空间
# 初始化变量
self.source_file_path = tk.StringVar() # 源文件路径
self.template_file_path = tk.StringVar() # 模板文件路径
self.output_directory_path = tk.StringVar() # 输出目录路径
self.file_name_prefix = tk.StringVar() # 文件名前缀
self.field_delimiter = tk.StringVar(value="_") # 字段连接符
self.file_name_fields: List[str] = [] # 文件名字段列表
self.field_mappings: List[Tuple[str, str, str]] = [] # 字段映射列表 (来源字段, 工作表, 单元格)
self.template_worksheets: List[str] = [] # 模板工作表列表
# 构建用户界面
self.setup_user_interface()
# 尝试加载默认配置
self.load_default_configuration()
def setup_user_interface(self) -> None:
"""构建用户界面"""
# 左侧功能区
self.create_file_selection_section()
self.create_filename_settings_section()
self.create_field_mapping_section()
self.create_processing_section()
# 右侧日志区
self.create_configuration_section()
self.create_log_section()
def create_file_selection_section(self) -> None:
"""创建文件选择区域"""
file_frame = ttk.Labelframe(self.left_frame, text="文件与输出设置", padding=10)
file_frame.pack(fill=tk.X, padx=5, pady=3)
self.create_file_selection_row(file_frame, "选择汇总文件", self.source_file_path, self.select_source_file)
self.create_file_selection_row(file_frame, "选择模板文件", self.template_file_path, self.select_template_file)
self.create_file_selection_row(file_frame, "选择输出文件夹", self.output_directory_path, self.select_output_directory)
def create_file_selection_row(self, parent_frame, button_text: str, variable: tk.StringVar, command_func) -> None:
"""创建文件选择行"""
row_frame = ttk.Frame(parent_frame)
row_frame.pack(fill=tk.X, pady=3) # 减小间距
ttk.Button(row_frame, text=button_text, command=command_func).pack(side=tk.LEFT, padx=5)
ttk.Entry(row_frame, textvariable=variable, state="readonly", width=50).pack( # 减小宽度
side=tk.LEFT, fill=tk.X, expand=True
)
def create_filename_settings_section(self) -> None:
"""创建文件名设置区域"""
filename_frame = ttk.Labelframe(self.left_frame, text="生成文件名设置", padding=10)
filename_frame.pack(fill=tk.X, padx=5, pady=3)
# 自定义前缀与连接符设置框架
user_defined_frame = ttk.Frame(filename_frame)
user_defined_frame.pack(fill=tk.X, pady=3)
# 文件名前缀设置
ttk.Label(user_defined_frame, text="自定义前缀:").pack(side=tk.LEFT)
ttk.Entry(user_defined_frame, textvariable=self.file_name_prefix, width=20).pack(side=tk.LEFT, fill=tk.X)
# 前缀变化时更新预览
self.file_name_prefix.trace_add("write", lambda *args: self.update_filename_preview())
# 字段连接符设置
ttk.Label(user_defined_frame, text="字段连接符:").pack(side=tk.LEFT, padx=(20,5))
ttk.Entry(user_defined_frame, textvariable=self.field_delimiter, width=3).pack(side=tk.LEFT)
# 连接符变化时更新预览
self.field_delimiter.trace_add("write", lambda *args: self.update_filename_preview())
# 字段组合设置(多选)
field_combo_frame = ttk.Frame(filename_frame)
field_combo_frame.pack(fill=tk.X, pady=3)
ttk.Label(field_combo_frame, text="选择字段(可多选):").pack(side=tk.LEFT)
self.all_columns_combobox = ttk.Combobox(field_combo_frame, state="readonly", width=15) # 减小宽度
self.all_columns_combobox.pack(side=tk.LEFT, padx=5)
ttk.Button(field_combo_frame, text="添加", command=self.add_filename_field).pack(side=tk.LEFT, padx=5)
ttk.Button(field_combo_frame, text="清除", command=self.clear_filename_fields).pack(side=tk.LEFT, padx=5)
# 列表框显示已添加的文件名字段
listbox_frame = ttk.Frame(filename_frame)
listbox_frame.pack(fill=tk.BOTH, expand=True, pady=3)
self.filename_field_listbox = tk.Listbox(listbox_frame, height=3, selectmode=tk.SINGLE) # 减小高度
self.filename_field_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar1 = ttk.Scrollbar(listbox_frame, orient=tk.VERTICAL, command=self.filename_field_listbox.yview)
scrollbar1.pack(side=tk.RIGHT, fill=tk.Y)
self.filename_field_listbox.config(yscrollcommand=scrollbar1.set)
# 文件名预览
preview_frame = ttk.Frame(filename_frame)
preview_frame.pack(fill=tk.BOTH, expand=True, pady=3)
self.preview_label = ttk.Label(preview_frame, text="示例文件名:", foreground="#555")
self.preview_label.pack(fill=tk.X, pady=3)
def create_field_mapping_section(self) -> None:
"""创建字段映射区域"""
mapping_frame = ttk.Labelframe(self.left_frame, text="字段映射 (来源字段 → 模板工作表.单元格)", padding=10)
mapping_frame.pack(fill=tk.X, padx=5, pady=3)
# 来源字段设置框架
config_frame = ttk.Frame(mapping_frame)
config_frame.pack(fill=tk.X, pady=3)
ttk.Label(config_frame, text="来源字段:").pack(side=tk.LEFT, padx=(0,5))
self.source_column_combobox = ttk.Combobox(config_frame, state="readonly", width=12)
self.source_column_combobox.pack(side=tk.LEFT, padx=5)
# 目标工作表和单元格设置框架
config2_frame = ttk.Frame(mapping_frame)
config2_frame.pack(fill=tk.X, pady=3)
ttk.Label(config2_frame, text="模板工作表:").pack(side=tk.LEFT, padx=(0,5))
self.destination_worksheet_combobox = ttk.Combobox(config2_frame, state="readonly", width=12)
self.destination_worksheet_combobox.pack(side=tk.LEFT, padx=5)
# 选择模板文件后,自动加载工作表列表
self.template_file_path.trace_add("write", lambda *args: self.refresh_template_worksheets())
ttk.Label(config2_frame, text="模板单元格:").pack(side=tk.LEFT, padx=5)
self.destination_cell_entry = ttk.Entry(config2_frame, width=3)
self.destination_cell_entry.pack(side=tk.LEFT, padx=5)
ttk.Button(config2_frame, text="添加", command=self.add_field_mapping).pack(side=tk.LEFT, padx=5)
ttk.Button(config2_frame, text="删除", command=self.remove_field_mapping).pack(side=tk.LEFT, padx=5)
ttk.Button(config2_frame, text="清除全部", command=self.clear_all_mappings).pack(side=tk.LEFT, padx=5)
list_frame = ttk.Frame(mapping_frame)
list_frame.pack(fill=tk.BOTH, expand=True, pady=3)
self.mapping_listbox = tk.Listbox(list_frame, height=3) # 减小高度
self.mapping_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar2 = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.mapping_listbox.yview)
scrollbar2.pack(side=tk.RIGHT, fill=tk.Y)
self.mapping_listbox.config(yscrollcommand=scrollbar2.set)
def create_processing_section(self) -> None:
"""创建处理区域"""
button_frame = ttk.Frame(self.left_frame)
button_frame.pack(pady=3) # 减小间距
ttk.Button(button_frame, text="开始处理", bootstyle=SUCCESS, command=self.start_processing).pack()
self.progress_bar = ttk.Progressbar(self.left_frame, mode="determinate")
self.progress_bar.pack(fill=tk.X, padx=5, pady=3)
def create_configuration_section(self) -> None:
"""创建配置导入导出区域"""
config_frame = ttk.Labelframe(self.right_frame, text="配置导入与导出", padding=10)
config_frame.pack(fill=tk.X, padx=5, pady=3)
ttk.Button(config_frame, text="导出配置", command=self.export_configuration).pack(side=tk.LEFT, padx=5)
ttk.Button(config_frame, text="导入配置", command=self.import_configuration).pack(side=tk.LEFT, padx=5)
def create_log_section(self) -> None:
"""创建日志区域"""
log_frame = ttk.Labelframe(self.right_frame, text="处理日志", padding=5)
log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=3)
self.log_text_widget = tk.Text(
log_frame, height=20, state=tk.DISABLED, wrap=tk.WORD, background="#f8f8f8" # 减小高度
)
scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text_widget.yview)
self.log_text_widget.configure(yscrollcommand=scrollbar.set)
self.log_text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def log_message(self, message: str) -> None:
"""记录日志消息"""
self.log_text_widget.config(state=tk.NORMAL)
self.log_text_widget.insert(tk.END, message + "\n")
self.log_text_widget.see(tk.END)
self.log_text_widget.config(state=tk.DISABLED)
self.root.update_idletasks()
def select_source_file(self) -> None:
"""选择源文件"""
file_path = filedialog.askopenfilename(
title="选择汇总文件",
filetypes=[("Excel (*.xlsx)", "*.xlsx"), ("CSV (*.csv)", "*.csv")],
)
if file_path:
self.source_file_path.set(file_path)
self.refresh_columns()
def select_template_file(self) -> None:
"""选择模板文件"""
file_path = filedialog.askopenfilename(
title="选择模板文件",
filetypes=[("Excel (*.xlsx)", "*.xlsx"
)
if file_path:
self.template_file_path.set(file_path)
self.refresh_template_worksheets() # 选择模板后刷新工作表列表
def select_output_directory(self) -> None:
"""选择输出目录"""
directory_path = filedialog.askdirectory(title="选择输出文件夹")
if directory_path:
self.output_directory_path.set(directory_path)
def refresh_columns(self) -> None:
"""刷新源文件列列表"""
source_path = self.source_file_path.get()
if not source_path or not os.path.exists(source_path):
return
try:
columns = self.get_columns_from_file(source_path)
self.all_columns_combobox["values"] = columns
self.source_column_combobox["values"] = columns
if not self.all_columns_combobox.get() and columns:
self.all_columns_combobox.current(0)
if not self.source_column_combobox.get() and columns:
self.source_column_combobox.current(0)
except Exception as exception:
messagebox.showerror("错误", f"无法读取字段:{exception}")
def refresh_template_worksheets(self) -> None:
"""刷新模板工作表列表"""
template_path = self.template_file_path.get()
if not template_path or not os.path.exists(template_path):
self.template_worksheets = []
self.destination_worksheet_combobox["values"] = []
return
try:
workbook = load_workbook(template_path, read_only=True)
self.template_worksheets = workbook.sheetnames
self.destination_worksheet_combobox["values"] = self.template_worksheets
if self.template_worksheets:
self.destination_worksheet_combobox.current(0)
else:
self.destination_worksheet_combobox.set("")
workbook.close()
except Exception as exception:
messagebox.showerror("错误", f"无法读取模板工作表:{exception}")
def get_columns_from_file(self, file_path: str) -> List[str]:
"""从文件中获取列名"""
if file_path.lower().endswith(".csv"):
with open(file_path, newline="", encoding="utf-8-sig") as file:
reader = csv.reader(file)
try:
return next(reader)
except StopIteration:
return []
else:
workbook = load_workbook(file_path, read_only=True, data_only=True)
worksheet = workbook.active
try:
header = next(worksheet.iter_rows(min_row=1, max_row=1, values_only=True))
return [str(cell).strip() if cell is not None else "" for cell in
finally:
workbook.close()
def add_filename_field(self) -> None:
"""添加文件名字段"""
selected_column = self.all_columns_combobox.get()
if not selected_column:
messagebox.showwarning("警告", "请先选择字段!")
return
if selected_column in self.file_name_fields:
messagebox.showinfo("提示", f"「{selected_column}」已在文件名字段清单中。")
return
self.file_name_fields.append(selected_column)
self.filename_field_listbox.insert(tk.END, selected_column)
self.update_filename_preview()
def clear_filename_fields(self) -> None:
"""清空文件名字段"""
self.file_name_fields.clear()
self.filename_field_listbox.delete(0, tk.END)
self.update_filename_preview()
def update_filename_preview(self) -> None:
"""
更新文件名预览
格式:{前缀}{字段1}{连接符}{字段2}... .xlsx
如果字段列表为空则显示 {前缀}record_1.xlsx
"""
prefix = self.file_name_prefix.get()
delimiter = self.field_delimiter.get()
if not delimiter: # 防止空字符串导致join错误
delimiter = ""
if self.file_name_fields:
placeholder = delimiter.join(self.file_name_fields)
else:
placeholder = "record_1"
preview_name = f"{prefix}{placeholder}.xlsx"
self.preview_label.config(text=f"示例文件名:{preview_name}")
def add_field_mapping(self) -> None:
"""添加字段映射"""
source_column = self.source_column_combobox.get()
destination_worksheet = self.destination_worksheet_combobox.get()
destination_cell = self.destination_cell_entry.get().strip().upper()
if not source_column or not destination_worksheet or not destination_cell:
messagebox.showwarning("警告", "请完整填写来源字段、模板工作表与模板单元格!")
return
self.field_mappings.append((source_column, destination_worksheet, destination_cell))
self.mapping_listbox.insert(tk.END, f"{source_column} → {destination_worksheet}.{destination_cell}")
self.destination_cell_entry.delete(0, tk.END)
def remove_field_mapping(self) -> None:
"""删除字段映射"""
selected_indices = self.mapping_listbox.curselection()
if not selected_indices:
return
selected_index = selected_indices[0
self.mapping_listbox.delete(selected_index)
del self.field_mappings[selected_index]
def clear_all_mappings(self) -> None:
"""清空所有映射"""
self.field_mappings.clear()
self.mapping_listbox.delete(0, tk.END)
def get_current_configuration(self) -> Dict:
"""获取当前配置"""
return {
"source_file_path": self.source_file_path.get(),
"template_file_path": self.template_file_path.get(),
"output_directory_path": self.output_directory_path.get(),
"file_name_prefix": self.file_name_prefix.get(),
"field_delimiter": self.field_delimiter.get(),
"file_name_fields": self.file_name_fields,
"field_mappings": self.field_mappings,
}
def export_configuration(self) -> None:
"""导出配置"""
configuration = self.get_current_configuration()
file_path = filedialog.asksaveasfilename(
defaultextension=".json",
filetypes=[("JSON文件", "*.json")],
title="导出配置文件",
)
if not file_path:
return
try:
with open(file_path, "w", encoding="utf-8") as file_pointer:
json.dump(configuration, file_pointer, ensure_ascii=False, indent=4)
self.log_message(f"✅ 配置已导出至 {os.path.basename(file_path)}")
except Exception as exception:
messagebox.showerror("错误", f"导出失败:{exception}")
def import_configuration(self) -> None:
"""导入配置"""
file_path = filedialog.askopenfilename(
filetypes=[("JSON文件", "*.json")], title="导入配置文件"
)
if not file_path:
return
self.load_configuration_from_file(file_path)
def load_configuration_from_file(self, file_path: str) -> None:
"""从文件加载配置"""
try:
with open(file_path, "r", encoding="utf-8") as file_pointer:
configuration = json.load(file_pointer)
except Exception as exception:
messagebox.showerror("错误", f"加载配置文件失败:{exception}")
return
self.source_file_path.set(configuration.get("source_file_path", ""))
self.template_file_path.set(configuration.get("template_file_path", ""))
self.output_directory_path.set(configuration.get("output_directory_path", ""))
self.file_name_prefix.set(configuration.get("file_name_prefix", ""))
self.field_delimiter.set(configuration.get("field_delimiter", "_"))
self.file_name_fields = configuration.get("file_name_fields", [])
self.filename_field_listbox.delete(0, tk.END)
for field in self.file_name_fields:
self.filename_field_listbox.insert(tk.END, field)
# 加载字段映射,兼容旧格式
raw_mappings = configuration.get("field_mappings", [])
self.field_mappings = []
for item in raw_mappings:
if isinstance(item, list) and len(item) == 3:
# 新格式: [source_col, worksheet, cell]
self.field_mappings.append(tuple(item))
elif isinstance(item, list) and len(item) == 2:
# 旧格式: [source_col, cell] -> 默认工作表
self.field_mappings.append((item[0], "Sheet1", item[1])) # 假设默认为Sheet1
else:
# 跳过无法识别的格式
continue
self.mapping_listbox.delete(0, tk.END)
for source, worksheet, destination in self.field_mappings:
self.mapping_listbox.insert(tk.END, f"{source} → {worksheet}.{destination}")
self.refresh_columns()
self.refresh_template_worksheets() # 刷新工作表列表
self.update_filename_preview()
self.log_message(f"✅ 配置已导入 {os.path.basename(file_path)}")
def load_default_configuration(self) -> None:
"""加载默认配置文件"""
default_config_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "config.json")
if os.path.isfile(default_config_path):
self.load_configuration_from_file(default_config_path)
def copy_cell_style(self, source_cell, target_cell) -> None:
"""复制单元格样式"""
for attribute in ("font", "fill", "border", "alignment", "number_format"):
try:
value = getattr(source_cell, attribute, None)
if value is None:
continue
if attribute == "fill" and getattr(value, "patternType", None) is None:
continue
setattr(target_cell, attribute, copy(value))
except Exception:
continue
def start_processing(self) -> None:
"""开始处理"""
if not all([
self.source_file_path.get(),
self.template_file_path.get(),
self.output_directory_path.get()
]):
messagebox.showerror("错误", "请先完成文件与文件夹的选择!")
return
if not self.field_mappings:
messagebox.showerror("错误", "请至少设置一笔字段映射!")
return
if not self.file_name_fields:
messagebox.showerror("错误", "请至少选择一个字段作为文件名组成!")
return
os.makedirs(self.output_directory_path.get(), exist_ok=True)
# 清空日志
self.log_text_widget.config(state=tk.NORMAL)
self.log_text_widget.delete(1.0, tk.END)
self.log_text_widget.config(state=tk.DISABLED)
try:
rows, columns = self.read_source_data()
if not rows:
messagebox.showinfo("提示", "汇总文件中无数据!")
return
self.process_data_rows(rows, columns)
except Exception as exception:
messagebox.showerror("严重错误", f"执行过程发生异常:{exception}")
self.log_message(f"❌ 严重错误:{exception}")
def read_source_data(self):
"""读取源数据"""
source_path = self.source_file_path.get()
rows = []
columns = []
if source_path.lower().endswith(".csv"):
with open(source_path, newline="", encoding="utf-8-sig") as file:
reader = csv.DictReader(file)
columns = reader.fieldnames or []
for raw_row in reader:
cleaned_row = {
key: (str(value).strip() if value is not None else "")
for key, value in raw_row.items()
}
rows.append(cleaned_row)
else:
workbook = load_workbook(source_path, read_only=True, data_only=True)
worksheet = workbook.active
iterator = worksheet.iter_rows(values_only=True)
try:
header = next(iterator)
columns = [str(cell).strip() if cell is not None else "" for cell in
for values in iterator:
extended_values = list(values) + [""] * (len(columns) - len(values))
cleaned_row = {
column: (str(value).strip() if value is not None else "")
for column, value in zip(columns, extended_values[: len(columns)])
}
rows.append(cleaned_row)
except StopIteration:
pass
finally:
workbook.close()
return rows, columns
def process_data_rows(self, rows, columns):
"""处理数据行"""
total_rows = len(rows)
self.progress_bar["maximum"] = total_rows
self.progress_bar["value"] = 0
success_count = 0
fail_count = 0
prefix = self.file_name_prefix.get() or ""
delimiter = self.field_delimiter.get() or "" # 防止None
for index, row in enumerate(rows, start=1):
try:
# 生成文件名
parts = [self.safe_string_format(row.get(column, "")) for column in self
parts = [part for part in parts if part] # 移除空字符串
if parts:
name_part = delimiter.join(parts)
else:
name_part = f"record_{index}"
filename = f"{prefix}{name_part}.xlsx"
output_path = os.path.join(self.output_directory_path.get(), filename)
# 复制模板
workbook = load_workbook(self.template_file_path.get())
for source_column, worksheet_name, cell_address in self.field_mappings:
# 获取目标工作表
try:
worksheet = workbook[worksheet_name]
except KeyError:
# 如果工作表不存在,跳过本次映射
self.log_message(f"⚠️ 工作表 '{worksheet_name}' 不存在,跳过映射 {source_column} -> {worksheet_name}.{cell_address}")
continue
value = row.get(source_column, "")
# 尝试类型转换
try:
value = int(value)
except ValueError:
try:
value = float(value)
except ValueError:
pass # 保持原字符串
# 保留原始单元格格式
try:
original_cell = worksheet[cell_address]
except Exception:
# 如果单元格地址无效,跳过本次映射
self.log_message(f"⚠️ 单元格地址 '{cell_address}' 无效,跳过映射 {source_column} -> {worksheet_name}.{cell_address}")
continue
# 设置新值
worksheet[cell_address] = value
# 复制原始单元格格式
self.copy_cell_style(original_cell, worksheet[cell_address])
workbook.save(output_path)
workbook.close()
success_count += 1
self.log_message(f"✅ 已生成:{filename}")
except Exception as exception:
fail_count += 1
self.log_message(f"❌ 第 {index} 笔失败:{exception}")
self.progress_bar["value"] = index
self.root.update_idletasks()
self.log_message("\n=== 执行结束 ===")
self.log_message(f"成功:{success_count} 失败:{fail_count}")
messagebox.showinfo("完成", f"全部完成!\n成功:{success_count}\n失败:{fail_count}")
@staticmethod
def safe_string_format(value: str) -> str:
"""安全字符串格式化,保留字母数字、空格、短横线和下划线,去除其他非法字符,并去除首尾空白"""
return "".join(char for char in value if char.isalnum() or char in (" ", "-", "_")).strip()
def main():
"""程序入口"""
root = ttk.Window(themename="flatly", title="批量Excel复制填充工具3.4 by xueluo224")
app = ExcelBatchFillerApp(root)
root.mainloop()
if __name__ == "__main__":
main()```