统信小程序(十六)xls转xlsx

'''

每次转换都模拟打开并关闭xlsx文件,导致速速变慢。但是文件内容不再报错。能够顺利被xlsx读取程序识别。

该版本在linux中已经弃用xls2xlsx,因为其所依赖的xlrd是1.2.0版本。指定后提示Could not find a version that satisfies the requirement pillow<11,>=7.1.0 (from versions: )

No matching distribution found for pillow<11,>=7.1.0

因此移除 xls2xlsx 依赖,仅使用 xlrd + openpyxl。

使用中,进行了升级:增加文件夹转换覆写按钮,首先选择一个文件夹,然后遍历其内部所有文件,包括子文件夹,发现有xls,就自动转换为xlsx,并覆盖原文件。此操作非常高效,但进行前,应提示备份。

'''

复制代码
'''

每次转换都模拟打开并关闭xlsx文件,导致速速变慢。但是文件内容不再报错。能够顺利被xlsx读取程序识别。

改进版本:移除 xls2xlsx 依赖,仅使用 xlrd + openpyxl
新增功能:配置记忆、批量框选(linux的tk不支持框选,此时按shift多选)、统一输出目录

'''
复制代码
'''

每次转换都模拟打开并关闭xlsx文件,导致速速变慢。但是文件内容不再报错。能够顺利被xlsx读取程序识别。

改进版本:移除 xls2xlsx 依赖,仅使用 xlrd + openpyxl
新增功能:配置记忆、批量框选、统一输出目录、文件夹递归转换覆写

'''

import os
import sys
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
import queue
from pathlib import Path
import shutil
import tempfile
import json

print("=" * 60)
print("Python环境诊断信息")
print("=" * 60)
print(f"Python可执行文件: {sys.executable}")
print(f"Python版本: {sys.version}")
print(f"平台: {sys.platform}")
print("=" * 60)

# 检查基础依赖
try:
    import xlrd

    print(f"✓ xlrd 版本: {xlrd.__version__}")

    # 检查 xlrd 版本兼容性
    xlrd_version = tuple(map(int, xlrd.__version__.split('.')[:2]))
    if xlrd_version >= (2, 0):
        print("⚠️  警告: xlrd 2.0+ 不再支持 .xlsx 格式")
        print("   建议降级: pip install xlrd==1.2.0")
    else:
        print("✓ xlrd 版本兼容 (.xls 和 .xlsx)")

    from openpyxl import Workbook, load_workbook
    import openpyxl

    print(f"✓ openpyxl 版本: {openpyxl.__version__}")

    from openpyxl.utils import get_column_letter

    print("✓ openpyxl.utils 导入成功")
except ImportError as e:
    error_msg = f"缺少基础依赖库: {e}\n请执行: pip install xlrd==1.2.0 openpyxl"
    print(error_msg)
    if 'tkinter' in sys.modules:
        messagebox.showerror("错误", error_msg)
    sys.exit(1)

print("=" * 60)
print("使用纯 xlrd + openpyxl 方案,无需 xls2xlsx")
print("=" * 60)


