吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 7234|回复: 102
收起左侧

[原创工具] 【python开发】Excel批量复制填充模版单元格工具3.4:一键告别重复劳动!!

    [复制链接]
xueluo224 发表于 2025-12-11 01:00
本帖最后由 xueluo224 于 2026-1-2 13:49 编辑

🚀 批量Excel复制填充工具:一键告别重复劳动!!

大家好!今天我要分享一个我自己开发的实用小工具——批量Excel复制填充工具。如果你经常需要根据一份数据源(如CSV或Excel)为每个人/每条记录生成一份独立的Excel文件(比如成绩单、合同、通知单等),那么这个工具绝对能帮你省下大量时间!

💡 为什么开发这个工具?

在日常办公中,我们常常遇到这样的场景:

  • 需要为100个学生生成个性化的成绩单
  • 为500个客户制作专属的报价单
  • 为每个部门生成定制化的报表

传统做法是手动复制模板,然后逐个填写数据,不仅效率低下还容易出错。于是我开发了这个自动化工具,只需简单配置,就能一键批量生成所有文件!

🛠️ 工具功能特点

支持多种数据源:CSV和Excel文件都可以作为数据源
可视化操作界面:无需编程基础,图形界面操作简单直观
灵活字段映射:可以将数据源中的任意列映射到模板的任意单元格
智能文件命名:支持自定义文件名前缀,并可从数据源中选择列作为文件名后缀
实时进度反馈:处理过程中显示进度条和详细日志,随时了解处理状态
错误处理机制:即使部分文件生成失败,也不会影响其他文件的处理  

📥 使用方法

1. 准备工作

  • 数据源文件:包含所有需要填充的数据(CSV或Excel格式)
  • 模板文件:设计好的Excel模板,留出需要填充数据的单元格位置
  • 输出文件夹:指定生成文件的保存位置

2. 操作步骤

  1. 选择文件:分别选择数据源文件、模板文件和输出文件夹
  2. 配置文件名
    • 设置文件名前缀(可选)
    • 选择用于生成文件名后缀的数据列
  3. 设置字段映射
    • 选择数据源中的列
    • 输入模板中对应的单元格地址(如A1、B2等)
    • 点击"添加映射"按钮
  4. 开始处理:点击"开始处理"按钮,等待批量生成完成!
    2.0版
    488282_GIF_2025-12-12_23-24-56_compressed.gif 3.4版

    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.21.生成的文件名可以是列标题组合;2.增加导入导出配置3.左右布局,将日志区放到右边

