Excel拆分和合并优化版本

代码总览

python 复制代码
import pandas as pd
import os
import tkinter as tk
from tkinter import filedialog, messagebox
from tkinter import ttk
import math
from collections import defaultdict
import logging
from openpyxl import load_workbook, Workbook
from openpyxl.utils import get_column_letter
import copy
import time

# --- 1. 配置日志 ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# --- 2. UI 文本常量 (提高可读性) ---
UI_TEXTS = {
    'APP_TITLE': "Excel工具:拆分与合并 (V4 可读性重构)",
    'TAB_SPLIT': "文件拆分",
    'TAB_COMBINE': "智能合并",
    'SELECT_FILE_LABEL': "选择Excel文件:",
    'BROWSE_BUTTON': "浏览",
    'ROWS_PER_FILE_LABEL': "每份文件行数:",
    'ROWS_UNIT': "行",
    'START_SPLIT_BUTTON': "开始拆分",
    'START_COMBINE_BUTTON': "开始智能合并",
    
    'PRESERVE_FORMAT_LABEL': "保留视觉格式 (字体/颜色/列宽,速度很慢)",
    'SPEED_FIRST_INFO': "取消勾选 = 速度最快,仅保留文本数据 (推荐, 可保留 '00123' 和身份证号)",
    
    'COMBINE_BASE_LABEL': "选择基准Excel文件:",
    'COMBINE_INFO': "系统会自动查找与选定文件相似的其他文件进行合并\n(基于文件名模式识别,如:文件1.xlsx, 文件2.xlsx)",
    
    'FILE_INFO_DEFAULT': "请选择Excel文件",
    'FILE_INFO_SELECTED': "已选择: {filename}",
    'FILE_INFO_ROWS': "文件总行数: {rows} 行 (不含表头)",
    
    'WARN_TITLE': "警告",
    'WARN_NO_FILE': "请先选择有效的Excel文件!",
    'WARN_INVALID_ROWS': "请输入有效的行数(大于0)!",
    'WARN_NAN': "请输入有效的数字!",
    'WARN_EMPTY_FILE': "文件为空,没有数据可拆分。",

    'ERROR_TITLE': "错误",
    'ERROR_CANT_READ': "无法读取文件信息",
    'ERROR_NO_FILES_FOUND': "目录中没有找到 Excel 文件!",
    'ERROR_NO_SIMILAR_FILES': "没有找到与 '{filename}' 相似的其他文件",
    'ERROR_NO_DATA_READ': "无法读取任何文件的数据。",
    'ERROR_DURING_SPLIT': "拆分过程中出现错误:{error}",
    'ERROR_DURING_COMBINE': "合并过程中出现错误:{error}",
    
    'SUCCESS_TITLE': "成功",
    'SUCCESS_SPLIT_FORMAT': (
        "文件拆分完成!(保留格式)\n"
        "共拆分成 {num_files} 个文件\n"
        "总耗时: {time:.2f}秒"
    ),
    'SUCCESS_SPLIT_FAST': (
        "文件拆分完成!(速度优先)\n"
        "共拆分成 {num_files} 个文件\n"
        "总耗时: {time:.2f}秒\n"
        "(注意:仅保留文本数据,视觉格式已丢失)"
    ),
    'SUCCESS_COMBINE_FORMAT': (
        "数据合并完成!(保留格式)\n"
        "合并了 {num_files} 个文件\n"
        "保存位置:{path}\n"
        "总行数:{rows} (不含表头)\n"
        "总耗时: {time:.2f}秒"
    ),
    'SUCCESS_COMBINE_FAST': (
        "数据合并完成!(速度优先)\n"
        "合并了 {num_files} 个文件\n"
        "保存位置:{path}\n"
        "总行数:{rows}\n"
        "总耗时: {time:.2f}秒\n"
        "(注意:仅保留文本数据,视觉格式已丢失)"
    ),
}