class ExcelConverter:
    def __init__(self, root):
        self.root = root
        self.root.title("WPS表格格式转换工具 (xlrd+openpyxl)")
        self.root.geometry("850x650")

        # 配置文件路径
        self.config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "慢速xls转xlsx_config.json")

        # 设置字体 - 使用标准方法
        self.font_style = ("宋体", 12)
        self.small_font_style = ("宋体", 10)

        # 配置ttk样式
        self._configure_styles()

        # 文件列表
        self.file_list = []

        # 处理控制
        self.queue = queue.Queue()
        self.process_running = False
        self.stop_requested = False

        # 最后打开的目录
        self.last_open_dir = self.get_desktop_path()

        # 创建界面
        self.create_widgets()

        # 加载配置
        self.load_config()

        # 启动队列处理器
        self.process_queue()

        # 窗口关闭时保存配置
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def _configure_styles(self):
        """配置ttk样式"""
        self.style = ttk.Style()

        # 为不同组件配置样式
        self.style.configure('TLabel', font=self.font_style)
        self.style.configure('TButton', font=self.font_style)
        self.style.configure('TCheckbutton', font=self.font_style)
        self.style.configure('TEntry', font=self.font_style)

    def create_widgets(self):
        """创建界面组件"""
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # 环境信息显示
        env_frame = ttk.LabelFrame(main_frame, text="环境信息", padding="5")
        env_frame.pack(fill=tk.X, pady=5)

        # 构建环境信息文本
        env_info_lines = [
            f"Python: {sys.executable}",
            f"版本: {sys.version.split()[0]}",
            f"xlrd: {xlrd.__version__}",
            f"openpyxl: {openpyxl.__version__}",
            "转换引擎: xlrd + openpyxl"
        ]

        env_text = " | ".join(env_info_lines)
        tk.Label(env_frame, text=env_text, justify=tk.LEFT,
                 font=("宋体", 9)).pack(anchor=tk.W, padx=5, pady=3)

        # 文件选择部分
        file_frame = ttk.LabelFrame(main_frame, text="文件选择", padding="5")
        file_frame.pack(fill=tk.X, pady=5)

        # 文件列表框
        listbox_frame = ttk.Frame(file_frame)
        listbox_frame.pack(fill=tk.X, pady=5)

        self.file_listbox = tk.Listbox(listbox_frame, height=6, font=self.small_font_style)
        self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        scrollbar = ttk.Scrollbar(listbox_frame, command=self.file_listbox.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.file_listbox.config(yscrollcommand=scrollbar.set)

        # 文件操作按钮
        file_btn_frame = ttk.Frame(file_frame)
        file_btn_frame.pack(fill=tk.X, pady=5)

        ttk.Button(file_btn_frame, text="添加文件",
                   command=self.add_files).pack(side=tk.LEFT, padx=5)
        ttk.Button(file_btn_frame, text="添加文件夹",
                   command=self.add_folder_files).pack(side=tk.LEFT, padx=5)
        ttk.Button(file_btn_frame, text="移除选中",
                   command=self.remove_files).pack(side=tk.LEFT, padx=5)
        ttk.Button(file_btn_frame, text="清空列表",
                   command=self.clear_files).pack(side=tk.LEFT, padx=5)
        ttk.Button(file_btn_frame, text="文件夹批量转换覆写",
                   command=self.batch_convert_folder).pack(side=tk.LEFT, padx=5)
        
        tk.Label(file_btn_frame, text="[警告] 直接覆盖原文件,请先备份!",
                font=("宋体", 9), fg="red").pack(side=tk.LEFT, padx=10)

        # 输出目录和备注部分 - 放在同一行
        output_remark_frame = ttk.Frame(main_frame)
        output_remark_frame.pack(fill=tk.X, pady=5)

        # 输出目录选择
        output_frame = ttk.LabelFrame(output_remark_frame, text="输出目录", padding="5")
        output_frame.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))

        output_subframe = ttk.Frame(output_frame)
        output_subframe.pack(fill=tk.X)

        self.output_var = tk.StringVar()
        output_entry = ttk.Entry(output_subframe, textvariable=self.output_var, font=self.font_style)
        output_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))

        ttk.Button(output_subframe, text="浏览",
                   command=self.browse_output).pack(side=tk.RIGHT)

        # 备注部分
        remark_frame = ttk.LabelFrame(output_remark_frame, text="文件夹命名备注", padding="5")
        remark_frame.pack(side=tk.RIGHT, fill=tk.X, padx=(5, 0))

        remark_subframe = ttk.Frame(remark_frame)
        remark_subframe.pack(fill=tk.X)

        ttk.Label(remark_subframe, text="备注1:").pack(side=tk.LEFT)
        self.remark1_var = tk.StringVar()
        ttk.Entry(remark_subframe, textvariable=self.remark1_var,
                  width=10, font=self.font_style).pack(side=tk.LEFT, padx=5)

        ttk.Label(remark_subframe, text="备注2:").pack(side=tk.LEFT)
        self.remark2_var = tk.StringVar()
        ttk.Entry(remark_subframe, textvariable=self.remark2_var,
                  width=10, font=self.font_style).pack(side=tk.LEFT, padx=5)

        # 清理选项部分 - 放在同一行
        clean_frame = ttk.LabelFrame(main_frame, text="数据清理选项", padding="5")
        clean_frame.pack(fill=tk.X, pady=5)

        clean_subframe = ttk.Frame(clean_frame)
        clean_subframe.pack(fill=tk.X)

        self.clean_space_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(clean_subframe, text="清理空格",
                        variable=self.clean_space_var).pack(side=tk.LEFT, padx=10)

        self.clean_empty_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(clean_subframe, text="清理空字符串",
                        variable=self.clean_empty_var).pack(side=tk.LEFT, padx=10)

        ttk.Label(clean_subframe, text="清理指定内容:").pack(side=tk.LEFT, padx=(20, 5))
        self.clean_content_var = tk.StringVar()
        ttk.Entry(clean_subframe, textvariable=self.clean_content_var,
                  width=15, font=self.font_style).pack(side=tk.LEFT, padx=5)

        # 状态显示
        status_frame = ttk.LabelFrame(main_frame, text="转换状态", padding="5")
        status_frame.pack(fill=tk.BOTH, expand=True, pady=5)

        self.status_text = tk.Text(status_frame, height=10, font=self.small_font_style)
        self.status_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        status_scrollbar = ttk.Scrollbar(status_frame, command=self.status_text.yview)
        status_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.status_text.config(yscrollcommand=status_scrollbar.set)

        # 按钮部分
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X, pady=10)

        self.convert_btn = ttk.Button(button_frame, text="开始转换",
                                      command=self.start_conversion)
        self.convert_btn.pack(side=tk.LEFT, padx=20)

        self.stop_btn = ttk.Button(button_frame, text="停止转换",
                                   command=self.stop_conversion, state=tk.DISABLED)
        self.stop_btn.pack(side=tk.LEFT, padx=20)

    def load_config(self):
        """加载配置文件"""
        try:
            if os.path.exists(self.config_file):
                with open(self.config_file, 'r', encoding='utf-8') as f:
                    config = json.load(f)

                # 恢复输出目录
                if 'output_dir' in config:
                    self.output_var.set(config['output_dir'])

                # 恢复备注
                if 'remark1' in config:
                    self.remark1_var.set(config['remark1'])
                if 'remark2' in config:
                    self.remark2_var.set(config['remark2'])

                # 恢复清理选项
                if 'clean_space' in config:
                    self.clean_space_var.set(config['clean_space'])
                if 'clean_empty' in config:
                    self.clean_empty_var.set(config['clean_empty'])
                if 'clean_content' in config:
                    self.clean_content_var.set(config['clean_content'])

                # 恢复最后打开的目录
                if 'last_open_dir' in config:
                    self.last_open_dir = config['last_open_dir']
                
                # 恢复最后批量转换的文件夹
                if 'last_batch_folder' in config:
                    self.last_batch_folder = config['last_batch_folder']
                else:
                    self.last_batch_folder = self.last_open_dir

                self.log_status("配置已加载", "info")
            else:
                self.last_open_dir = self.get_desktop_path()
                self.last_batch_folder = self.last_open_dir
        except Exception as e:
            self.last_open_dir = self.get_desktop_path()
            self.last_batch_folder = self.last_open_dir
            self.log_status(f"加载配置失败: {str(e)}", "warning")

    def save_config(self):
        """保存配置文件"""
        try:
            config = {
                'output_dir': self.output_var.get(),
                'remark1': self.remark1_var.get(),
                'remark2': self.remark2_var.get(),
                'clean_space': self.clean_space_var.get(),
                'clean_empty': self.clean_empty_var.get(),
                'clean_content': self.clean_content_var.get(),
                'last_open_dir': self.last_open_dir,
                'last_batch_folder': getattr(self, 'last_batch_folder', self.last_open_dir)
            }

            with open(self.config_file, 'w', encoding='utf-8') as f:
                json.dump(config, f, ensure_ascii=False, indent=2)

            self.log_status("配置已保存", "info")
        except Exception as e:
            self.log_status(f"保存配置失败: {str(e)}", "error")

    def on_closing(self):
        """窗口关闭时的处理"""
        self.save_config()
        self.root.destroy()

    def log_status(self, message, level="info"):
        """记录状态信息"""
        timestamp = ""
        if level == "error":
            prefix = "❌ 错误"
        elif level == "warning":
            prefix = "⚠️ 警告"
        else:
            prefix = "ℹ️ 信息"

        log_line = f"{prefix}: {message}\n"
        self.status_text.insert(tk.END, log_line)
        self.status_text.see(tk.END)

    def get_desktop_path(self):
        """获取桌面路径"""
        import platform
        system = platform.system()
        if system == "Windows":
            return os.path.join(os.path.expanduser("~"), "Desktop")
        elif system == "Linux":
            return os.path.join(os.path.expanduser("~"), "Desktop")
        else:
            return os.path.expanduser("~")

    def add_files(self):
        """添加文件到列表(支持多选)"""
        files = filedialog.askopenfilenames(
            title="选择Excel文件",
            initialdir=self.last_open_dir,
            filetypes=[("Excel files", "*.xls *.xlsx *.et"), ("All files", "*.*")]
        )
        if files:
            # 保存最后打开的目录
            self.last_open_dir = os.path.dirname(files[0])

            added_count = 0
            for file in files:
                if file not in self.file_list:
                    self.file_list.append(file)
                    self.file_listbox.insert(tk.END, file)
                    added_count += 1

            if added_count > 0:
                self.log_status(f"已添加 {added_count} 个文件", "info")
        else:
            self.log_status("未选择任何文件", "warning")

    def add_folder_files(self):
        """从文件夹中添加所有Excel文件"""
        folder_path = filedialog.askdirectory(
            title="选择包含Excel文件的文件夹",
            initialdir=self.last_open_dir
        )
        if not folder_path:
            self.log_status("未选择文件夹", "warning")
            return

        # 保存最后打开的目录
        self.last_open_dir = folder_path

        # 扫描文件夹中的所有Excel文件
        excel_files = []
        for filename in os.listdir(folder_path):
            if filename.lower().endswith(('.xlsx', '.xls', '.et')) and not filename.startswith('~$'):
                full_path = os.path.join(folder_path, filename)
                if os.path.isfile(full_path):
                    excel_files.append(full_path)

        if not excel_files:
            self.log_status(f"在文件夹中未找到Excel文件:{folder_path}", "warning")
            return

        # 添加到列表(去重)
        existing_files = set(self.file_list)
        added_count = 0
        for f in sorted(excel_files):
            if f not in existing_files:
                self.file_list.append(f)
                self.file_listbox.insert(tk.END, f)
                existing_files.add(f)
                added_count += 1

        if added_count > 0:
            self.log_status(f"已从文件夹添加 {added_count} 个Excel文件", "info")
        else:
            self.log_status("所选文件已全部存在,未添加新文件", "warning")

    def remove_files(self):
        """移除选中的文件"""
        selected = self.file_listbox.curselection()
        if selected:
            for index in selected[::-1]:
                self.file_list.pop(index)
                self.file_listbox.delete(index)

    def clear_files(self):
        """清空文件列表"""
        self.file_list.clear()
        self.file_listbox.delete(0, tk.END)

    def browse_output(self):
        """选择输出目录"""
        directory = filedialog.askdirectory(
            title="选择输出目录",
            initialdir=self.last_open_dir
        )
        if directory:
            self.output_var.set(directory)
            self.last_open_dir = directory

    def batch_convert_folder(self):
        """文件夹批量转换并覆写原文件"""
        # 警告提示
        warning_msg = """⚠ 高危操作警告 ⚠

此操作将:
1. 递归遍历选定文件夹及其所有子文件夹
2. 找到所有 .xls 和 .et 文件
3. 转换为 .xlsx 格式
4. 【直接覆盖】原文件(原文件将被删除)

重要提示:
• 此操作不可逆!
• 强烈建议先备份整个文件夹!
• 转换前请确认重要数据已备份

是否继续?"""
        
        if not messagebox.askyesno("高危操作确认", warning_msg, icon="warning"):
            self.log_status("用户取消了批量转换操作", "warning")
            return
        
        # 选择文件夹(使用记忆的路径)
        folder_path = filedialog.askdirectory(
            title="选择要批量转换的文件夹",
            initialdir=getattr(self, 'last_batch_folder', self.last_open_dir)
        )
        
        if not folder_path:
            self.log_status("未选择文件夹", "warning")
            return
        
        # 保存最后选择的批量转换文件夹
        self.last_batch_folder = folder_path
        self.last_open_dir = folder_path
        
        # 启动后台线程执行批量转换
        thread = threading.Thread(target=self._execute_batch_convert, args=(folder_path,))
        thread.daemon = True
        thread.start()

    def _execute_batch_convert(self, folder_path):
        """执行批量转换(后台线程)"""
        self.process_running = True
        self.stop_requested = False
        self.root.after(0, lambda: self.convert_btn.config(state=tk.DISABLED))
        self.root.after(0, lambda: self.stop_btn.config(state=tk.NORMAL))
        self.root.after(0, lambda: self.status_text.delete(1.0, tk.END))
        
        self.log_status(f"开始扫描文件夹: {folder_path}", "info")
        
        # 递归查找所有 .xls 和 .et 文件
        xls_files = []
        for root, dirs, files in os.walk(folder_path):
            if self.stop_requested:
                self.log_status("用户终止操作", "warning")
                break
            
            for filename in files:
                if filename.lower().endswith(('.xls', '.et')) and not filename.startswith('~$'):
                    full_path = os.path.join(root, filename)
                    xls_files.append(full_path)
        
        if not xls_files:
            self.log_status("未找到需要转换的文件", "warning")
            self.process_running = False
            self.root.after(0, lambda: self.convert_btn.config(state=tk.NORMAL))
            self.root.after(0, lambda: self.stop_btn.config(state=tk.DISABLED))
            return
        
        total_files = len(xls_files)
        self.log_status(f"找到 {total_files} 个需要转换的文件", "info")
        self.log_status("=" * 50, "info")
        
        success_count = 0
        fail_count = 0
        processed = 0
        
        for file_path in xls_files:
            if self.stop_requested:
                self.log_status("用户终止操作", "warning")
                break
            
            temp_file = None
            try:
                processed += 1
                relative_path = os.path.relpath(file_path, folder_path)
                self.log_status(f"[{processed}/{total_files}] 转换: {relative_path}")
                
                # 转换为临时文件
                temp_file = file_path + ".temp.xlsx"
                success, message = self._convert_via_xlrd(file_path, temp_file)
                
                if success:
                    # 验证临时文件是否有效
                    if not self._validate_xlsx_file(temp_file):
                        self.log_status(f"  ✗ 失败: 生成的文件无效", "error")
                        fail_count += 1
                        if os.path.exists(temp_file):
                            os.remove(temp_file)
                        continue
                    
                    # 应用清理选项
                    if (self.clean_space_var.get() or self.clean_empty_var.get() or
                            self.clean_content_var.get()):
                        try:
                            self._apply_cleaning(temp_file)
                        except Exception as clean_err:
                            self.log_status(f"  ⚠ 清理失败: {str(clean_err)},跳过清理继续转换", "warning")
                    
                    # 备份原文件(添加.bak扩展名)
                    backup_file = file_path + ".bak"
                    shutil.move(file_path, backup_file)
                    
                    # 将临时文件重命名为原文件名(但扩展名为.xlsx)
                    xlsx_file = file_path.rsplit('.', 1)[0] + '.xlsx'
                    shutil.move(temp_file, xlsx_file)
                    
                    # 删除备份文件(可选,如果想保留备份可以注释掉这行)
                    os.remove(backup_file)
                    
                    self.log_status(f"  ✓ 成功: {os.path.basename(xlsx_file)}", "info")
                    success_count += 1
                    temp_file = None  # 标记已成功移动,不需要清理
                else:
                    # 转换失败,删除临时文件
                    self.log_status(f"  ✗ 失败: {message}", "error")
                    fail_count += 1
                    
            except Exception as e:
                self.log_status(f"  ✗ 错误: {str(e)}", "error")
                fail_count += 1
            finally:
                # 清理临时文件(如果还存在)
                if temp_file and os.path.exists(temp_file):
                    try:
                        os.remove(temp_file)
                    except:
                        pass
        
        # 完成
        self.log_status("=" * 50, "info")
        self.log_status(f"批量转换完成! 成功: {success_count}, 失败: {fail_count}", "info")
        
        self.process_running = False
        self.root.after(0, lambda: self.convert_btn.config(state=tk.NORMAL))
        self.root.after(0, lambda: self.stop_btn.config(state=tk.DISABLED))
        
        if not self.stop_requested:
            self.root.after(0, lambda: messagebox.showinfo(
                "完成",
                f"批量转换完成!\n成功: {success_count} 个文件\n失败: {fail_count} 个文件\n\n原文件已被替换为.xlsx格式"
            ))

    def _validate_xlsx_file(self, file_path):
        """验证 xlsx 文件是否有效"""
        try:
            wb = load_workbook(file_path, read_only=True)
            wb.close()
            return True
        except:
            return False

    def start_conversion(self):
        """开始转换"""
        if not self.file_list:
            messagebox.showwarning("警告", "请先添加要转换的文件")
            return

        self.process_running = True
        self.stop_requested = False
        self.convert_btn.config(state=tk.DISABLED)
        self.stop_btn.config(state=tk.NORMAL)

        # 清空状态文本
        self.status_text.delete(1.0, tk.END)

        # 启动转换线程
        thread = threading.Thread(target=self.convert_files)
        thread.daemon = True
        thread.start()

    def stop_conversion(self):
        """停止转换"""
        self.stop_requested = True
        self.log_status("正在停止转换...", "warning")

    def process_queue(self):
        """处理消息队列"""
        try:
            while True:
                task = self.queue.get_nowait()
                if task[0] == "message":
                    self.status_text.insert(tk.END, task[1] + "\n")
                    self.status_text.see(tk.END)
                elif task[0] == "progress":
                    self.update_progress(task[1])
        except queue.Empty:
            pass
        finally:
            self.root.after(100, self.process_queue)

    def update_progress(self, progress_info):
        """更新进度"""
        current, total = progress_info
        self.convert_btn.config(text=f"转换中 ({current}/{total})")

    def get_output_directory(self):
        """获取统一的输出目录"""
        if self.output_var.get():
            output_dir = Path(self.output_var.get())
        else:
            # 如果没有指定输出目录,使用第一个文件的目录
            if self.file_list:
                input_path = Path(self.file_list[0])
                parent_dir = input_path.parent

                # 构建新目录名
                remark1 = self.remark1_var.get()
                remark2 = self.remark2_var.get()
                new_dir_name = f"{remark1}{parent_dir.name}xlsx{remark2}"

                # 确保目录名唯一
                output_dir = parent_dir / new_dir_name
                counter = 1
                while output_dir.exists():
                    output_dir = parent_dir / f"{new_dir_name}_{counter}"
                    counter += 1
            else:
                # 如果没有文件,使用桌面
                output_dir = Path(self.get_desktop_path()) / "转换结果"

        # 创建目录(如果不存在)
        output_dir.mkdir(parents=True, exist_ok=True)
        return output_dir

    def should_clean_cell(self, cell_value):
        """判断是否应该清理单元格"""
        if cell_value is None:
            return False

        if not isinstance(cell_value, str):
            return False

        content = str(cell_value).strip()

        # 检查清理指定内容
        clean_pattern = self.clean_content_var.get().strip()
        if clean_pattern and clean_pattern in content:
            return True

        # 检查空格和空字符串清理
        if self.clean_space_var.get() and self.clean_empty_var.get():
            # 同时清理空格和空字符串
            if not content or content.replace(' ', '').replace(' ', '') == '':
                return True
        elif self.clean_space_var.get():
            # 只清理空格
            if content.replace(' ', '').replace(' ', '') == '' and content != '':
                return True
        elif self.clean_empty_var.get():
            # 只清理空字符串
            if content == '':
                return True

        return False

    def convert_files(self):
        """转换文件的主函数"""
        total_files = len(self.file_list)
        processed_files = 0
        success_count = 0
        fail_count = 0

        # 获取统一的输出目录
        output_dir = self.get_output_directory()
        self.log_status(f"输出目录: {output_dir}", "info")

        for file_path in self.file_list:
            if self.stop_requested:
                break

            try:
                self.log_status(f"正在处理: {os.path.basename(file_path)}")

                input_path = Path(file_path)
                output_file = output_dir / f"{input_path.stem}.xlsx"

                # 根据文件扩展名选择转换方法
                file_ext = input_path.suffix.lower()
                success = False
                message = ""

                if file_ext in ['.xls', '.et']:
                    # 使用xlrd转换XLS/ET文件
                    success, message = self._convert_via_xlrd(file_path, output_file)
                elif file_ext == '.xlsx':
                    # 处理XLSX文件
                    success, message = self._process_xlsx_file(file_path, output_file)
                else:
                    message = f"不支持的文件格式: {file_ext}"

                if success:
                    # 应用清理选项
                    if (self.clean_space_var.get() or self.clean_empty_var.get() or
                            self.clean_content_var.get()):
                        self._apply_cleaning(output_file)

                    self.log_status(f"✓ {os.path.basename(file_path)} - {message}", "info")
                    success_count += 1
                else:
                    self.log_status(f"✗ {os.path.basename(file_path)} - {message}", "error")
                    fail_count += 1

                processed_files += 1
                self.queue.put(("progress", (processed_files, total_files)))

            except Exception as e:
                self.log_status(f"✗ 处理文件时出错 {os.path.basename(file_path)}: {str(e)}", "error")
                fail_count += 1

        # 恢复按钮状态
        self.process_running = False
        self.root.after(100, self._reset_buttons)

        # 显示总结
        self.log_status("=" * 50, "info")
        self.log_status(f"转换完成! 成功: {success_count}, 失败: {fail_count}", "info")
        self.log_status(f"输出目录: {output_dir}", "info")
        self.log_status("=" * 50, "info")

        if not self.stop_requested:
            messagebox.showinfo("完成",
                                f"转换完成!\n成功: {success_count} 个文件\n失败: {fail_count} 个文件\n\n输出目录:\n{output_dir}")

    def _reset_buttons(self):
        """重置按钮状态"""
        self.convert_btn.config(state=tk.NORMAL, text="开始转换")
        self.stop_btn.config(state=tk.DISABLED)

    def _convert_via_xlrd(self, source_file, xlsx_file):
        """通过xlrd读取并转换为xlsx"""
        try:
            workbook = xlrd.open_workbook(source_file)
            wb = Workbook()

            # 删除默认创建的工作表
            wb.remove(wb.active)

            for sheet_index in range(workbook.nsheets):
                sheet = workbook.sheet_by_index(sheet_index)

                # 创建新工作表
                if sheet_index == 0:
                    ws = wb.active
                    ws.title = sheet.name[:31]  # Excel工作表名最长31字符
                else:
                    ws = wb.create_sheet(sheet.name[:31])

                # 复制数据
                for row in range(sheet.nrows):
                    for col in range(sheet.ncols):
                        try:
                            cell_value = sheet.cell_value(row, col)
                            if cell_value is not None and cell_value != "":
                                ws.cell(row=row + 1, column=col + 1, value=cell_value)
                        except:
                            continue

            wb.save(xlsx_file)
            return True, "通过xlrd转换成功"
        except Exception as e:
            # 备用方案:直接复制
            try:
                shutil.copy2(source_file, xlsx_file)
                return True, "通过复制文件处理"
            except Exception as e2:
                return False, f"转换失败: {e}, 备用方案也失败: {e2}"

    def _process_xlsx_file(self, source_file, target_file):
        """处理XLSX文件"""
        try:
            shutil.copy2(source_file, target_file)
            return True, "文件已复制"
        except Exception as e:
            return False, f"处理失败: {e}"

    def _apply_cleaning(self, file_path):
        """应用清理选项"""
        try:
            wb = load_workbook(file_path)
            modified = False

            for sheet_name in wb.sheetnames:
                ws = wb[sheet_name]
                for row in ws.iter_rows():
                    for cell in row:
                        if cell.value is not None and self.should_clean_cell(cell.value):
                            cell.value = None
                            modified = True

            if modified:
                wb.save(file_path)

        except Exception as e:
            self.log_status(f"清理失败 {os.path.basename(file_path)}: {str(e)}", "error")


