win10程序(七)暴力xls转xlsx程序
'''
暴力转换,不论xls是不是这个格式。
因为有个的软件导出的xls实际上有xml压缩,一次会被错误识别,这个程序在转换时如果发现错误,直接强制转换扩展名,目前还没发现问题。同时避免了xlrd的1.2版本限制问题。
使用tkinter和 xls2xlsx制作python表格转格式程序。功能是将一个文件夹中一个或多个文件,不论是xls还是xlsx,统一另存为xlsx。窗口的字体均为宋体,字号12,窗口尺寸850*600,有执行按键,停止按键。适应大量数据,至少11万行。使用标准的Tkinter字体配置方法,避免处理过程中出错: self.tk.call(_tkinter.TclError: unknown option "-font"。避免处理过程中出错: '<' not supported between instances of 'int' and 'NoneType'。避免output_dir = self.custom_output_dir or self. SyntaxError: invalid syntax错误
1.
文件选择框。应该支持可选一个或者多个文件。注意:维持源文件xls中的内容,尤其是其中的字体、字号、格式不要变化。
具备转换状态提示功能。如果文件格式与扩展名不符,例如xls文件实际是xlsx格式或者反之,或者转换出错时,则直接改变扩展名为xlsx另存,并提示处理方式是直接转扩展名。
“备注1”和“备注2”输入框,默认均为空,用于修改另存为文件夹的名称。
2.
另存文件位置选择框,可选择转换成的文件的另存位置,默认为空,如果为空则在原文件夹新建一个名为“备注”+原文件夹名+“xlsx”+“备注2”的文件夹,将转换后的文件按放入,不改变文件名。如果原来已经有改名字的文件夹则自动添加序号另存。注意:同一批转换的文件应另存入同一文件夹,而不是每个文件单独建文件夹。
3.数据i清理功能。
半角空格清理选项,默认选中,如单元格内容是半角空格,则另存时删除单元格内容及格式。
全角空格清理选项,默认选中,如单元格内容是全角空格,则另存时删除单元格内容及格式。
清理数功能输入框1:默认值为空,如果有内容,则删除所有单元格内对应的这个内容。
注意:软件窗口安排应紧凑,例如另存位置重置路径之类能放在同一行的尽量压缩窗口高度。
注意:检查所有功能是否实现,不可忽略。
'''
import os import tkinter as tk from tkinter import filedialog, ttk, messagebox, font import threading from xls2xlsx import XLS2XLSX import openpyxl# 检测真实文件格式 def is_xlsx_file(file_path):try:with open(file_path, 'rb') as f:return f.read(4) == b'PK\x03\x04'except Exception:return Falsedef is_xls_file(file_path):try:with open(file_path, 'rb') as f:return f.read(8) == b'\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1'except Exception:return Falseclass ExcelConverterApp:def __init__(self, root):self.root = rootself.root.title("Excel 批量转 XLSX(保留格式)")self.root.geometry("850x600")self.root.minsize(750, 550)# 设置全局字体:宋体 12号default_font = font.nametofont("TkDefaultFont")default_font.configure(family="宋体", size=12)self.root.option_add("*Font", default_font)self.is_running = Falseself.stop_requested = Falseself.file_list = []self.custom_output_dir = "" # 用户自选路径self.final_output_dir = "" # 实际使用的输出文件夹(统一一个)self.setup_ui()def setup_ui(self):main_frame = ttk.Frame(self.root, padding="10")main_frame.pack(fill=tk.BOTH, expand=True)# === 1. 文件选择区 ===file_frame = ttk.LabelFrame(main_frame, text="1. 选择文件", padding="5")file_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 8))# 文件列表显示self.file_text = tk.Text(file_frame, height=6, wrap=tk.WORD)self.file_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)btn_frame1 = ttk.Frame(file_frame)btn_frame1.pack(side=tk.RIGHT, padx=(10, 0))ttk.Button(btn_frame1, text="选择文件", command=self.select_files).pack(fill=tk.X, pady=2)ttk.Button(btn_frame1, text="清空列表", command=self.clear_files).pack(fill=tk.X, pady=2)# === 2. 命名与输出路径 ===path_frame = ttk.LabelFrame(main_frame, text="2. 命名与保存位置", padding="5")path_frame.pack(fill=tk.X, pady=(0, 8))# 备注输入ttk.Label(path_frame, text="备注1:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))self.remark1_var = tk.StringVar()ttk.Entry(path_frame, textvariable=self.remark1_var, width=12).grid(row=0, column=1, padx=(0, 10))ttk.Label(path_frame, text="备注2:").grid(row=0, column=2, sticky=tk.W, padx=(0, 5))self.remark2_var = tk.StringVar()ttk.Entry(path_frame, textvariable=self.remark2_var, width=12).grid(row=0, column=3, padx=(0, 10))# 输出路径显示output_label_frame = ttk.Frame(path_frame)output_label_frame.grid(row=1, column=0, columnspan=4, sticky=tk.EW, pady=(5, 0))# 更新提示文本self.output_var = tk.StringVar(value="(默认:在原文件夹创建 '备注1+原文件夹名+xlsx+备注2' 文件夹)")ttk.Label(output_label_frame, textvariable=self.output_var, wraplength=600).pack(fill=tk.X)# 按钮行(紧凑)btn_frame2 = ttk.Frame(path_frame)btn_frame2.grid(row=2, column=0, columnspan=4, pady=(5, 0), sticky=tk.E)ttk.Button(btn_frame2, text="选择另存位置", command=self.select_output_dir).pack(side=tk.LEFT, padx=(0, 5))ttk.Button(btn_frame2, text="重置路径", command=self.reset_output_dir).pack(side=tk.LEFT)path_frame.columnconfigure(0, weight=1)# === 3. 数据清理 ===clean_frame = ttk.LabelFrame(main_frame, text="3. 数据清理", padding="5")clean_frame.pack(fill=tk.X, pady=(0, 8))self.clean_halfspace = tk.BooleanVar(value=True)self.clean_fullspace = tk.BooleanVar(value=True)ttk.Checkbutton(clean_frame, text="清理半角空格", variable=self.clean_halfspace).pack(side=tk.LEFT)ttk.Checkbutton(clean_frame, text="清理全角空格", variable=self.clean_fullspace).pack(side=tk.LEFT, padx=(10, 0))#ttk.Label(clean_frame, text="清理指定内容:").pack(anchor=tk.W, pady=(3, 0))self.clean_text_var = tk.StringVar()ttk.Entry(clean_frame, textvariable=self.clean_text_var).pack(fill=tk.X, pady=(0, 0))# === 4. 操作按钮 ===action_frame = ttk.Frame(main_frame)action_frame.pack(fill=tk.X, pady=(0, 8))self.execute_btn = ttk.Button(action_frame, text="▶ 执行转换", command=self.start_conversion)self.execute_btn.pack(side=tk.LEFT)self.stop_btn = ttk.Button(action_frame, text="■ 停止", command=self.request_stop, state=tk.DISABLED)self.stop_btn.pack(side=tk.LEFT, padx=(5, 0))# === 5. 日志区域 ===log_frame = ttk.LabelFrame(main_frame, text="4. 转换状态", padding="5")log_frame.pack(fill=tk.BOTH, expand=True)self.log_text = tk.Text(log_frame, wrap=tk.WORD, height=10, state=tk.DISABLED)scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview)self.log_text.configure(yscrollcommand=scrollbar.set)self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)scrollbar.pack(side=tk.RIGHT, fill=tk.Y)def log(self, message):self.log_text.config(state=tk.NORMAL)self.log_text.insert(tk.END, message + "\n")self.log_text.see(tk.END)self.log_text.config(state=tk.DISABLED)self.root.update_idletasks()def select_files(self):files = filedialog.askopenfilenames(title="选择 Excel 文件",filetypes=[("Excel 文件", "*.xls *.xlsx"), ("所有文件", "*.*")])if files:self.file_list = list(files)self.file_text.delete(1.0, tk.END)for f in self.file_list:self.file_text.insert(tk.END, os.path.basename(f) + "\n")def clear_files(self):self.file_list = []self.file_text.delete(1.0, tk.END)def select_output_dir(self):selected = filedialog.askdirectory(title="选择统一保存文件夹")if selected:self.custom_output_dir = selectedself.output_var.set(selected)def reset_output_dir(self):self.custom_output_dir = ""# 更新提示文本self.output_var.set("(默认:在原文件夹创建 '备注1+原文件夹名+xlsx+备注2' 文件夹)")def get_output_folder(self):"""为整批文件生成统一的输出文件夹"""if self.custom_output_dir:os.makedirs(self.custom_output_dir, exist_ok=True)return self.custom_output_dirif not self.file_list:return Nonefirst_file = self.file_list[0]dir_path = os.path.dirname(first_file)folder_name = os.path.basename(dir_path) # 原文件夹名remark1 = self.remark1_var.get().strip()remark2 = self.remark2_var.get().strip()# 新的命名规则:备注1 + 原文件夹名 + xlsx + 备注2new_folder_name = ""if remark1:new_folder_name += remark1new_folder_name += folder_namenew_folder_name += "xlsx"if remark2:new_folder_name += remark2safe_name = "".join(c for c in new_folder_name if c.isalnum() or c in (' ', '-', '_'))base_path = os.path.join(dir_path, safe_name)output_dir = base_pathcounter = 1while os.path.exists(output_dir):output_dir = f"{base_path}_{counter}"counter += 1os.makedirs(output_dir, exist_ok=True)return output_dirdef start_conversion(self):if not self.file_list:messagebox.showwarning("警告", "请先选择至少一个文件!")returnif self.is_running:messagebox.showinfo("提示", "转换已在进行中,请勿重复点击。")return# 生成统一输出文件夹self.final_output_dir = self.get_output_folder()if not self.final_output_dir:messagebox.showerror("错误", "无法确定输出文件夹,请检查文件列表。")returnself.is_running = Trueself.stop_requested = Falseself.execute_btn.config(state=tk.DISABLED)self.stop_btn.config(state=tk.NORMAL)self.log(f"📁 所有文件将统一保存至:{self.final_output_dir}")thread = threading.Thread(target=self.convert_files, daemon=True)thread.start()def request_stop(self):self.stop_requested = Trueself.log("⏹ 正在请求停止...")def convert_files(self):total = len(self.file_list)self.log(f"开始转换,共 {total} 个文件...")for idx, file_path in enumerate(self.file_list):if self.stop_requested:self.log("🛑 用户停止,终止转换。")breakself.log(f"[{idx+1}/{total}] 处理: {os.path.basename(file_path)}")try:self.process_single_file(file_path)except Exception as e:self.log(f"❌ 失败: {str(e)}")self.finish_conversion()def process_single_file(self, file_path):ext = os.path.splitext(file_path)[1].lower()name = os.path.splitext(os.path.basename(file_path))[0]output_path = os.path.join(self.final_output_dir, f"{name}.xlsx")try:if is_xlsx_file(file_path):wb = openpyxl.load_workbook(file_path, keep_vba=False)self.apply_cleaning(wb)wb.save(output_path)wb.close()self.log(f"✅ 已处理: {output_path}")elif is_xls_file(file_path):temp_xlsx = os.path.join(self.final_output_dir, f"{name}_temp.xlsx")x2x = XLS2XLSX(file_path)x2x.to_xlsx(temp_xlsx)wb = openpyxl.load_workbook(temp_xlsx)self.apply_cleaning(wb)wb.save(output_path)wb.close()os.remove(temp_xlsx)self.log(f"✅ 已转换: {output_path}")else:# 格式异常,直接复制with open(file_path, 'rb') as src, open(output_path, 'wb') as dst:dst.write(src.read())self.log(f"📎 格式异常,已复制为: {output_path}")except Exception as e:self.log(f"⚠️ 失败,尝试直接复制: {e}")try:with open(file_path, 'rb') as src, open(output_path, 'wb') as dst:dst.write(src.read())self.log(f"📎 已直接复制为: {output_path}")except Exception as e2:self.log(f"❌ 直接复制也失败: {e2}")def apply_cleaning(self, wb):clean_half = self.clean_halfspace.get()clean_full = self.clean_fullspace.get()clean_text = self.clean_text_var.get().strip()space_half, space_full = ' ', ' 'for sheet in wb.worksheets:for row in sheet.iter_rows():for cell in row:val = cell.valueif not isinstance(val, str):continueval = val.strip()if (clean_half and val == space_half) or \(clean_full and val == space_full) or \(clean_text and val == clean_text):cell.value = Noneself.clear_cell_style(cell)@staticmethoddef clear_cell_style(cell):cell.font = openpyxl.styles.Font()cell.fill = openpyxl.styles.PatternFill()cell.border = openpyxl.styles.Border()cell.alignment = openpyxl.styles.Alignment()cell.number_format = 'General'def finish_conversion(self):self.is_running = Falseself.stop_requested = Falseself.execute_btn.config(state=tk.NORMAL)self.stop_btn.config(state=tk.DISABLED)if not self.stop_requested:self.log(f"🎉 全部完成!文件已保存至:{self.final_output_dir}")else:self.log("⏹ 转换已停止。")def run(self):self.root.mainloop()# === 启动应用 === if __name__ == "__main__":root = tk.Tk()app = ExcelConverterApp(root)app.run()