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 文件复制给其他人。

相关推荐
m0_674031436 小时前
GitHub等平台形成的开源文化正在重也有人
java·windows·mysql
牵牛老人6 小时前
Qt 中如何操作 Excel 表格:主流开源库说明介绍与 QXlsx 库应用全解析
qt·开源·excel
十碗饭吃不饱7 小时前
RuoYi/ExcelUtil修改(导入excel表时,表中字段没有映射上数据库表字段)
数据库·windows·excel
十五年专注C++开发7 小时前
Drogon: 一个开源的C++高性能Web框架
linux·c++·windows·后端开发·服务器开发
m0_674031437 小时前
GitHub等平台形成的开源文化正在重塑林语堂
windows·mysql·spring
搬砖的小码农_Sky8 小时前
如何从Windows 操作系统登录Linux(Ubuntu)操作系统
linux·windows·ubuntu·远程工作
weixin_438077498 小时前
langchain_neo4j 以及 neo4j (windows-community) 的学习使用
windows·langchain·neo4j
林月明16 小时前
【VBA】自动设置excel目标列的左邻列格式
开发语言·excel·vba·格式
JavaOpsPro19 小时前
审计 jenkins获取构建历史,生成excel
运维·jenkins·excel