def main():
    """主函数"""
    root = tk.Tk()
    app = ExcelConverter(root)
    root.mainloop()


if __name__ == "__main__":
    main()
相关推荐
孝顺的书包1 小时前
将《C# 调用非托管程序》一文中最后一种方法修改如下(篇幅原因简化了注释):
开发语言·c#
梦想三三1 小时前
Flask + PyTorch模型部署实战:从训练权重到API接口完整工程解析(附完整代码)
人工智能·pytorch·python·flask·模型推理·ai 工程化
不知名的老吴1 小时前
浅谈:编程语言中的那些「锁」事(二)
java·开发语言
杨超越luckly1 小时前
Agent 应用指南:基于 OurAirports 的中国机场设施数据可视化
python·html·github·可视化·机场设施
卷无止境2 小时前
pytest 从零到实战:要想代码好,测试少不了
后端·python
吃糖的小孩2 小时前
# RootGraph v1.5 收工:我给 QQ 机器人补上了聊天运行时的“黑匣子”
python
卓怡学长2 小时前
w272基于springboot便民医疗服务小程序
java·spring boot·spring·小程序·intellij-idea
蜗牛~turbo2 小时前
金蝶云星空的网络控制设置
开发语言·c#·金蝶·erp·云星空·k3 cloud
hangyuekejiGEO2 小时前
临沂GEO技术服务方案对比分析
大数据·人工智能·python