感谢46楼朋友 @alex17855楼朋友@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()```

免费评分

参与人数 22吾爱币 +27 热心值 +18 收起 理由
观林之鸟 + 1 + 1 我很赞同!
段亚栋 + 1 + 1 用心讨论,共获提升!
52p0 + 1 用心讨论,共获提升!
nizeze + 1 + 1 谢谢@Thanks!
thisisball + 1 + 1 谢谢@Thanks!
wangyagang11 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Richeng + 1 + 1 谢谢@Thanks!
小破吾 + 1 用心讨论,共获提升!
wuloveyou + 1 我很赞同!
ycat + 2 我很赞同!
alex178 + 1 + 1 我很赞同!
caihuachaorou87 + 1 + 1 谢谢@Thanks!
旗飞扬 + 1 + 1 热心回复!
243788502 + 1 谢谢@Thanks!
tisen + 1 + 1 非常棒,能加个批量映射功能吗,有时候映射太多
jaffa + 1 谢谢@Thanks!
余涂涂 + 1 我很赞同!
tz_being + 3 + 1 美中不足的是金额设置为文本导致合计不了
zhengzhenhui945 + 1 我很赞同!
fengtian99 + 1 + 1 测试可以,速度很快,感谢发布优秀作品
qing124A + 1 + 1 谢谢@Thanks!
风之暇想 + 7 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

ynthzkx 发表于 2025-12-16 19:25
一个一个匹配单元格太麻烦了,最好让表头字符串与单元格字符串自动匹配对应,模板字符加大括号匹配。
 楼主| xueluo224 发表于 2025-12-13 22:13
tz_being 发表于 2025-12-12 23:28
美中不足的是金额设置为文本导致合计不了,有空帮看一下。
https://wwbem.lanzouu.com/i5WlP3dhawof

https://wwbpf.lanzout.com/it5o63djte8h  密码:52pj

发现的问题很有价值
解决:增加了一个复制模板单元格格式的函数,我试了一下没问题了


名称: excel_batch_filler_2.0.py
大小: 16969 字节 : 16 KiB
SHA256: 9f06ee1b9c55a83978822897806c3c7e9ab60fa5d64ec0dbede429a325cf3ff8

名称: excel_batch_filler_2.0-win32.exe
大小: 9620781 字节 : 9395 KiB
SHA256: 4aca90e4fb0423011ace21fd7b0f847be51f638c66a50efd3b45dd5ab743278a

名称: excel_batch_filler_2.0-win64.exe
大小: 20162333 字节 : 19 MiB
SHA256: d69707943a7862346778a127eabc4a20b8214f6ee7a8459e6511fd474cfb338b

免费评分

参与人数 1吾爱币 +3 热心值 +1 收起 理由
tz_being + 3 + 1 谢谢!

查看全部评分

tz_being 发表于 2025-12-12 23:28
bigcarp 发表于 2026-2-5 11:25
1e3e 发表于 2026-2-5 11:23
大佬给个源码行不行?我这用的是linux系统,windows的程序运行不起来

[Python] 纯文本查看 复制代码
import pandas as pd
import openpyxl
from openpyxl.utils import get_column_letter
from copy import copy
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
import os
import sys
import time
import threading
import subprocess


def resource_path(relative_path):
    """ 获取资源绝对路径,兼容开发环境和 PyInstaller 打包环境 """
    try:
        # PyInstaller 创建临时文件夹并存储路径在 _MEIPASS
        base_path = sys._MEIPASS
    except Exception:
        base_path = os.path.abspath(".")
    return os.path.join(base_path, relative_path)


class GeneralExcelGenerator:
    def __init__(self, root):
        self.root = root
        self.root.title("通用Excel模板批量填充工具")
        self.root.geometry("1000x800")  # 调高窗口以容纳图片

        self.template_path = tk.StringVar()
        self.data_path = tk.StringVar()
        self.output_path = tk.StringVar()

        # 缓存原图用于弹出预览
        self.full_image = None

        self.setup_ui()

    def setup_ui(self):
        # 创建 Notebook (标签页控件)
        self.notebook = ttk.Notebook(self.root)
        self.notebook.pack(fill="both", expand=True, padx=10, pady=10)

        # 创建两个 Tab
        self.tab_operate = ttk.Frame(self.notebook)
        self.tab_manual = ttk.Frame(self.notebook)

        self.notebook.add(self.tab_operate, text="  操作界面  ")
        self.notebook.add(self.tab_manual, text="  使用说明  ")

        self.create_operate_widgets()
        self.create_manual_widgets()

    def create_operate_widgets(self):
        """第一个Tab:核心操作区域"""
        style = ttk.Style()
        style.configure("Accent.TButton", font=("微软雅黑", 11, "bold"))

        # 1. 模板配置
        frame1 = ttk.LabelFrame(self.tab_operate, text="Step 1: 模板配置", padding="10")
        frame1.pack(fill="x", padx=20, pady=10)
        ttk.Entry(frame1, textvariable=self.template_path, width=70).grid(row=0, column=0, padx=5)
        ttk.Button(frame1, text="选择模板", command=self.select_template).grid(row=0, column=1, padx=5)

        # 2. 数据源配置
        frame2 = ttk.LabelFrame(self.tab_operate, text="Step 2: 数据源配置", padding="10")
        frame2.pack(fill="x", padx=20, pady=10)
        ttk.Entry(frame2, textvariable=self.data_path, width=70).grid(row=0, column=0, padx=5)
        ttk.Button(frame2, text="选择数据", command=self.select_data).grid(row=0, column=1, padx=5)

        # 3. 输出路径
        frame3 = ttk.LabelFrame(self.tab_operate, text="Step 3: 保存路径", padding="10")
        frame3.pack(fill="x", padx=20, pady=10)
        ttk.Entry(frame3, textvariable=self.output_path, width=70).grid(row=0, column=0, padx=5)

        # 4. 执行按钮
        btn_frame = ttk.Frame(self.tab_operate)
        btn_frame.pack(pady=15)
        self.generate_btn = ttk.Button(
            btn_frame, text="开始批量填充生成",
            command=self.start_generate_thread,
            style="Accent.TButton", width=25
        )
        self.generate_btn.pack()

        # 5. 进度反馈
        self.progress_bar = ttk.Progressbar(self.tab_operate, orient="horizontal", length=800, mode="determinate")
        self.progress_bar.pack(pady=10)

        self.progress_var = tk.StringVar(value="准备就绪")
        ttk.Label(self.tab_operate, textvariable=self.progress_var, font=("微软雅黑", 10), foreground="#2E8B57").pack()

    def create_manual_widgets(self):
        """第二个Tab:带说明文字和图片预览"""
        paned = tk.PanedWindow(self.tab_manual, orient=tk.VERTICAL)
        paned.pack(fill="both", expand=True)

        # 上半部分:文字说明区域
        text_frame = ttk.Frame(paned)
        paned.add(text_frame, height=300)

        scrollbar = ttk.Scrollbar(text_frame)
        scrollbar.pack(side=tk.RIGHT, fill="y")

        manual_text = (
            "【通用Excel模板批量填充工具 - 使用手册】\n\n"
            "1. 工作原理:\n"
            "   本工具通过读取【数据文件】(Excel)中的每一行信息,将其填充到【模板文件】(Excel)\n"
            "   并为每一行数据生成一个独立的 Sheet(工作表)。\n\n"
            "2. 模板准备:\n"
            "   - 在 Excel 模板的单元格中输入 {{字段名}}(注意是双大括号)。\n"
            "   - 这里的“字段名”必须与数据文件中的“列标题”完全一致。\n\n"
            "3. 数据源准备:\n"
            "   - 数据文件第一行应为表头(列名)。\n"
            "   - 【第一列】的值将被用作生成后 Sheet 的名称。\n\n"
            "4. 注意事项:\n"
            "   - 生成过程中请勿打开正在使用的模板文件或目标输出文件。\n"
            "   - (提示:点击下方图片可查看原图解)\n"
        )

        self.text_area = tk.Text(
            text_frame,
            wrap=tk.WORD,
            font=("微软雅黑", 10),
            yscrollcommand=scrollbar.set,
            padx=20,
            pady=10,
            bg="#F9F9F9"
        )
        self.text_area.insert(tk.END, manual_text)
        self.text_area.config(state=tk.DISABLED)
        self.text_area.pack(fill="both", expand=True)
        scrollbar.config(command=self.text_area.yview)

        # 下半部分:图片说明区域
        img_frame = ttk.LabelFrame(paned, text="操作图解 (点击图片查看原图)")
        paned.add(img_frame)

        self.img_label = ttk.Label(img_frame, text="正在加载说明图片...", cursor="hand2")
        self.img_label.pack(fill="both", expand=True, padx=10, pady=10)

        # 绑定点击事件
        self.img_label.bind("<Button-1>", lambda e: self.show_full_image())

        self.load_manual_image()

    def load_manual_image(self):
        """加载 help2.png"""
        try:
            img_path = resource_path("help2.png")
            if os.path.exists(img_path):
                self.full_image = Image.open(img_path)
                display_img = self.full_image.copy()
                display_img.thumbnail((900, 400))
                self.photo = ImageTk.PhotoImage(display_img)
                self.img_label.config(image=self.photo, text="")
            else:
                self.img_label.config(text=f"未找到说明图片: help2.png\n请确保打包或运行目录下存在该图片。")
        except Exception as e:
            self.img_label.config(text=f"图片加载失败: {str(e)}")

    def show_full_image(self):
        """弹出大图预览窗并最大化"""
        if self.full_image is None:
            return

        top = tk.Toplevel(self.root)
        top.title("操作图解预览")

        try:
            top.state('zoomed')  # 最大化
        except:
            top.attributes("-fullscreen", True)

        img_w, img_h = self.full_image.size

        canvas = tk.Canvas(top, highlightthickness=0)
        hbar = ttk.Scrollbar(top, orient=tk.HORIZONTAL, command=canvas.xview)
        vbar = ttk.Scrollbar(top, orient=tk.VERTICAL, command=canvas.yview)

        canvas.config(xscrollcommand=hbar.set, yscrollcommand=vbar.set)

        hbar.pack(side=tk.BOTTOM, fill=tk.X)
        vbar.pack(side=tk.RIGHT, fill=tk.Y)
        canvas.pack(side=tk.LEFT, expand=True, fill=tk.BOTH)

        self.full_photo = ImageTk.PhotoImage(self.full_image)
        canvas.create_image(0, 0, anchor=tk.NW, image=self.full_photo)
        canvas.config(scrollregion=(0, 0, img_w, img_h))

    # --- 原有逻辑保持不变 ---

    def select_template(self):
        path = filedialog.askopenfilename(filetypes=[("Excel", "*.xlsx")])
        if path: self.template_path.set(path)

    def select_data(self):
        path = filedialog.askopenfilename(filetypes=[("Excel", "*.xlsx")])
        if path:
            self.data_path.set(path)
            base = os.path.splitext(path)[0]
            self.output_path.set(f"{base}_结果_{time.strftime('%H%M%S')}.xlsx")

    def _copy_worksheet(self, source_ws, target_ws):
        for row in range(1, source_ws.max_row + 1):
            for col in range(1, source_ws.max_column + 1):
                source_cell = source_ws.cell(row=row, column=col)
                target_cell = target_ws.cell(row=row, column=col, value=source_cell.value)
                if source_cell.has_style:
                    target_cell.font = copy(source_cell.font)
                    target_cell.border = copy(source_cell.border)
                    target_cell.fill = copy(source_cell.fill)
                    target_cell.number_format = source_cell.number_format
                    target_cell.alignment = copy(source_cell.alignment)

        for col in range(1, source_ws.max_column + 1):
            col_letter = get_column_letter(col)
            target_ws.column_dimensions[col_letter].width = source_ws.column_dimensions[col_letter].width
        for row in range(1, source_ws.max_row + 1):
            if source_ws.row_dimensions[row].height:
                target_ws.row_dimensions[row].height = source_ws.row_dimensions[row].height
        for merged_range in source_ws.merged_cells.ranges:
            target_ws.merge_cells(str(merged_range))

    def start_generate_thread(self):
        if not all([self.template_path.get(), self.data_path.get()]):
            messagebox.showwarning("提示", "请确保已选择模板文件和数据文件!")
            return
        self.generate_btn.config(state=tk.DISABLED)
        threading.Thread(target=self.generate_excel_logic, daemon=True).start()

    def generate_excel_logic(self):
        try:
            self.progress_var.set("正在加载数据...")
            df = pd.read_excel(self.data_path.get())
            total_rows = len(df)

            template_wb = openpyxl.load_workbook(self.template_path.get())
            template_ws = template_wb.active
            new_wb = openpyxl.Workbook()
            new_wb.remove(new_wb.active)

            for index, row in df.iterrows():
                raw_sheet_val = str(row.iloc[0])
                safe_name = "".join([c for c in raw_sheet_val if c not in r'\*?:/[]'])[:30]

                self.progress_var.set(f"正在处理 ({index + 1}/{total_rows}): {safe_name}")
                self.root.after(0, lambda v=((index + 1) / total_rows * 100): self.update_progress(v))

                new_ws = new_wb.create_sheet(title=safe_name)
                self._copy_worksheet(template_ws, new_ws)

                for r_idx in range(1, new_ws.max_row + 1):
                    for c_idx in range(1, new_ws.max_column + 1):
                        cell = new_ws.cell(row=r_idx, column=c_idx)
                        if cell.value and isinstance(cell.value, str) and "{{" in cell.value:
                            field_name = cell.value.replace("{{", "").replace("}}", "").strip()
                            found = False
                            for col_name in df.columns:
                                clean_col_name = col_name.split('.')[0] if '.' in col_name else col_name
                                if clean_col_name == field_name:
                                    cell.value = row[col_name] if pd.notna(row[col_name]) else ""
                                    found = True
                                    break

            self.progress_var.set("保存中...")
            out_file = self.output_path.get()
            new_wb.save(out_file)

            self.root.after(0, lambda: self.ask_to_open_file(out_file))

        except Exception as e:
            self.root.after(0, lambda err=str(e): messagebox.showerror("错误", err))
        finally:
            self.root.after(0, self.reset_ui)

    def ask_to_open_file(self, file_path):
        ans = messagebox.askyesno("生成成功", f"&#9989; 文件已生成!\n路径:{file_path}\n\n是否立即打开该文件?")
        if ans:
            try:
                if os.name == 'nt':
                    os.startfile(file_path)
                elif os.name == 'posix':
                    subprocess.call(['open' if sys.platform == 'darwin' else 'xdg-open', file_path])
            except Exception as e:
                messagebox.showwarning("提醒", f"无法自动打开文件。\n{e}")

    def update_progress(self, value):
        self.progress_bar['value'] = value

    def reset_ui(self):
        self.generate_btn.config(state=tk.NORMAL)
        self.progress_var.set("准备就绪")
        self.progress_bar['value'] = 0


if __name__ == "__main__":
    try:
        from ctypes import windll

        windll.shcore.SetProcessDpiAwareness(1)
    except:
        pass
    root = tk.Tk()
    app = GeneralExcelGenerator(root)
    root.mainloop()

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
1e3e + 1 + 1 谢谢@Thanks!

查看全部评分

xcaoyi520 发表于 2025-12-11 21:33
感谢楼主分享
lizh1984 发表于 2025-12-12 09:12
谢谢楼主分析,这个和Word中的邮件合并目录功能比较类似。
flylujun 发表于 2025-12-12 10:08
比邮件合并方便些
jmz14865220 发表于 2025-12-12 10:14
感觉很实用但是能不能出个演示视频
acd12345 发表于 2025-12-12 10:26
谢谢楼主,现在自动化办公软件越来越多了
fengtian99 发表于 2025-12-12 10:29
这个是以什么规则填充的,还是就像邮件合并那种
eleven7607 发表于 2025-12-12 10:32
先收藏了,感谢楼主分享,十分实用的工具。
lizhipei78 发表于 2025-12-12 11:34
Excel版邮件合并,这个对于不懂得代码的同志,要批量生成模板文件还是很实用的
lizhipei78 发表于 2025-12-12 11:39
刚测试了一下,速度还算可以,不知道在没有安装Python的机子是否能顺利运行
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-28 13:09

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表