# --- 3. 逻辑类:文件名处理 ---
class ExcelFileUtils:
    """只负责文件名匹配和模式识别"""
    
    @staticmethod
    def extract_common_pattern(filename):
        name_without_ext = os.path.splitext(filename)[0]
        patterns = []
        if any(char.isdigit() for char in name_without_ext):
            non_digit_parts = []
            current_part = ""
            for char in name_without_ext:
                if char.isdigit():
                    if current_part:
                        non_digit_parts.append(current_part)
                        current_part = ""
                else:
                    current_part += char
            if current_part:
                non_digit_parts.append(current_part)
            if non_digit_parts:
                patterns.append(''.join(non_digit_parts).strip(' _-'))
        
        for sep in ['_', '-', ' ']:
            if sep in name_without_ext:
                parts = name_without_ext.split(sep)
                if len(parts) > 1:
                    patterns.append(sep.join(parts[:-1]))
        
        if not patterns:
            patterns.append(name_without_ext)
        return patterns

    @staticmethod
    def find_similar_files(all_files, patterns, base_file):
        similar_files = [base_file]
        for pattern in patterns:
            if pattern:
                for filename in all_files:
                    if filename == base_file:
                        continue
                    name_without_ext = os.path.splitext(filename)[0]
                    if (pattern in filename or 
                        pattern in name_without_ext or
                        filename.startswith(pattern) or
                        name_without_ext.startswith(pattern)):
                        similar_files.append(filename)
        return list(dict.fromkeys(similar_files))

    @staticmethod
    def generate_output_name(patterns, base_file):
        if patterns:
            longest_pattern = max(patterns, key=len)
            if len(longest_pattern) > 3:
                return longest_pattern
        return os.path.splitext(base_file)[0]

# --- 4. 逻辑类:Pandas 快速处理 ---
class PandasProcessor:
    """只负责速度优先的 Pandas 逻辑 (dtype=str)"""

    @staticmethod
    def split_excel_pandas(file_path, rows_per_file):
        try:
            start_time = time.time()
            logging.info(f"[Pandas-STR] 开始拆分文件: {file_path}")
            
            df = pd.read_excel(file_path, dtype=str, engine='openpyxl') 
            total_data_rows = len(df)
            
            if total_data_rows == 0:
                messagebox.showerror(UI_TEXTS['ERROR_TITLE'], UI_TEXTS['WARN_EMPTY_FILE'])
                return
            
            num_files = math.ceil(total_data_rows / rows_per_file)
            file_dir = os.path.dirname(file_path)
            file_name = os.path.splitext(os.path.basename(file_path))[0]
            
            for i in range(num_files):
                start_row = i * rows_per_file
                end_row = min((i + 1) * rows_per_file, total_data_rows)
                chunk_df = df.iloc[start_row:end_row]
                
                output_filename = f"{file_name}_拆分_{i+1}.xlsx"
                output_path = os.path.join(file_dir, output_filename)
                
                chunk_df.to_excel(output_path, index=False, engine='openpyxl')
                logging.info(f"[Pandas-STR] 保存拆分文件: {output_path}")

            total_time = time.time() - start_time
            messagebox.showinfo(UI_TEXTS['SUCCESS_TITLE'], 
                                UI_TEXTS['SUCCESS_SPLIT_FAST'].format(
                                    num_files=num_files, 
                                    time=total_time
                                ))

        except Exception as e:
            messagebox.showerror(UI_TEXTS['ERROR_TITLE'], 
                                 UI_TEXTS['ERROR_DURING_SPLIT'].format(error=e))
            logging.error(f"[Pandas-STR] 拆分过程中出现错误:{e}")

    @staticmethod
    def combine_excel_pandas(selected_file):
        try:
            start_time = time.time()
            logging.info(f"[Pandas-STR] 开始合并文件,以 {selected_file} 为基准")
            file_dir = os.path.dirname(selected_file)
            file_name = os.path.basename(selected_file)
            
            listdir = os.listdir(file_dir)
            excel_files = [f for f in listdir if f.endswith(('.xlsx', '.xls'))]
            
            if not excel_files:
                messagebox.showerror(UI_TEXTS['ERROR_TITLE'], UI_TEXTS['ERROR_NO_FILES_FOUND'])
                return

            base_name = file_name
            common_patterns = ExcelFileUtils.extract_common_pattern(base_name)
            similar_files = ExcelFileUtils.find_similar_files(excel_files, common_patterns, base_name)
            
            if len(similar_files) <= 1:
                messagebox.showinfo(UI_TEXTS['SUCCESS_TITLE'], 
                                    UI_TEXTS['ERROR_NO_SIMILAR_FILES'].format(filename=file_name))
                return

            file_list = "\n".join(similar_files)
            confirm = messagebox.askyesno(
                f"{UI_TEXTS['SUCCESS_TITLE']} ({UI_TEXTS['TAB_SPLIT']})", 
                f"将合并以下文件 (仅保留文本数据):\n\n{file_list}\n\n共 {len(similar_files)} 个文件"
            )
            
            if not confirm:
                logging.info("[Pandas-STR] 用户取消合并")
                return
            
            all_dfs = []
            for filename in similar_files:
                file_path = os.path.join(file_dir, filename)
                try:
                    df = pd.read_excel(file_path, dtype=str, engine='openpyxl')
                    all_dfs.append(df)
                except Exception as e:
                    logging.warning(f"[Pandas-STR] 无法读取文件 {filename}: {e}")
            
            if not all_dfs:
                messagebox.showerror(UI_TEXTS['ERROR_TITLE'], UI_TEXTS['ERROR_NO_DATA_READ'])
                return

            merged_df = pd.concat(all_dfs, ignore_index=True)
            total_rows = len(merged_df)

            output_name = ExcelFileUtils.generate_output_name(common_patterns, base_name)
            output_path = os.path.join(file_dir, f"{output_name}_合并结果_Pandas.xlsx")
            
            counter = 1
            while os.path.exists(output_path):
                output_path = os.path.join(file_dir, f"{output_name}_合并结果_Pandas({counter}).xlsx")
                counter += 1
            
            merged_df.to_excel(output_path, index=False, engine='openpyxl')
            total_time = time.time() - start_time
            logging.info(f"[Pandas-STR] 保存合并结果: {output_path} (总耗时: {total_time:.2f}s)")
            
            messagebox.showinfo(
                UI_TEXTS['SUCCESS_TITLE'], 
                UI_TEXTS['SUCCESS_COMBINE_FAST'].format(
                    num_files=len(similar_files),
                    path=output_path,
                    rows=total_rows,
                    time=total_time
                ))
            
        except Exception as e:
            messagebox.showerror(UI_TEXTS['ERROR_TITLE'], 
                                 UI_TEXTS['ERROR_DURING_COMBINE'].format(error=e))
            logging.error(f"[Pandas-STR] 合并过程中出现错误:{e}")

# --- 5. 逻辑类:OpenPyXL 格式处理 ---
class OpenPyXLProcessor:
    """只负责保留格式的 OpenPyXL 逻辑 (慢)"""
    
    @staticmethod
    def _cache_styles(ws):
        """辅助函数:缓存标题行和列宽"""
        header_format = []
        for row in ws.iter_rows(min_row=1, max_row=1):
            header_format.append([(cell.value, cell.number_format, cell.font, cell.fill, 
                                 cell.border, cell.alignment) for cell in row])
        
        column_widths = {}
        for col_idx in range(1, ws.max_column + 1):
            col_letter = get_column_letter(col_idx)
            column_dim = ws.column_dimensions[col_letter]
            if column_dim and hasattr(column_dim, 'width') and column_dim.width:
                column_widths[col_letter] = column_dim.width
        
        return header_format, column_widths

    @staticmethod
    def _apply_header_styles(ws, header_format):
        """辅助函数:应用缓存的标题行格式"""
        for col_idx, (value, num_format, font, fill, border, alignment) in enumerate(header_format[0], 1):
            cell = ws.cell(row=1, column=col_idx, value=value)
            cell.number_format = num_format
            if font: cell.font = copy.copy(font)
            if fill: cell.fill = copy.copy(fill)
            if border: cell.border = copy.copy(border)
            if alignment: cell.alignment = copy.copy(alignment)
        return ws.max_column

    @staticmethod
    def _apply_column_widths(ws, column_widths):
        """辅助函数:应用缓存的列宽"""
        for col_letter, width in column_widths.items():
            ws.column_dimensions[col_letter].width = width

    @staticmethod
    def split_excel_preserve_format(file_path, rows_per_file, total_data_rows):
        try:
            start_time = time.time()
            logging.info(f"[PreserveFormat] 开始拆分文件: {file_path}")
            
            wb = load_workbook(file_path)
            ws = wb.active
            
            if total_data_rows == 0:
                messagebox.showerror(UI_TEXTS['ERROR_TITLE'], UI_TEXTS['WARN_EMPTY_FILE'])
                return
            
            num_files = math.ceil(total_data_rows / rows_per_file)
            file_dir = os.path.dirname(file_path)
            file_name = os.path.splitext(os.path.basename(file_path))[0]
            
            header_format, column_widths = OpenPyXLProcessor._cache_styles(ws)
            max_col = ws.max_column
            
            for i in range(num_files):
                file_start_time = time.time()
                start_row = i * rows_per_file + 2  
                end_row = min((i + 1) * rows_per_file + 1, total_data_rows + 1)
                
                new_wb = Workbook()
                new_ws = new_wb.active
                
                OpenPyXLProcessor._apply_header_styles(new_ws, header_format)
                
                # 核心:逐个复制数据和格式
                data_row_idx = 2
                for row in range(start_row, end_row + 1):
                    for col in range(1, max_col + 1):
                        source_cell = ws.cell(row=row, column=col)
                        target_cell = new_ws.cell(row=data_row_idx, column=col, value=source_cell.value)
                        
                        target_cell.number_format = source_cell.number_format
                        if source_cell.font:
                            target_cell.font = copy.copy(source_cell.font)
                        if source_cell.fill:
                            target_cell.fill = copy.copy(source_cell.fill)
                        if source_cell.border:
                            target_cell.border = copy.copy(source_cell.border)
                        if source_cell.alignment:
                            target_cell.alignment = copy.copy(source_cell.alignment)
                    data_row_idx += 1
                
                OpenPyXLProcessor._apply_column_widths(new_ws, column_widths)

                output_filename = f"{file_name}_拆分_{i+1}.xlsx"
                output_path = os.path.join(file_dir, output_filename)
                
                new_wb.save(output_path)
                file_time = time.time() - file_start_time
                logging.info(f"[PreserveFormat] 保存拆分文件: {output_path} (耗时: {file_time:.2f}s)")
            
            total_time = time.time() - start_time
            messagebox.showinfo(UI_TEXTS['SUCCESS_TITLE'], 
                                UI_TEXTS['SUCCESS_SPLIT_FORMAT'].format(
                                    num_files=num_files,
                                    time=total_time
                                ))
            
        except Exception as e:
            messagebox.showerror(UI_TEXTS['ERROR_TITLE'], 
                                 UI_TEXTS['ERROR_DURING_SPLIT'].format(error=e))
            logging.error(f"[PreserveFormat] 拆分过程中出现错误:{e}")

    @staticmethod
    def combine_excel_preserve_format(selected_file):
        try:
            start_time = time.time()
            logging.info(f"[PreserveFormat] 开始合并文件,以 {selected_file} 为基准")
            file_dir = os.path.dirname(selected_file)
            file_name = os.path.basename(selected_file)
            
            listdir = os.listdir(file_dir)
            excel_files = [f for f in listdir if f.endswith(('.xlsx', '.xls'))]
            
            if not excel_files:
                messagebox.showerror(UI_TEXTS['ERROR_TITLE'], UI_TEXTS['ERROR_NO_FILES_FOUND'])
                return
            
            base_name = file_name
            common_patterns = ExcelFileUtils.extract_common_pattern(base_name)
            similar_files = ExcelFileUtils.find_similar_files(excel_files, common_patterns, base_name)
            
            if len(similar_files) <= 1:
                messagebox.showinfo(UI_TEXTS['SUCCESS_TITLE'], 
                                    UI_TEXTS['ERROR_NO_SIMILAR_FILES'].format(filename=file_name))
                return
            
            file_list = "\n".join(similar_files)
            confirm = messagebox.askyesno(
                f"{UI_TEXTS['SUCCESS_TITLE']} ({UI_TEXTS['TAB_COMBINE']})", 
                f"将合并以下文件 (此操作可能较慢):\n\n{file_list}\n\n共 {len(similar_files)} 个文件"
            )
            
            if not confirm:
                logging.info("[PreserveFormat] 用户取消合并")
                return
            
            template_file = os.path.join(file_dir, similar_files[0])
            template_wb = load_workbook(template_file)
            template_ws = template_wb.active
            
            merged_wb = Workbook()
            merged_ws = merged_wb.active
            
            header_format, column_widths = OpenPyXLProcessor._cache_styles(template_ws)
            max_col_count = OpenPyXLProcessor._apply_header_styles(merged_ws, header_format)
            
            current_row = 2
            total_rows = 0
            
            for filename in similar_files:
                file_path = os.path.join(file_dir, filename)
                wb = load_workbook(file_path)
                ws = wb.active
                
                for row_idx, row in enumerate(ws.iter_rows(min_row=2), 2):
                    for col_idx, cell in enumerate(row, 1):
                        if col_idx <= max_col_count:
                            target_cell = merged_ws.cell(row=current_row, column=col_idx, value=cell.value)
                            
                            target_cell.number_format = cell.number_format
                            if cell.font:
                                target_cell.font = copy.copy(cell.font)
                            if cell.fill:
                                target_cell.fill = copy.copy(cell.fill)
                            if cell.border:
                                target_cell.border = copy.copy(cell.border)
                            if cell.alignment:
                                target_cell.alignment = copy.copy(cell.alignment)
                    
                    current_row += 1
                    total_rows += 1
                
                logging.info(f"[PreserveFormat] 合并文件: {filename}, 添加了 {ws.max_row - 1} 行数据")
                wb.close()
            
            OpenPyXLProcessor._apply_column_widths(merged_ws, column_widths)
            
            output_name = ExcelFileUtils.generate_output_name(common_patterns, base_name)
            output_path = os.path.join(file_dir, f"{output_name}_合并结果.xlsx")
            
            counter = 1
            while os.path.exists(output_path):
                output_path = os.path.join(file_dir, f"{output_name}_合并结果({counter}).xlsx")
                counter += 1
            
            merged_wb.save(output_path)
            total_time = time.time() - start_time
            logging.info(f"[PreserveFormat] 保存合并结果: {output_path} (总耗时: {total_time:.2f}s)")
            
            messagebox.showinfo(
                UI_TEXTS['SUCCESS_TITLE'], 
                UI_TEXTS['SUCCESS_COMBINE_FORMAT'].format(
                    num_files=len(similar_files),
                    path=output_path,
                    rows=total_rows,
                    time=total_time
                ))
            
        except Exception as e:
            messagebox.showerror(UI_TEXTS['ERROR_TITLE'], 
                                 UI_TEXTS['ERROR_DURING_COMBINE'].format(error=e))
            logging.error(f"[PreserveFormat] 合并过程中出现错误:{e}")

# --- 6. UI 类:拆分标签页 ---
class SplitterTab(ttk.Frame):
    """文件拆分标签页的UI和逻辑 (已重构)"""
    
    def __init__(self, parent):
        super().__init__(parent, padding="15")
        self.total_data_rows = 0
        
        self.split_file_var = tk.StringVar()
        self.rows_var = tk.StringVar(value="10000")
        self.split_info_var = tk.StringVar(value=UI_TEXTS['FILE_INFO_DEFAULT'])
        self.preserve_format_var = tk.BooleanVar(value=True) # 默认保留格式
        
        self.create_widgets()

    def create_widgets(self):
        # --- 使用 UI_TEXTS 常量 ---
        ttk.Label(self, text=UI_TEXTS['SELECT_FILE_LABEL']).grid(row=0, column=0, sticky=tk.W, pady=5)
        ttk.Entry(self, textvariable=self.split_file_var, width=40).grid(row=0, column=1, padx=5, pady=5)
        ttk.Button(self, text=UI_TEXTS['BROWSE_BUTTON'], command=self.select_file).grid(row=0, column=2, padx=5, pady=5)

        ttk.Label(self, textvariable=self.split_info_var, foreground="blue").grid(row=1, column=1, sticky=tk.W, pady=2)

        ttk.Label(self, text=UI_TEXTS['ROWS_PER_FILE_LABEL']).grid(row=2, column=0, sticky=tk.W, pady=10)
        ttk.Entry(self, textvariable=self.rows_var, width=15).grid(row=2, column=1, sticky=tk.W, padx=5, pady=10)
        ttk.Label(self, text=UI_TEXTS['ROWS_UNIT']).grid(row=2, column=1, sticky=tk.W, padx=100, pady=10)

        ttk.Checkbutton(
            self, 
            text=UI_TEXTS['PRESERVE_FORMAT_LABEL'], 
            variable=self.preserve_format_var
        ).grid(row=3, column=1, sticky=tk.W, pady=(10, 0))

        ttk.Label(
            self, 
            text=UI_TEXTS['SPEED_FIRST_INFO'],
            foreground="gray", font=("Arial", 9)
        ).grid(row=4, column=1, sticky=tk.W, padx=5, pady=(0, 10))

        ttk.Button(self, text=UI_TEXTS['START_SPLIT_BUTTON'], command=self.start_split).grid(row=5, column=1, pady=10)

        self.columnconfigure(1, weight=1)

    def select_file(self):
        file_path = filedialog.askopenfilename(
            title=UI_TEXTS['SELECT_FILE_LABEL'],
            filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
        )
        if file_path:
            self.split_file_var.set(file_path)
            if self._read_row_count(file_path): # 自动读取行数
                self.split_info_var.set(UI_TEXTS['FILE_INFO_ROWS'].format(rows=self.total_data_rows))
            else:
                self.split_info_var.set(UI_TEXTS['ERROR_CANT_READ'])

    def _read_row_count(self, file_path):
        """辅助函数:读取行数"""
        try:
            df = pd.read_excel(file_path, engine='openpyxl')
            self.total_data_rows = len(df)
            logging.info(f"选中拆分文件: {file_path}, 总行数: {self.total_data_rows}")
            return True
        except Exception as e:
            self.total_data_rows = 0
            logging.error(f"无法读取文件信息: {e}")
            return False

    def _validate_input(self):
        """辅助函数:集中处理所有输入验证"""
        file_path = self.split_file_var.get()
        
        if not file_path or not os.path.exists(file_path):
            messagebox.showwarning(UI_TEXTS['WARN_TITLE'], UI_TEXTS['WARN_NO_FILE'])
            return None, None
        
        # 如果选择文件时读取失败,在此重试
        if self.total_data_rows == 0 and UI_TEXTS['ERROR_CANT_READ'] in self.split_info_var.get():
            if not self._read_row_count(file_path):
                messagebox.showerror(UI_TEXTS['ERROR_TITLE'], UI_TEXTS['ERROR_CANT_READ'])
                return None, None
        
        if self.total_data_rows == 0:
             messagebox.showwarning(UI_TEXTS['WARN_TITLE'], UI_TEXTS['WARN_EMPTY_FILE'])
             return None, None

        try:
            rows_per_file = int(self.rows_var.get())
            if rows_per_file <= 0:
                messagebox.showwarning(UI_TEXTS['WARN_TITLE'], UI_TEXTS['WARN_INVALID_ROWS'])
                return None, None
        except ValueError:
            messagebox.showwarning(UI_TEXTS['WARN_TITLE'], UI_TEXTS['WARN_NAN'])
            return None, None
            
        return file_path, rows_per_file

    def start_split(self):
        """开始拆分(逻辑更清晰)"""
        file_path, rows_per_file = self._validate_input()
        
        if not file_path:
            return # 验证失败
        
        if self.preserve_format_var.get():
            # 方案1:保留格式 (慢)
            OpenPyXLProcessor.split_excel_preserve_format(file_path, rows_per_file, self.total_data_rows)
        else:
            # 方案2:速度优先 (快)
            PandasProcessor.split_excel_pandas(file_path, rows_per_file)

# --- 7. UI 类:合并标签页 ---
class CombinerTab(ttk.Frame):
    """智能合并标签页的UI和逻辑 (已重构)"""
    
    def __init__(self, parent):
        super().__init__(parent, padding="15")
        
        self.combine_file_var = tk.StringVar()
        self.combine_info_var = tk.StringVar(value=UI_TEXTS['FILE_INFO_DEFAULT'])
        self.preserve_format_var = tk.BooleanVar(value=True)
        
        self.create_widgets()

    def create_widgets(self):
        # --- 使用 UI_TEXTS 常量 ---
        ttk.Label(self, text=UI_TEXTS['COMBINE_BASE_LABEL']).grid(row=0, column=0, sticky=tk.W, pady=5)
        ttk.Entry(self, textvariable=self.combine_file_var, width=50).grid(row=0, column=1, padx=5, pady=5)
        ttk.Button(self, text=UI_TEXTS['BROWSE_BUTTON'], command=self.select_file).grid(row=0, column=2, padx=5, pady=5)

        ttk.Label(self, textvariable=self.combine_info_var, foreground="blue").grid(row=1, column=1, sticky=tk.W, pady=5)

        ttk.Label(self, text=UI_TEXTS['COMBINE_INFO'], foreground="gray", font=("Arial", 9)).grid(row=2, column=1, sticky=tk.W, pady=5)

        ttk.Checkbutton(
            self, 
            text=UI_TEXTS['PRESERVE_FORMAT_LABEL'], 
            variable=self.preserve_format_var
        ).grid(row=3, column=1, sticky=tk.W, pady=(10, 0))

        ttk.Label(
            self, 
            text=UI_TEXTS['SPEED_FIRST_INFO'],
            foreground="gray", font=("Arial", 9)
        ).grid(row=4, column=1, sticky=tk.W, padx=5, pady=(0, 10))
        
        ttk.Button(self, text=UI_TEXTS['START_COMBINE_BUTTON'], command=self.start_combine).grid(row=5, column=1, pady=15)

        self.columnconfigure(1, weight=1)

    def select_file(self):
        file_path = filedialog.askopenfilename(
            title=UI_TEXTS['COMBINE_BASE_LABEL'],
            filetypes=[("Excel files", "*.xlsx *.xls"), ("All files", "*.*")]
        )
        if file_path:
            self.combine_file_var.set(file_path)
            file_name = os.path.basename(file_path)
            self.combine_info_var.set(UI_TEXTS['FILE_INFO_SELECTED'].format(filename=file_name))
            logging.info(f"选中合并基准文件: {file_path}")

    def start_combine(self):
        """开始合并"""
        file_path = self.combine_file_var.get()
        
        if not file_path or not os.path.exists(file_path):
            messagebox.showwarning(UI_TEXTS['WARN_TITLE'], UI_TEXTS['WARN_NO_FILE'])
            return
        
        if self.preserve_format_var.get():
            OpenPyXLProcessor.combine_excel_preserve_format(file_path)
        else:
            PandasProcessor.combine_excel_pandas(file_path)

# --- 8. 主应用 ---
class ExcelToolApp:
    """主应用程序类"""
    def __init__(self, root):
        self.root = root
        self.root.title(UI_TEXTS['APP_TITLE'])
        self.root.geometry("600x380")
        
        self.create_widgets()

    def create_widgets(self):
        notebook = ttk.Notebook(self.root)
        notebook.pack(fill='both', expand=True, padx=10, pady=10)

        split_frame = SplitterTab(notebook)
        notebook.add(split_frame, text=UI_TEXTS['TAB_SPLIT'])

        combine_frame = CombinerTab(notebook)
        notebook.add(combine_frame, text=UI_TEXTS['TAB_COMBINE'])

    def run(self):
        self.root.mainloop()

# --- 9. 启动入口 ---
if __name__ == "__main__":
    root = tk.Tk()
    app = ExcelToolApp(root)
    app.run()

打包步骤

创建一个虚拟环境

bash 复制代码
python -m venv venv_pack

激活虚拟环境

bash 复制代码
.\venv_pack\Scripts\activate

安装最小依赖: 在这个干净的环境中,只安装您的程序真正需要的库。

bash 复制代码
pip install pyinstaller pandas openpyxl

运行 PyInstaller 打包命令

选项 A:打包成一个文件夹(推荐,启动快)

这会创建一个 dist\Excel工具 文件夹,里面包含您的 Excel工具.exe 和所有依赖。启动速度比"单文件"快得多。

bash 复制代码
pyinstaller --noconsole --name="Excel工具" --upx-dir="." "excel_tool.py"

选项 B:打包成一个单独的 EXE 文件(整洁,但启动慢)

这会创建一个 dist\Excel工具.exe 单文件。

bash 复制代码
pyinstaller --noconsole --onefile --name="Excel工具" --upx-dir="." "excel_tool.py"

找到您的 EXE

打包完成后,PyInstaller 会创建几个文件夹:

  • build:(临时文件,可以删除)

  • dist:您的程序在这里!

  • Excel工具.spec:(打包配置文件,可以保留)

您只需要进入 dist 文件夹。

如果使用选项 A,您需要将整个 Excel工具 文件夹 复制给其他人。

如果使用选项 B,您只需要将 Excel工具.exe 文件复制给其他人。

相关推荐
呉師傅1 小时前
【使用技巧】Adobe Photoshop 2024调整缩放与布局125%后出现点菜单项漂移问题的简单处理
运维·服务器·windows·adobe·电脑·photoshop
梦帮科技3 小时前
OpenClaw 桥接调用 Windows MCP:打造你的 AI 桌面自动化助手
人工智能·windows·自动化
春日见4 小时前
如何创建一个PR
运维·开发语言·windows·git·docker·容器
C++ 老炮儿的技术栈4 小时前
VS2015 + Qt 实现图形化Hello World(详细步骤)
c语言·开发语言·c++·windows·qt
浩浩测试一下5 小时前
内网---> WriteOwner权限滥用
网络·汇编·windows·安全·microsoft·系统安全
铁蛋AI编程实战6 小时前
MemoryLake 实战:构建超长对话 AI 助手的完整代码教程
人工智能·python·microsoft·机器学习
一个人旅程~6 小时前
Dell n4020双系统分区步骤和linux优化操作
linux·windows·电脑
love530love6 小时前
【高阶编译】Windows 环境下强制编译 Flash Attention:绕过 CUDA 版本不匹配高阶指南
人工智能·windows·python·flash_attn·flash-attn·flash-attention·定制编译
勾股导航6 小时前
Windows安装GPU环境
人工智能·windows·gnu
x***r1516 小时前
PhpStudy2018怎么用?完整安装与使用指南(新手必看)
windows