.to_excel的OSError报错

博主在使用pandas时,走到io.open处报错,出现OSError语法错误,经检查是文件名错误,错将文件内容当作文件名传入。博主表示刚开始接触数据分析,要一步步掌握debug,无惧语法问题。

开始用pandas的

to_excel方法时报的错

看到走到io.open时报错,OSError报语法错误,是文件名错误了,错把文件内容当作文件名传入

原:

data = pdr.get_data_yahoo("SPY", start="2017-01-01", end="2017-04-30")
data.to_excel('$s.xlsx'%data)
data.to_csv('$s.csv'%data)

改:

data.to_excel('test.xlsx')
data.to_csv('test.csv')

刚开始接触数据分析,一步步来吧,掌握debug,什么语法都不怕

现在没有报错了,但是文本文件里面的顺序也是对的,但是excel里面的顺序是不对的,行顺序不是按照大小排列的:信号名 变更类型 数据名 变更详情 行位置 SCM1S04 数据变更 XACCMAIN 備考: []→[0固定送信] 957 SCM1S04 数据变更 SETSP 備考: []→[0固定送信] 951 SCM1S04 数据变更 XACCACT 備考: []→[0固定送信] 953 SCM1S04 数据变更 XACCFAIL 備考: []→[0固定送信] 956 SCM1S04 数据变更 XACCBLINK 備考: []→[0固定送信] 971 SCM1S04 数据变更 XACCHOLD 備考: []→[0固定送信] 963 SCM1S04 数据变更 XACCTGTLOST 備考: []→[0固定送信] 961 SCM1S04 数据变更 XACCBADWETH 備考: []→[0固定送信] 972 SCM1S04 数据变更 XACCSTPRLS 備考: []→[0固定送信] 958 SCM1S04 数据变更 XCRUON 備考: []→[0固定送信] 965 SCM1S04 数据变更 ACCBUZ 備考: []→[0固定送信] 966 SCM1S04 数据变更 ACCMSG 備考: []→[0固定送信] 967 SCM1S04 数据变更 XACCOPNG 備考: []→[0固定送信] 964 SCM1S04 数据变更 XACCTARGET 備考: []→[0固定送信] 955 SCM1S04 数据变更 XACCTGTBUZ 備考: []→[0固定送信] 970 SCM1S04 数据变更 XDISTWARN 備考: []→[0固定送信] 962 SCM1S04 数据变更 ACCLRN 備考: []→[0固定送信] 969 SCM1S04 数据变更 SETHEADWAY 備考: []→[3固定送信] 952 SCM1S04 数据变更 XACCCANCEL 備考: []→[0固定送信] 954 import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import pandas as pd import os import threading import tempfile import zipfile import shutil import configparser from xml.etree import ElementTree as ET from openpyxl import load_workbook import re import datetime # 辅助函数:将Excel列字母转换为索引(A=0, B=1, ..., AA=26等) def excel_column_letter_to_index(letter): """Convert Excel column letter to zero-based index""" index = 0 for char in letter: index = index * 26 + (ord(char.upper()) - ord('A') + 1) return index - 1 class ExcelComparatorApp: def __init__(self, root): self.root = root self.root.title("Excel 文件比较工具") self.root.geometry("1000x950") self.root.minsize(900, 600) # 加载配置 self.config = configparser.ConfigParser() self.config_path = os.path.join(os.path.expanduser("~"), "excel_comparator_config.ini") self.load_config() # 初始化配置相关的属性 self.initialize_config_based_attributes() # 配置样式 self.style = ttk.Style() self.style.configure("TFrame", padding=10) self.style.configure("TButton", padding=6) self.style.configure("TLabel", padding=5) self.style.configure("TCheckbutton", padding=5) self.style.configure("Accent.TButton", background="#4CAF50", foreground="white", font=("Arial", 10, "bold")) # 主容器使用网格布局管理器 self.main_frame = ttk.Frame(root) self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 创建界面 self.create_interface() # 初始化变量 self.old_file = "" self.new_file = "" # 确保界面元素可见 self.root.update_idletasks() # 初始化后尝试加载列名 self.root.after(100, self.try_load_columns) def initialize_config_based_attributes(self): """初始化所有配置相关的属性""" # 文件路径 self.old_file = "" self.new_file = "" # 表格设置 self.skiprows = self.config.getint('SETTINGS', 'skiprows', fallback=9) # 列范围(使用Excel字母格式) self.signal_start_col = 0 self.signal_end_col = 12 self.data_start_col = 53 self.data_end_col = 55 # 报告类型变量 self.generate_text_report = tk.BooleanVar( value=self.config.getboolean('SETTINGS', 'generate_text_report', fallback=True)) self.generate_excel_report = tk.BooleanVar( value=self.config.getboolean('SETTINGS', 'generate_excel_report', fallback=True)) # 列加载基准文件选项 self.col_load_based_on = tk.StringVar( value=self.config.get('SETTINGS', 'col_load_based_on', fallback='old')) # 列选择变量 self.signal_col_vars = {} self.data_col_vars = {} # 初始化其他变量 self.old_file_entry = None self.new_file_entry = None self.old_sheet_var = None self.new_sheet_var = None self.output_path_entry = None self.log_text = None self.status_var = None self.compare_btn = None # 进度条相关 self.progress = None self.status_label = None # 初始化 skiprows_var self.skiprows_var = tk.StringVar(value=str(self.skiprows)) # 列范围变量 self.signal_start_var = tk.StringVar(value=self.config.get('SETTINGS', 'signal_start', fallback='A')) self.signal_end_var = tk.StringVar(value=self.config.get('SETTINGS', 'signal_end', fallback='M')) self.data_start_var = tk.StringVar(value=self.config.get('SETTINGS', 'data_start', fallback='BT')) self.data_end_var = tk.StringVar(value=self.config.get('SETTINGS', 'data_end', fallback='CD')) # 报告类型变量 self.text_report_var = tk.BooleanVar(value=self.generate_text_report.get()) self.excel_report_var = tk.BooleanVar(value=self.generate_excel_report.get()) def try_load_columns(self): """尝试在界面初始化后加载列名(如果文件已存在)""" if self.old_file_entry.get() or self.new_file_entry.get(): self.load_columns_from_file() def load_config(self): """加载配置文件""" if os.path.exists(self.config_path): try: self.config.read(self.config_path) except: self.create_default_config() else: self.create_default_config() def create_default_config(self): """创建默认配置""" self.config['SETTINGS'] = { 'skiprows': '9', 'signal_start': 'A', 'signal_end': 'M', 'data_start': 'BT', 'data_end': 'CD', 'generate_text_report': 'True', 'generate_excel_report': 'True', 'col_load_based_on': 'old', 'output_path': '' } self.save_config() def save_config(self): """保存配置到文件""" with open(self.config_path, 'w') as configfile: self.config.write(configfile) def create_interface(self): """创建界面元素 - 使用网格布局""" # 创建行计数器 row = 0 # 文件选择部分 file_frame = ttk.LabelFrame(self.main_frame, text="文件选择") file_frame.grid(row=row, column=0, sticky="ew", padx=5, pady=5) row += 1 # 旧文件选择 ttk.Label(file_frame, text="旧Excel文件:").grid(row=0, column=0, sticky="w", padx=5, pady=5) self.old_file_entry = ttk.Entry(file_frame, width=60) self.old_file_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.old_file_entry.insert(0, self.config.get('SETTINGS', 'old_file', fallback='')) ttk.Button( file_frame, text="浏览...", command=lambda: self.browse_file(self.old_file_entry), width=10 ).grid(row=0, column=2, padx=5, pady=5) # 旧文件Sheet配置 ttk.Label(file_frame, text="Sheet名称或索引:").grid(row=0, column=3, sticky="w", padx=(15, 5), pady=5) self.old_sheet_var = tk.StringVar(value=self.config.get('SETTINGS', 'old_sheet', fallback='')) self.old_sheet_entry = ttk.Entry(file_frame, textvariable=self.old_sheet_var, width=20) self.old_sheet_entry.grid(row=0, column=4, padx=5, pady=5, sticky="w") # 新文件选择 ttk.Label(file_frame, text="新Excel文件:").grid(row=1, column=0, sticky="w", padx=5, pady=5) self.new_file_entry = ttk.Entry(file_frame, width=60) self.new_file_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew") self.new_file_entry.insert(0, self.config.get('SETTINGS', 'new_file', fallback='')) ttk.Button( file_frame, text="浏览...", command=lambda: self.browse_file(self.new_file_entry), width=10 ).grid(row=1, column=2, padx=5, pady=5) # 新文件Sheet配置 ttk.Label(file_frame, text="Sheet名称或索引:").grid(row=1, column=3, sticky="w", padx=(15, 5), pady=5) self.new_sheet_var = tk.StringVar(value=self.config.get('SETTINGS', 'new_sheet', fallback='')) self.new_sheet_entry = ttk.Entry(file_frame, textvariable=self.new_sheet_var, width=20) self.new_sheet_entry.grid(row=1, column=4, padx=5, pady=5, sticky="w") file_frame.columnconfigure(1, weight=1) # 设置按钮 settings_btn = ttk.Button( self.main_frame, text="高级设置", command=self.open_settings_dialog, width=15 ) settings_btn.grid(row=row, column=0, sticky="e", padx=5, pady=5) row += 1 # 列选择部分 cols_frame = ttk.LabelFrame(self.main_frame, text="选择要比较的列 (文件选择后自动加载)") cols_frame.grid(row=row, column=0, sticky="nsew", padx=5, pady=5) # 配置网格权重 self.main_frame.rowconfigure(row, weight=1) self.main_frame.columnconfigure(0, weight=1) cols_frame.columnconfigure(0, weight=1) cols_frame.rowconfigure(0, weight=1) # 创建两个面板容器 paned_window = ttk.PanedWindow(cols_frame, orient=tk.HORIZONTAL) paned_window.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) cols_frame.columnconfigure(0, weight=1) cols_frame.rowconfigure(0, weight=1) # 信号级别列 signal_frame = ttk.LabelFrame(paned_window, text="信号级别列") signal_frame.grid(row=0, column=0, sticky="nsew") # 信号级别列的滚动区域 signal_scroll = ttk.Scrollbar(signal_frame) signal_scroll.pack(side=tk.RIGHT, fill=tk.Y) self.signal_canvas = tk.Canvas(signal_frame, yscrollcommand=signal_scroll.set) self.signal_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) signal_scroll.config(command=self.signal_canvas.yview) self.signal_inner_frame = ttk.Frame(self.signal_canvas) self.signal_canvas.create_window((0, 0), window=self.signal_inner_frame, anchor="nw") self.signal_inner_frame.bind( "<Configure>", lambda e: self.signal_canvas.configure(scrollregion=self.signal_canvas.bbox("all")) ) paned_window.add(signal_frame, weight=1) # 数据级别列 data_frame = ttk.LabelFrame(paned_window, text="数据级别列") data_frame.grid(row=0, column=1, sticky="nsew") # 数据级别列的滚动区域 data_scroll = ttk.Scrollbar(data_frame) data_scroll.pack(side=tk.RIGHT, fill=tk.Y) self.data_canvas = tk.Canvas(data_frame, yscrollcommand=data_scroll.set) self.data_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) data_scroll.config(command=self.data_canvas.yview) self.data_inner_frame = ttk.Frame(self.data_canvas) self.data_canvas.create_window((0, 0), window=self.data_inner_frame, anchor="nw") self.data_inner_frame.bind( "<Configure>", lambda e: self.data_canvas.configure(scrollregion=self.data_canvas.bbox("all")) ) paned_window.add(data_frame, weight=1) # 绑定鼠标滚轮事件 self.signal_canvas.bind("<MouseWheel>", lambda e: self.signal_canvas.yview_scroll(int(-1*(e.delta/120)), "units")) self.data_canvas.bind("<MouseWheel>", lambda e: self.data_canvas.yview_scroll(int(-1*(e.delta/120)), "units")) row += 1 # 输出路径 output_frame = ttk.LabelFrame(self.main_frame, text="输出设置") output_frame.grid(row=row, column=0, sticky="ew", padx=5, pady=5) row += 1 # 输出路径 ttk.Label(output_frame, text="输出路径:").grid(row=0, column=0, sticky="w", padx=5, pady=5) self.output_path_entry = ttk.Entry(output_frame) self.output_path_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.output_path_entry.insert(0, self.config.get('SETTINGS', 'output_path', fallback='')) ttk.Button( output_frame, text="浏览...", command=self.browse_output_path, width=10 ).grid(row=0, column=2, padx=5, pady=5) output_frame.columnconfigure(1, weight=1) # 按钮区域 button_frame = ttk.Frame(self.main_frame) button_frame.grid(row=row, column=0, sticky="ew", padx=5, pady=10) row += 1 # 比较按钮 self.compare_btn = ttk.Button( button_frame, text="开始比较", command=self.start_comparison_thread, width=30, style="Accent.TButton" ) self.compare_btn.pack(pady=10) # 进度条 self.progress = ttk.Progressbar( button_frame, orient=tk.HORIZONTAL, length=400, mode='indeterminate' ) self.progress.pack(pady=5, fill=tk.X, expand=True) self.progress.pack_forget() # 初始隐藏 # 状态栏 self.status_var = tk.StringVar(value="就绪") status_bar = ttk.Label(self.main_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.grid(row=row, column=0, sticky="ew", padx=5, pady=5) row += 1 # 日志区域 log_frame = ttk.LabelFrame(self.main_frame, text="日志输出") log_frame.grid(row=row, column=0, sticky="nsew", padx=5, pady=5) self.main_frame.rowconfigure(row, weight=1) # 日志区域获得额外空间 # 日志文本区域 self.log_text = scrolledtext.ScrolledText( log_frame, wrap=tk.WORD, height=10 ) self.log_text.pack(fill="both", expand=True, padx=5, pady=5) self.log_text.config(state=tk.DISABLED) # 添加标签样式 self.log_text.tag_config("error", foreground="red") self.log_text.tag_config("success", foreground="green") self.log_text.tag_config("warning", foreground="orange") # 添加清除日志按钮 ▼▼▼ 新增代码 ▼▼▼ clear_log_frame = ttk.Frame(log_frame) clear_log_frame.pack(fill=tk.X, padx=5, pady=5) ttk.Button( clear_log_frame, text="清除日志", command=self.clear_log, width=10 ).pack(side=tk.RIGHT, padx=5, pady=2) # ▼▼▼ 新增方法:清除日志 ▼▼▼ def clear_log(self): """清除日志内容""" self.log_text.config(state=tk.NORMAL) self.log_text.delete(1.0, tk.END) self.log_text.config(state=tk.DISABLED) self.log("日志已清除") def open_settings_dialog(self): """打开设置对话框""" dialog = tk.Toplevel(self.root) dialog.title("高级设置") dialog.geometry("550x500") dialog.transient(self.root) dialog.grab_set() # 对话框框架 main_frame = ttk.Frame(dialog, padding=10) main_frame.pack(fill=tk.BOTH, expand=True) # 列名行配置 row_frame = ttk.LabelFrame(main_frame, text="列名行配置") row_frame.pack(fill=tk.X, padx=5, pady=5) ttk.Label(row_frame, text="列名所在行索引 (0表示第一行):").grid(row=0, column=0, sticky="w", padx=5, pady=5) skiprows_entry = ttk.Entry(row_frame, textvariable=self.skiprows_var, width=10) skiprows_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w") # 列范围配置 range_frame = ttk.LabelFrame(main_frame, text="列范围配置 (Excel列字母)") range_frame.pack(fill=tk.X, padx=5, pady=5) # 信号级别列范围 ttk.Label(range_frame, text="信号级别列范围:").grid(row=0, column=0, sticky="w", padx=5, pady=5) signal_start_entry = ttk.Entry(range_frame, textvariable=self.signal_start_var, width=5) signal_start_entry.grid(row=0, column=1, padx=5, pady=5, sticky="w") ttk.Label(range_frame, text="至").grid(row=0, column=2, padx=5, pady=5) signal_end_entry = ttk.Entry(range_frame, textvariable=self.signal_end_var, width=5) signal_end_entry.grid(row=0, column=3, padx=5, pady=5, sticky="w") # 数据级别列范围 ttk.Label(range_frame, text="数据级别列范围:").grid(row=1, column=0, sticky="w", padx=5, pady=5) data_start_entry = ttk.Entry(range_frame, textvariable=self.data_start_var, width=5) data_start_entry.grid(row=1, column=1, padx=5, pady=5, sticky="w") ttk.Label(range_frame, text="至").grid(row=1, column=2, padx=5, pady=5) data_end_entry = ttk.Entry(range_frame, textvariable=self.data_end_var, width=5) data_end_entry.grid(row=1, column=3, padx=5, pady=5, sticky="w") # 列加载基准文件设置 col_based_frame = ttk.LabelFrame(main_frame, text="列加载基准文件") col_based_frame.pack(fill=tk.X, padx=5, pady=5) ttk.Radiobutton( col_based_frame, text="基于旧文件加载列", variable=self.col_load_based_on, value="old" ).pack(anchor=tk.W, padx=10, pady=3) ttk.Radiobutton( col_based_frame, text="基于新文件加载列", variable=self.col_load_based_on, value="new" ).pack(anchor=tk.W, padx=10, pady=3) # 输出设置 output_frame = ttk.LabelFrame(main_frame, text="输出设置") output_frame.pack(fill=tk.X, padx=5, pady=5) # 报告输出 ttk.Checkbutton( output_frame, text="生成文本报告", variable=self.text_report_var ).pack(anchor=tk.W, padx=10, pady=5) ttk.Checkbutton( output_frame, text="生成Excel报告", variable=self.excel_report_var ).pack(anchor=tk.W, padx=10, pady=5) # 应用按钮 btn_frame = ttk.Frame(main_frame) btn_frame.pack(fill=tk.X, pady=10) ttk.Button( btn_frame, text="应用设置", command=lambda: self.apply_settings(dialog), width=15 ).pack(side=tk.RIGHT, padx=5) ttk.Button( btn_frame, text="保存设置", command=self.save_settings, width=15 ).pack(side=tk.RIGHT, padx=5) ttk.Button( btn_frame, text="重置为默认", command=self.reset_settings, width=15 ).pack(side=tk.LEFT, padx=5) def reset_settings(self): """重置设置为默认值""" self.skiprows_var.set('9') self.signal_start_var.set('A') self.signal_end_var.set('M') self.data_start_var.set('BT') self.data_end_var.set('CD') self.text_report_var.set(True) self.excel_report_var.set(True) self.col_load_based_on.set('old') self.log("设置已重置为默认值") def save_settings(self): """保存当前设置""" try: # 更新配置对象 self.config['SETTINGS'] = { 'skiprows': self.skiprows_var.get(), 'signal_start': self.signal_start_var.get(), 'signal_end': self.signal_end_var.get(), 'data_start': self.data_start_var.get(), 'data_end': self.data_end_var.get(), 'generate_text_report': str(self.text_report_var.get()), 'generate_excel_report': str(self.excel_report_var.get()), 'col_load_based_on': self.col_load_based_on.get(), 'old_file': self.old_file_entry.get(), 'new_file': self.new_file_entry.get(), 'old_sheet': self.old_sheet_var.get(), 'new_sheet': self.new_sheet_var.get(), 'output_path': self.output_path_entry.get() } # 保存到文件 self.save_config() self.log("设置已成功保存") except Exception as e: self.log(f"保存设置失败: {str(e)}", error=True) def apply_settings(self, dialog=None): """应用设置并关闭对话框""" try: # 应用列范围设置 self.apply_column_range() # 保存设置 self.save_settings() # 重新加载列 self.load_columns_from_file() # 关闭对话框 if dialog: dialog.destroy() except Exception as e: self.log(f"设置应用错误: {str(e)}", error=True) def browse_file(self, entry_widget): """浏览文件并设置到输入框""" file_path = filedialog.askopenfilename( filetypes=[("Excel文件", "*.xlsx *.xls"), ("所有文件", "*.*")] ) if file_path: entry_widget.delete(0, tk.END) entry_widget.insert(0, file_path) # 文件选择后自动加载列 self.load_columns_from_file() def browse_output_path(self): """浏览输出路径""" dir_path = filedialog.askdirectory() if dir_path: self.output_path_entry.delete(0, tk.END) self.output_path_entry.insert(0, dir_path) def apply_column_range(self): """应用列范围设置(使用Excel字母格式)""" try: # 将字母转换为数字索引 self.signal_start_col = excel_column_letter_to_index(self.signal_start_var.get()) self.signal_end_col = excel_column_letter_to_index(self.signal_end_var.get()) self.data_start_col = excel_column_letter_to_index(self.data_start_var.get()) self.data_end_col = excel_column_letter_to_index(self.data_end_var.get()) self.log(f"列范围设置已应用: 信号列[{self.signal_start_var.get()} ({self.signal_start_col}) - {self.signal_end_var.get()} ({self.signal_end_col})], 数据列[{self.data_start_var.get()} ({self.data_start_col}) - {self.data_end_var.get()} ({self.data_end_col})]") except ValueError: self.log("错误: 请输入有效的列范围索引", error=True) def load_columns_from_file(self): """从文件中加载列名(显示索引位置)并根据范围过滤""" try: # 确定基于哪个文件加载列 if self.col_load_based_on.get() == 'old': file_path = self.old_file_entry.get() sheet_name = self.old_sheet_var.get().strip() else: file_path = self.new_file_entry.get() sheet_name = self.new_sheet_var.get().strip() if not file_path: return skiprows = int(self.skiprows_var.get()) if self.skiprows_var.get().isdigit() else 9 # 如果sheet_name为空,则使用0(第一个sheet) if not sheet_name and sheet_name != "0": sheet_name = 0 else: try: sheet_name = int(sheet_name) except ValueError: pass # 保持为字符串 df = self.safe_read_excel(file_path, skiprows, sheet_name) if df is None: self.log(f"无法加载文件: {file_path}", error=True) return # 清空现有列选择 for widget in self.signal_inner_frame.winfo_children(): widget.destroy() for widget in self.data_inner_frame.winfo_children(): widget.destroy() # 创建列选择复选框 self.signal_col_vars = {} self.data_col_vars = {} # 获取配置的列范围 signal_start = self.signal_start_col signal_end = self.signal_end_col data_start = self.data_start_col data_end = self.data_end_col # 信号级别列 for i, col in enumerate(df.columns): if signal_start <= i <= signal_end: var = tk.BooleanVar(value=True) cb = ttk.Checkbutton( self.signal_inner_frame, text=f"{i}: {col}", variable=var ) cb.pack(anchor=tk.W, padx=5, pady=2) self.signal_col_vars[col] = var # 数据级别列 for i, col in enumerate(df.columns): if data_start <= i <= data_end: var = tk.BooleanVar(value=True) cb = ttk.Checkbutton( self.data_inner_frame, text=f"{i}: {col}", variable=var ) cb.pack(anchor=tk.W, padx=5, pady=2) self.data_col_vars[col] = var self.log(f"列名已从文件加载 ({self.col_load_based_on.get()}文件)") except Exception as e: self.log(f"加载列名失败: {str(e)}", error=True) def start_comparison_thread(self): """启动比较线程""" # 禁用按钮避免重复点击 self.compare_btn.config(state=tk.DISABLED) self.status_var.set("正在比较...") # 显示进度条 self.progress.pack(pady=5, fill=tk.X, expand=True) self.progress.start(10) # 获取输入参数 self.old_file = self.old_file_entry.get() self.new_file = self.new_file_entry.get() self.skiprows = int(self.skiprows_var.get()) if self.skiprows_var.get().isdigit() else 9 # 验证文件是否存在 if not os.path.exists(self.old_file) or not os.path.exists(self.new_file): self.log("错误: 文件不存在", error=True) self.comparison_finished() return # 获取选择的列 self.signal_cols = [col for col, var in self.signal_col_vars.items() if var.get()] self.data_cols = [col for col, var in self.data_col_vars.items() if var.get()] if not self.signal_cols or not self.data_cols: self.log("错误: 请选择要比较的列", error=True) self.comparison_finished() return # 获取新旧文件的Sheet名称 old_sheet = self.old_sheet_var.get().strip() new_sheet = self.new_sheet_var.get().strip() # 如果为空,则使用第一个Sheet if not old_sheet and old_sheet != "0": old_sheet = 0 else: try: old_sheet = int(old_sheet) except: pass # 保持为字符串 if not new_sheet and new_sheet != "0": new_sheet = 0 else: try: new_sheet = int(new_sheet) except: pass # 启动后台线程 threading.Thread(target=self.compare_excel_wrapper, args=(old_sheet, new_sheet), daemon=True).start() def compare_excel_wrapper(self, old_sheet, new_sheet): """比较Excel文件的包装函数""" try: # 执行比较 comparison_result = self.compare_excel(self.old_file, self.new_file, self.skiprows, old_sheet, new_sheet) if comparison_result is not None and not comparison_result.empty: # 保存结果 output_dir = self.output_path_entry.get() or os.getcwd() if not os.path.exists(output_dir): try: os.makedirs(output_dir) self.log(f"创建输出目录: {output_dir}") except PermissionError: # 如果没有权限,使用临时目录 temp_dir = tempfile.mkdtemp() self.log(f"无法创建输出目录,使用临时目录: {temp_dir}", warning=True) output_dir = temp_dir # 生成Excel报告 (改进的保存机制) if self.excel_report_var.get(): # 确保文件名唯一 timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") excel_filename = f"comparison_result_{timestamp}.xlsx" excel_path = os.path.join(output_dir, excel_filename) try: # 尝试保存 comparison_result.to_excel(excel_path, index=False) self.log(f"Excel报告保存到: {excel_path}", success=True) # 尝试打开文件以确保没有锁定 with open(excel_path, 'rb') as test_file: test_file.read(1) except (PermissionError, OSError) as e: # 如果保存失败,尝试使用临时目录 temp_dir = tempfile.mkdtemp() temp_excel_path = os.path.join(temp_dir, excel_filename) comparison_result.to_excel(temp_excel_path, index=False) self.log(f"原始保存失败,文件已保存到临时目录: {temp_excel_path}", warning=True) self.log(f"错误详情: {str(e)}", warning=True) # 生成文本报告 (改进的保存机制) if self.text_report_var.get(): # 确保文件名唯一 text_filename = f"comparison_report_{timestamp}.txt" report_path = os.path.join(output_dir, text_filename) try: report = self.generate_report(comparison_result) with open(report_path, 'w', encoding='utf-8') as f: f.write(report) self.log(f"文本报告保存到: {report_path}", success=True) except (PermissionError, OSError) as e: # 如果保存失败,尝试使用临时目录 temp_dir = tempfile.mkdtemp() temp_report_path = os.path.join(temp_dir, text_filename) with open(temp_report_path, 'w', encoding='utf-8') as f: f.write(report) self.log(f"原始保存失败,文件已保存到临时目录: {temp_report_path}", warning=True) self.log(f"错误详情: {str(e)}", warning=True) self.log(f"比较完成! 发现 {len(comparison_result)} 处变更", success=True) else: self.log("比较完成! 未发现变更", success=True) except Exception as e: self.log(f"比较过程中发生错误: {str(e)}", error=True) import traceback self.log(traceback.format_exc(), error=True) finally: self.comparison_finished() def comparison_finished(self): """比较完成后的清理工作""" self.progress.stop() self.progress.pack_forget() # 隐藏进度条 self.compare_btn.config(state=tk.NORMAL) self.status_var.set("完成") def log(self, message, error=False, success=False): """记录日志消息""" self.log_text.config(state=tk.NORMAL) if error: self.log_text.insert(tk.END, "[错误] ") self.log_text.tag_add("error", "end-1c linestart", "end-1c lineend") elif success: self.log_text.insert(tk.END, "[成功] ") self.log_text.tag_add("success", "end-1c linestart", "end-1c lineend") else: self.log_text.insert(tk.END, "[信息] ") self.log_text.insert(tk.END, message + "\n") self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) # ======================== 以下是核心比较功能 ======================== def safe_read_excel(self, file_path, skiprows, sheet_name=None): """安全的Excel读取函数,包含自动修复功能和Sheet选择""" if sheet_name is None: sheet_name = 0 # 默认为第一个Sheet # 最后尝试:使用二进制模式读取 self.log("二进制读取") try: with open(file_path, 'rb') as f: # 尝试不同的引擎 for engine in ['openpyxl', 'xlrd', 'odf']: try: df = pd.read_excel( f, skiprows=skiprows, dtype=str, engine=engine, sheet_name=sheet_name ).fillna('') self.log(f"成功使用引擎: {engine}", success=True) return df except: continue except Exception as e: self.log(f"二进制读取失败: {str(e)}", error=True) # 终极手段:手动提取数据 self.log("所有方法失败,尝试手动提取数据") try: wb = load_workbook(file_path, read_only=True, data_only=True, keep_vba=False) # 获取指定Sheet if isinstance(sheet_name, int): if sheet_name < len(wb.sheetnames): ws = wb.worksheets[sheet_name] else: ws = wb.active else: if sheet_name in wb.sheetnames: ws = wb[sheet_name] else: ws = wb.active data = [] for i, row in enumerate(ws.iter_rows(values_only=True)): if i < skiprows: continue data.append(row) headers = data[0] if data else [] df = pd.DataFrame(data[1:], columns=headers) return df.fillna('').astype(str) except Exception as e: raise ValueError(f"无法读取文件 {file_path}: {str(e)}") def find_met_columns(self, df, is_old_file=True): """ 查找MET列 - 旧文件:查找包含"MET"的列(V列和W列) - 新文件:查找包含"MET"的列(AE列) """ met_cols = [] # 尝试通过列名匹配 for col in df.columns: if "MET" in col.upper(): met_cols.append(col) # 如果未找到,尝试按位置查找 if not met_cols: self.log(f"警告: 通过列名未找到MET列,尝试按位置查找") if is_old_file: # 旧文件:V列(22)和W列(23),索引21和22 if len(df.columns) > 22: met_cols = [df.columns[21], df.columns[22]] self.log(f"使用位置索引的MET列: {met_cols}") elif len(df.columns) > 21: met_cols = [df.columns[21]] else: # 新文件:AE列(31),索引30 if len(df.columns) > 30: met_cols = [df.columns[30]] self.log(f"使用位置索引的MET列: {met_cols}") if not met_cols: self.log(f"严重警告: 未找到任何MET列,创建虚拟列以避免错误") # 创建虚拟列 df['MET_DUMMY'] = '' met_cols = ['MET_DUMMY'] return met_cols def check_met_status(self, row, met_cols): """ 检查行的MET状态 如果任一MET列包含'T'或'R',则返回True """ for col in met_cols: if col in row: met_value = str(row[col]).strip().upper() if met_value in ['T', 'R']: return True return False def build_signal_hierarchy(self, df, signal_col, data_col): """构建信号-数据的层级结构(修复行位置键问题)""" signals = {} current_signal = None for idx, row in df.iterrows(): # 检查是否是信号行(信号名列非空) row_data = row.to_dict() # 转换为字典以便处理 signal_val = str(row_data.get(signal_col, '')).strip() data_val = str(row_data.get(data_col, '')).strip() # 明确信号行判断:信号列有值且数据列为空 if signal_val and not data_val: # 新信号开始 current_signal = signal_val # 确保初始化所有必需键 signals.setdefault(current_signal, { 'signal_row': row_data, 'data_items': [], '行位置': idx # 记录行位置 }) # 更新行位置 signals[current_signal]['行位置'] = idx # 数据行判断:数据列有值且当前有活跃信号 elif current_signal and data_val: # 确保信号字典存在 if current_signal in signals: # 确保初始化行位置(如果尚未初始化) if '行位置' not in signals[current_signal]: signals[current_signal]['行位置'] = idx # 使用当前行作为默认 self.log(f"警告: 信号 {current_signal} 行位置未初始化,使用默认值 {idx}", warning=True) # 添加数据项并记录其行位置 signals[current_signal]['data_items'].append({ 'row_data': row_data, '行位置': idx # 记录数据项的行位置 }) # 后处理:确保所有信号都有行位置 for signal_name, signal_data in signals.items(): if '行位置' not in signal_data: # 尝试从第一个数据项获取行位置 if signal_data['data_items']: signal_data['行位置'] = signal_data['data_items'][0]['行位置'] self.log(f"警告: 信号 {signal_name} 行位置未设置,使用第一个数据项位置", warning=True) else: signal_data['行位置'] = 0 # 默认值 self.log(f"警告: 信号 {signal_name} 无数据项,行位置设置为0", warning=True) return signals def normalize_col_name(self, col): """标准化列名:移除非单词字符,空格,并转为小写""" return re.sub(r'[^\w]', '', str(col).replace(' ', '').lower()) def compare_excel(self, old_file, new_file, skiprows, old_sheet, new_sheet): # 安全读取文件 self.log(f"正在读取旧文件: {old_file} (Sheet: {old_sheet})") df_old = self.safe_read_excel(old_file, skiprows, old_sheet) self.log(f"正在读取新文件: {new_file} (Sheet: {new_sheet})") df_new = self.safe_read_excel(new_file, skiprows, new_sheet) # === 列名标准化函数 === def normalize_col_name(col): return re.sub(r'[^\w]', '', str(col).replace(' ', '').lower()) # 标准化列名 df_old.columns = [normalize_col_name(col) for col in df_old.columns] df_new.columns = [normalize_col_name(col) for col in df_new.columns] # === 关键列检测 === # 信号列 SIGNAL_COLS = ['フレーム名', 'フレーム名', 'framename', 'signalname'] signal_col = next((col for col in SIGNAL_COLS if col in df_old.columns), df_old.columns[0]) # 数据列 DATA_COLS = ['データ名', 'データ名', 'dataname', 'itemname'] data_col = next((col for col in DATA_COLS if col in df_old.columns), df_old.columns[1]) self.log(f"使用信号列: '{signal_col}'") self.log(f"使用数据: '{data_col}'") # 查找MET列 old_met_cols = self.find_met_columns(df_old, is_old_file=True) new_met_cols = self.find_met_columns(df_new, is_old_file=False) # === 关键修复 1: 标准化用户选择的列 === self.signal_cols = [self.normalize_col_name(col) for col in self.signal_cols] self.data_cols = [self.normalize_col_name(col) for col in self.data_cols] # === 关键修复 2: 改进MET检查 === def check_met_status(row, met_cols): """修正MET状态检查""" for col in met_cols: try: # 直接使用列索引访问,避免get()方法问题 met_value = str(row[col]).strip().upper() if met_value in ['T', 'R', '1', 'Y', 'TRUE']: return True except KeyError: continue return False # === 关键修复 3: 重构层级构建方法 === def build_signal_hierarchy(df, signal_col, data_col): signals = {} current_signal = None for idx, row in df.iterrows(): row_data = row.to_dict() signal_val = str(row_data.get(signal_col, '')).strip() data_val = str(row_data.get(data_col, '')).strip() # 明确信号行判断:信号列有值且数据列为空 if signal_val and not data_val: current_signal = signal_val # 保存行索引 signals[current_signal] = { 'signal_row': row_data, 'data_items': [], '行位置': idx # 新增行位置信息 } # 数据行判断:数据列有值 elif data_val and current_signal: # 保存行索引 signals[current_signal]['data_items'].append({ 'row_data': row_data, '行位置': idx # 新增行位置信息 }) return signals # 构建层级结构 old_signals = self.build_signal_hierarchy(df_old, signal_col, data_col) new_signals = self.build_signal_hierarchy(df_new, signal_col, data_col) # 存储结果 results = [] # 比较信号级别 for signal_name in set(old_signals.keys()) - set(new_signals.keys()): if check_met_status(old_signals[signal_name]['signal_row'], old_met_cols): results.append({ '信号名': signal_name, '变更类型': '信号删除', '数据名': '', '变更详情': "整个信号被删除", '行位置': old_signals[signal_name]['行位置'] # 修复:使用正确的键名 }) for signal_name in set(new_signals.keys()) - set(old_signals.keys()): if check_met_status(new_signals[signal_name]['signal_row'], new_met_cols): results.append({ '信号名': signal_name, '变更类型': '信号新增', '数据名': '', '变更详情': "整个信号被新增", '行位置': new_signals[signal_name]['行位置'] # 修复:使用正确的键名 }) # 比较共有信号 for signal_name in set(old_signals.keys()) & set(new_signals.keys()): old_signal = old_signals[signal_name] new_signal = new_signals[signal_name] # 确保行位置存在 old_row_pos = old_signal.get('行位置', 0) new_row_pos = new_signal.get('行位置', 0) # 信号级别变更 signal_changes = [] for col in self.signal_cols: norm_col = normalize_col_name(col) old_val = str(old_signal['signal_row'].get(norm_col, '')).strip() new_val = str(new_signal['signal_row'].get(norm_col, '')).strip() # 数值类型特殊处理 if old_val.isdigit() and new_val.isdigit(): if int(old_val) != int(new_val): signal_changes.append(f"{col}: {old_val}→{new_val}") elif old_val != new_val: signal_changes.append(f"{col}: {old_val}→{new_val}") if signal_changes and (check_met_status(old_signal['signal_row'], old_met_cols) or check_met_status(new_signal['signal_row'], new_met_cols)): results.append({ '信号名': signal_name, '变更类型': '信号变更', '数据名': '', '变更详情': "; ".join(signal_changes), '行位置': old_row_pos # 使用安全获取的行位置 }) # 数据级别比较 # 创建数据项映射(数据-> (行位置, 行数据)) old_data_map = {} for item in old_signal['data_items']: data_name = str(item['row_data'].get(data_col, '')).strip() if data_name: # 确保数据名不为空 old_data_map[data_name] = (item['行位置'], item['row_data']) new_data_map = {} for item in new_signal['data_items']: data_name = str(item['row_data'].get(data_col, '')).strip() if data_name: # 确保数据名不为空 new_data_map[data_name] = (item['行位置'], item['row_data']) # 检查删除的数据项 for data_name in set(old_data_map.keys()) - set(new_data_map.keys()): row_pos, old_row = old_data_map[data_name] if check_met_status(old_row, old_met_cols): results.append({ '信号名': signal_name, '变更类型': '数据删除', '数据名': data_name, '变更详情': f"数据被删除(满足MET条件)", '行位置': row_pos # 记录行位置 }) # 检查新增的数据项 for data_name in set(new_data_map.keys()) - set(old_data_map.keys()): row_pos, new_row = new_data_map[data_name] if check_met_status(new_row, new_met_cols): results.append({ '信号名': signal_name, '变更类型': '数据新增', '数据名': data_name, '变更详情': f"数据被新增(满足MET条件)", '行位置': row_pos # 记录行位置 }) # 检查变更的数据项 for data_name in set(old_data_map.keys()) & set(new_data_map.keys()): old_row_pos, old_row = old_data_map[data_name] new_row_pos, new_row = new_data_map[data_name] # 初始化 data_changes 列表 data_changes = [] # 收集所有数据列的变更 for col in self.data_cols: try: # 直接比较原始值,不进行strip处理 old_val = str(old_row[col]) new_val = str(new_row[col]) if old_val != new_val: # 记录原始值(包含空格) data_changes.append(f"{col}: [{old_val}]→[{new_val}]") except KeyError: continue # 如果列不存在,跳过 # 检查MET条件:旧行或新行满足MET条件 if data_changes and (check_met_status(old_row, old_met_cols) or check_met_status(new_row, new_met_cols)): results.append({ '信号名': signal_name, '变更类型': '数据变更', '数据名': data_name, '变更详情': "; ".join(data_changes), '行位置': old_row_pos # 使用旧文件中的行位置 }) # 转为结果DataFrame result_df = pd.DataFrame(results) # 如果结果为空,添加一行提示信息 if result_df.empty: result_df = pd.DataFrame([{ '信号名': '无变更', '变更类型': '无变更', '数据名': '', '变更详情': '未发现满足MET条件的变更', '行位置': 0 }]) return result_df def generate_report(self, comparison_result): """生成文本格式的详细报告(按照行位置排序)""" report = [] # 检查是否需要生成报告 if comparison_result.empty or (len(comparison_result) == 1 and comparison_result.iloc[0]['信号名'] == '无变更'): return "比较完成,未发现满足MET条件的变更" # 创建行位置字典(信号名 -> 行索引) signal_line_map = {} # 首先处理信号级别的变更 signal_changes = comparison_result[comparison_result['数据名'] == ''] # 按行位置排序 for _, row in signal_changes.iterrows(): signal_name = row['信号名'] # 为每个信号存储变更信息和行位置 - 修复:包含'行位置'键 if signal_name not in signal_line_map: signal_line_map[signal_name] = { 'signal_changes': [], 'data_changes': [], '行位置': row['行位置'] # 新增行位置存储 } else: signal_line_map[signal_name]['行位置'] = row['行位置'] signal_line_map[signal_name]['signal_changes'].append({ '变更类型': row['变更类型'], '详情': row['变更详情'] }) # 处理数据级别的变更 data_changes = comparison_result[comparison_result['数据名'] != ''] # 创建数据项的位置字典(信号名->数据->行位置) for signal_name, group in data_changes.groupby('信号名'): if signal_name not in signal_line_map: signal_line_map[signal_name] = { 'signal_changes': [], 'data_changes': [], '行位置': group.iloc[0]['行位置'] # 新增行位置存储 } # 对每个信号下的数据变更按行位置排序 sorted_data = group.sort_values(by='行位置') for _, row in sorted_data.iterrows(): signal_line_map[signal_name]['data_changes'].append({ '数据名': row['数据名'], '变更类型': row['变更类型'], '详情': row['变更详情'] }) # 按信号行位置排序 - 添加默认值处理 sorted_signals = sorted(signal_line_map.keys(), key=lambda x: signal_line_map[x].get('行位置', float('inf'))) # 生成报告内容 for signal_name in sorted_signals: signal_info = signal_line_map[signal_name] report.append(f"\n信号: {signal_name}") # 输出信号级别的变更 for change in signal_info['signal_changes']: if change['变更类型'] == '信号删除': report.append(f" - 信号删除: {change['详情']}") elif change['变更类型'] == '信号新增': report.append(f" - 信号新增: {change['详情']}") elif change['变更类型'] == '信号变更': report.append(f" - 信号变更: {change['详情']}") # 输出数据级别的变更(已按行位置排序) for change in signal_info['data_changes']: if change['变更类型'] == '数据删除': report.append(f" - 数据删除: {change['数据名']} - {change['详情']}") elif change['变更类型'] == '数据新增': report.append(f" - 数据新增: {change['数据名']} - {change['详情']}") elif change['变更类型'] == '数据变更': report.append(f" - 数据变更: {change['数据名']} - {change['详情']}") # 添加总结信息 total_changes = len(comparison_result) signal_changes_count = len(signal_changes) data_changes_count = len(data_changes) summary = f"\n\n总结:\n" summary += f"总变更数: {total_changes}\n" summary += f"- 信号级别变更: {signal_changes_count}\n" summary += f"- 数据级别变更: {data_changes_count}" report.append(summary) return "\n".join(report) # ===== 主程序入口 ===== if __name__ == "__main__": root = tk.Tk() app = ExcelComparatorApp(root) root.mainloop()
09-29
D:\Anaconda\python.exe D:\PycharmProjects\PythonProject2\.idea\inspectionProfiles\hb.py D:\PycharmProjects\PythonProject2\.idea\inspectionProfiles\hb.py:29: SyntaxWarning: invalid escape sequence '\T' D:\PycharmProjects\PythonProject2\.idea\inspectionProfiles\hb.py:30: SyntaxWarning: invalid escape sequence '\m' Traceback (most recent call last): File "D:\PycharmProjects\PythonProject2\.idea\inspectionProfiles\hb.py", line 27, in <module> merge_excel_files( File "D:\PycharmProjects\PythonProject2\.idea\inspectionProfiles\hb.py", line 22, in merge_excel_files combined_df.to_excel(output_file, index=False) File "D:\Anaconda\Lib\site-packages\pandas\util\_decorators.py", line 333, in wrapper return func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^ File "D:\Anaconda\Lib\site-packages\pandas\core\generic.py", line 2417, in to_excel formatter.write( File "D:\Anaconda\Lib\site-packages\pandas\io\formats\excel.py", line 943, in write writer = ExcelWriter( ^^^^^^^^^^^^ File "D:\Anaconda\Lib\site-packages\pandas\io\excel\_openpyxl.py", line 61, in __init__ super().__init__( File "D:\Anaconda\Lib\site-packages\pandas\io\excel\_base.py", line 1246, in __init__ self._handles = get_handle( ^^^^^^^^^^^ File "D:\Anaconda\Lib\site-packages\pandas\io\common.py", line 749, in get_handle check_parent_directory(str(handle)) File "D:\Anaconda\Lib\site-packages\pandas\io\common.py", line 616, in check_parent_directory raise OSError(rf"Cannot save file into a non-existent directory: '{parent}'") 这串报错代码帮我解释一下是什么原因,结合上面问你的那串代码
04-03
import tkinter as tk from tkinter import ttk, messagebox, filedialog from PIL import Image, ImageTk, ImageDraw, ImageFont import vlc import os import cv2 import json import time import threading import queue import random import subprocess import sqlite3 from datetime import datetime import pandas as pd import pymysql import asyncio import telnetlib3 import contextlib from threading import Thread, Event from telnet566 import asyncio, telnet_control OUT_DIR = "./output" class EmployeeClockSystem: def __init__(self, root): self.root = root self.root.title("员工工牌识别打卡系统") self.root.geometry("1200x700") self.root.configure(bg="#f0f0f0") self.idle = Event() # 创建事件对象 self.idle.set() # 初始设置为空闲状态(允许采集) self.capture_thread = None self.cap = None # 设置输出目录 self.OUT_DIR = "./output" os.makedirs(self.OUT_DIR, exist_ok=True) # 创建样式 self.style = ttk.Style() self.style.configure("Title.TLabel", font=("微软雅黑", 18, "bold"), foreground="#2c3e50") self.style.configure("Subtitle.TLabel", font=("微软雅黑", 14), foreground="#34495e") self.style.configure("Info.TLabel", font=("微软雅黑", 12), foreground="#2c3e50") self.style.configure("Card.TFrame", background="#ffffff", borderwidth=1, relief="raised", padding=10) self.style.configure("Control.TFrame", background="#e0e0e0", borderwidth=1, relief="sunken", padding=10) # 主布局框架 - 使用PanedWindow实现可调整的分割 main_paned = tk.PanedWindow(root, orient=tk.HORIZONTAL, sashrelief=tk.RAISED, sashwidth=4) main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 左侧视频区域 (50%) left_frame = ttk.Frame(main_paned) main_paned.add(left_frame, stretch="always") # 右侧员工信息区域 (50%) right_frame = ttk.Frame(main_paned) main_paned.add(right_frame, stretch="always") # 视频流标题 ttk.Label(left_frame, text="实时视频监控", style="Title.TLabel").pack(pady=(0, 10), anchor=tk.W, padx=10) # 视频显示区域 video_card = ttk.Frame(left_frame, style="Card.TFrame") video_card.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) self.video_container = ttk.Frame(video_card) self.video_container.pack(fill=tk.BOTH, expand=True) # 视频控制面板 control_frame = ttk.Frame(left_frame, style="Control.TFrame") control_frame.pack(fill=tk.X, padx=10, pady=(0, 10)) # URL输入框 ttk.Label(control_frame, text="RTSP地址:").pack(side=tk.LEFT, padx=(0, 5)) self.url_entry = ttk.Entry(control_frame, width=40) self.url_entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 10)) self.url_entry.insert(0, "rtsp://192.168.1.101/stream1") # 连接按钮 self.connect_button = ttk.Button(control_frame, text="启动监控", command=self.toggle_stream, width=12) self.connect_button.pack(side=tk.LEFT, padx=(0, 5)) # 截图按钮 self.snapshot_button = ttk.Button(control_frame, text="抓拍", command=self.take_snapshot, width=8, state=tk.DISABLED) self.snapshot_button.pack(side=tk.LEFT) # 员工信息标题 ttk.Label(right_frame, text="员工信息识别", style="Title.TLabel").pack(pady=(0, 10), anchor=tk.W, padx=10) # 员工信息卡片 info_card = ttk.Frame(right_frame, style="Card.TFrame") info_card.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) # 员工照片和基本信息 info_frame = ttk.Frame(info_card) info_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # 左侧员工照片区域 avatar_frame = ttk.Frame(info_frame, width=180, height=200) avatar_frame.pack(side=tk.LEFT, padx=(0, 20), fill=tk.Y) self.avatar_label = ttk.Label(avatar_frame) self.avatar_label.pack(fill=tk.BOTH, expand=True) # 默认头像 self.update_avatar() # 右侧员工详细信息 detail_frame = ttk.Frame(info_frame) detail_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) ttk.Label(detail_frame, text="员工基本信息", style="Subtitle.TLabel").pack(anchor=tk.W, pady=(0, 10)) # 信息标签 - 使用Grid布局更精确控制 label_frame = ttk.Frame(detail_frame) label_frame.pack(fill=tk.X, pady=5) ttk.Label(label_frame, text="姓名:", width=8, anchor=tk.E, style="Info.TLabel").grid(row=0, column=0, sticky="e", padx=5, pady=5) self.name_value = ttk.Label(label_frame, text="", width=20, anchor=tk.W, style="Info.TLabel") self.name_value.grid(row=0, column=1, sticky="w", padx=5, pady=5) ttk.Label(label_frame, text="工号:", width=8, anchor=tk.E, style="Info.TLabel").grid(row=1, column=0, sticky="e", padx=5, pady=5) self.id_value = ttk.Label(label_frame, text="", width=20, anchor=tk.W, style="Info.TLabel") self.id_value.grid(row=1, column=1, sticky="w", padx=5, pady=5) ttk.Label(label_frame, text="部门:", width=8, anchor=tk.E, style="Info.TLabel").grid(row=2, column=0, sticky="e", padx=5, pady=5) self.dept_value = ttk.Label(label_frame, text="", width=20, anchor=tk.W, style="Info.TLabel") self.dept_value.grid(row=2, column=1, sticky="w", padx=5, pady=5) # ttk.Label(label_frame, text="打卡状态:", width=8, anchor=tk.E, style="Info.TLabel").grid(row=4, column=0, sticky="e", padx=5, pady=5) # self.status_value = ttk.Label(label_frame, text="未识别", width=20, anchor=tk.W, style="Info.TLabel") # self.status_value.grid(row=4, column=1, sticky="w", padx=5, pady=5) # 打卡按钮 # 打卡按钮 button_frame = ttk.Frame(detail_frame) button_frame.pack(fill=tk.X, pady=10) self.clock_button = ttk.Button(button_frame, text="导出打卡EXCEL", command=self.export_attendance_to_excel, width=15) self.clock_button.pack(side=tk.RIGHT, padx=(0, 20)) self.search_button = ttk.Button(button_frame, text="查询", command=self.open_search_window, width=15) self.search_button.pack(side=tk.RIGHT, padx=(0, 30)) # 考勤记录标题 ttk.Label(right_frame, text="今日考勤记录", style="Title.TLabel").pack(pady=(10, 10), anchor=tk.W, padx=10) # 考勤记录表格 record_card = ttk.Frame(right_frame, style="Card.TFrame") record_card.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0, 10)) # 创建表格 columns = ("time", "id", "name", "dept", "status") self.record_tree = ttk.Treeview(record_card, columns=columns, show="headings", height=8) # 设置列标题 self.record_tree.heading("time", text="时间", anchor=tk.W) self.record_tree.heading("id", text="工号", anchor=tk.W) self.record_tree.heading("name", text="姓名", anchor=tk.W) self.record_tree.heading("dept", text="部门", anchor=tk.W) # 设置列宽 self.record_tree.column("time", width=150, anchor=tk.W) self.record_tree.column("id", width=100, anchor=tk.W) self.record_tree.column("name", width=100, anchor=tk.W) self.record_tree.column("dept", width=120, anchor=tk.W) # 添加滚动条 scrollbar = ttk.Scrollbar(record_card, orient="vertical", command=self.record_tree.yview) self.record_tree.configure(yscrollcommand=scrollbar.set) # 布局 self.record_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 状态栏 self.status_var = tk.StringVar(value="系统就绪") status_bar = ttk.Label(root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) status_bar.pack(side=tk.BOTTOM, fill=tk.X) # 初始化变量 self.stream_active = False self.instance = vlc.Instance("--no-xlib") self.player = self.instance.media_player_new() # 模拟员工数据库 self.employees = { "1001": {"name": "张明", "dept": "技术部", "position": "高级工程师", "avatar": "avatar1.jpg"}, "1002": {"name": "李华", "dept": "市场部", "position": "市场经理", "avatar": "avatar2.jpg"}, "1003": {"name": "王芳", "dept": "财务部", "position": "会计", "avatar": "avatar3.jpg"}, "1004": {"name": "赵刚", "dept": "人力资源", "position": "招聘主管", "avatar": "avatar4.jpg"}, "1005": {"name": "陈晓", "dept": "产品部", "position": "产品经理", "avatar": "avatar5.jpg"}, } # 考勤记录 self.attendance_records = [] self.add_sample_records() # 初始化线程队列 self.gui_queue = queue.Queue() self.root.after(100, self.process_queue) def process_queue(self): """处理队列中的GUI更新任务""" while not self.gui_queue.empty(): try: task = self.gui_queue.get_nowait() if task["type"] == "update_employee_info": self.name_value.config(text=task["name"]) self.id_value.config(text=task["id"]) self.dept_value.config(text=task["dept"]) self.position_value.config(text=task["position"]) self.status_value.config(text=task["status"]) self.show_employee_avatar(task["avatar"]) self.clock_button.config(state=tk.NORMAL) elif task["type"] == "update_status": self.status_var.set(task["message"]) elif task["type"] == "clock_in": self.clock_in_task(task["emp_id"], task["emp_name"], task["emp_dept"]) except queue.Empty: pass self.root.after(100, self.process_queue) def update_avatar(self, image_path=None): if image_path and os.path.exists(image_path): try: # 尝试加载图片 img = Image.open(image_path) # 调整图片大小以适应显示区域 img = self.resize_image(img, target_width=180, target_height=200) # 转换为Tkinter PhotoImage photo = ImageTk.PhotoImage(img) # 更新显示 self.avatar_label.config(image=photo) self.avatar_label.image = photo # 保持引用 print(f"成功加载头像: {image_path}") return True except Exception as e: print(f"加载图片失败: {e}") # 加载失败时显示默认头像 self.show_default_avatar(error=True) return False else: # 没有提供图片路径或路径无效 self.show_default_avatar() return False def show_default_avatar(self): """显示默认头像""" default_img = Image.new('RGB', (180, 200), color='#3498db') draw = ImageDraw.Draw(default_img) try: font = ImageFont.truetype("arial.ttf", 24) except: font = ImageFont.load_default() # draw.text((40, 85), "无数据", fill="white", font=font) default_photo = ImageTk.PhotoImage(default_img) self.avatar_label.config(image=default_photo) self.avatar_label.image = default_photo def resize_image(self, img, target_width, target_height): # """调整图片大小,保持宽高比并填充背景""" # 计算缩放比例 width, height = img.size scale = min(target_width / width, target_height / height) # 计算新尺寸 new_width = int(width * scale) new_height = int(height * scale) # 调整图片大小 img = img.resize((new_width, new_height), Image.LANCZOS) # 创建新图片并粘贴调整后的图片到中心 new_img = Image.new('RGB', (target_width, target_height), color='white') position = ( (target_width - new_width) // 2, (target_height - new_height) // 2 ) new_img.paste(img, position) return new_img def open_search_window(self): # """打开查找员工记录的弹窗""" search_window = tk.Toplevel(self.root) search_window.title("查找员工考勤记录") search_window.geometry("600x400") search_window.resizable(False, False) # 输入框 input_frame = ttk.Frame(search_window) input_frame.pack(pady=10, padx=10, fill=tk.X) ttk.Label(input_frame, text="姓名或工号:", width=12, anchor=tk.E).pack(side=tk.LEFT) search_entry = ttk.Entry(input_frame, width=30) search_entry.pack(side=tk.LEFT, padx=5) # 查询按钮 search_btn = ttk.Button(input_frame, text="查询", command=lambda: self.perform_search(search_entry, tree)) search_btn.pack(side=tk.LEFT) # 表格 tree_frame = ttk.Frame(search_window) tree_frame.pack(padx=10, pady=5, fill=tk.BOTH, expand=True) tree = ttk.Treeview(tree_frame, columns=("工号", "姓名", "部门","时间", "次数"), show="headings", height=15) tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 设置列标题 for col in tree["columns"]: tree.heading(col, text=col) tree.column(col, width=100, anchor=tk.CENTER) # 滚动条 scrollbar = ttk.Scrollbar(tree_frame, orient="vertical", command=tree.yview) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) tree.configure(yscrollcommand=scrollbar.set) # 提示信息 self.search_status_label = ttk.Label(search_window, text="", foreground="red") self.search_status_label.pack(pady=5) # 保存状态 self.current_search_tree = tree def perform_search(self, entry_widget, tree_widget): keyword = entry_widget.get().strip() if not keyword: self.search_status_label.config(text="请输入姓名或工号") return try: conn = pymysql.connect( host='localhost', user='root', password='139800', database='employeeinfomation' ) cursor = conn.cursor() query = """ SELECT * FROM workcard WHERE NAME LIKE %s """ cursor.execute(query, (f"%{keyword}%")) results = cursor.fetchall() print(results) # 清空表格 for item in tree_widget.get_children(): tree_widget.delete(item) # 插入查询结果 for record in results: tree_widget.insert("", tk.END, values=record) if not results: self.search_status_label.config(text=f"未找到包含“{keyword}”的记录") else: self.search_status_label.config(text=f"找到 {len(results)} 条记录") conn.close() except Exception as e: self.search_status_label.config(text=f"查询失败:{str(e)}") def add_sample_records(self): records = [ ("09:00:24", "18084232", "陈嘉睿", "消费产品研发处"), ("09:01:35", "18084380", "陈欣怡", "消费产品研发处"), ("09:05:47", "18083687", "赵雪荣", "消费产品研发处"), ("12:01:15", "18098777", "杨思成", "消费产品研发处"), ] for record in records: self.attendance_records.append(record) self.record_tree.insert("", tk.END, values=record) def add_new_records(self, record): if (record.get('id') and record.get('name') and record.get('dept')): new_record = ( datetime.now().strftime("%H:%M:%S"), # 当前时间 record.get('id'), # 员工ID record.get('name'), # 姓名 record.get('dept'), # 部门 ) self.attendance_records.append(new_record) self.record_tree.insert("", tk.END, values=new_record) else: # 可选:添加错误处理(如日志记录或弹窗提示) print("错误:缺少必要的字段信息,记录未添加") def export_attendance_to_excel(self): # """从本地数据库导出考勤记录到Excel文件""" # 弹出文件保存路径 file_path = filedialog.asksaveasfilename( defaultextension=".xlsx", filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")], title="保存考勤记录为Excel" ) if not file_path: return # 用户取消了保存 try: conn = pymysql.connect( host='localhost', user='root', password='139800', database='employeeinfomation' ) # 2. 使用 pandas 直接读取 SQL 查询结果 query = "SELECT * FROM workcard" df = pd.read_sql_query(query, conn) # 3. 关闭数据库连接 conn.close() # 4. 如果没有数据 if df.empty: messagebox.showwarning("导出失败", "数据库中没有可导出的考勤记录!") return # 5. 导出为 Excel 文件 df.to_excel(file_path, index=False) messagebox.showinfo("导出成功", f"考勤记录已成功导出到:\n{file_path}") except Exception as e: messagebox.showerror("导出失败", f"导出考勤记录时发生错误:\n{str(e)}") def search_count(self, name): conn = pymysql.connect( host='localhost', user='root', password='139800', database='employeeinfomation' ) # 2. 使用 pandas 直接读取 SQL 查询结果 cursor = conn.cursor() query = "SELECT COUNT FROM workcard WHERE NAME LIKE %s " cursor.execute(query, (f"%{name}%")) count = cursor.fetchall() # 3. 关闭数据库连接 conn.close() return count[0][0] def toggle_stream(self): """切换视频流状态""" if self.stream_active: self.stop_stream() else: self.start_stream() def start_stream(self): """启动视频流""" url = self.url_entry.get().strip() if not url: messagebox.showerror("错误", "请输入有效的视频流URL") return try: media = self.instance.media_new(url) self.player.set_media(media) win_id = self.video_container.winfo_id() if os.name == 'nt': win_id = int(win_id) self.player.set_hwnd(win_id) else: self.player.set_xwindow(win_id) self.player.play() self.stream_active = True self.connect_button.config(text="停止监控") self.snapshot_button.config(state=tk.NORMAL) self.status_var.set(f"正在播放: {url}") # 启动视频流线程 threading.Thread(target=self.video_thread, daemon=True).start() # 启动识别线程 # threading.Thread(target=self.recognition_thread, daemon=True).start() # 启动采集线程 self.capture_thread = Thread(target=self.capture_loop, daemon=True) self.capture_thread.start() except Exception as e: messagebox.showerror("连接错误", f"无法连接到视频流: {str(e)}") self.status_var.set("连接失败") def video_thread(self): """视频流播放线程""" pass # 视频流由VLC内部处理,无需额外操作 def capture_loop(self): # """视频采集主循环,使用事件控制采集频率""" self.cap = cv2.VideoCapture(self.url_entry.get().strip()) if not self.cap.isOpened(): print("[错误] 无法打开视频流") return # 加载预训练的人脸检测模型 face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') while self.stream_active: # 等待空闲状态(允许采集) self.idle.wait() # time.sleep(5) # 读取一帧 ret, frame = self.cap.read() if not ret: print("[调试信息] 无法读取视频帧") time.sleep(0.1) continue # 转为灰度图,提高检测效率 gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 检测人脸 faces = face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30) ) if len(faces) > 0: print(f"[调试信息] 检测到 {len(faces)} 张人脸") # 立即设置为忙碌状态(禁止进一步采集) self.idle.clear() # 启动处理线程 Thread( target=self.process_frame, args=(frame.copy(),), # 复制帧以避免后续修改 daemon=True ).start() else: # 未检测到人脸时短暂休眠,控制抽帧频率 time.sleep(0.05) def process_frame(self, frame): # """处理检测到人脸的帧""" try: # 保存当前帧 temp_image_path = "temp_frame.jpg" cv2.imwrite(temp_image_path, frame) print(f"[调试信息] 图像已保存至 {temp_image_path}") # 调用main.py进行图像识别 print(f"[调试信息] 正在调用 main.py 处理 {temp_image_path}") subprocess.run(["python", "main.py", temp_image_path], check=True) print("[调试信息] main.py 执行完成") # 读取main.py输出的JSON文件 stem = os.path.splitext(os.path.basename(temp_image_path))[0] final_json = os.path.join(OUT_DIR, f"{stem}_face_result.json") final_picture = os.path.join(OUT_DIR, f"{stem}_face.jpg") if os.path.exists(final_json): print(f"[调试信息] JSON文件已找到: {final_json}") with open(final_json, "r", encoding="utf-8") as f: result = json.load(f) ocr_info = result.get("OCR", {}) emp_id = ocr_info.get("id") emp_name = ocr_info.get("name") emp_dept = ocr_info.get("department") # 打印识别结果 print(f"[调试信息] 识别结果: ID={emp_id}, 姓名={emp_name}, 部门={emp_dept}") # 更新GUI img = Image.open(final_picture) self.update_avatar(final_picture) # 将识别结果发送到GUI队列 task = { "type": "update_employee_info", "name": emp_name, "id": emp_id, "dept": emp_dept, "position": "未知", "avatar": final_picture } print("************",task,"*************") self.gui_queue.put(task) # 检查打卡记录 count = self.search_count(emp_name) # asyncio.run(telnet_control(play_success=True)) print("********",count,"**********") if (count > 1): messagebox.showinfo("警告", f"{emp_name} 重复打卡!") else: self.add_new_records(task) asyncio.run(telnet_control(play_success=True)) messagebox.showinfo("打卡成功", f"{emp_name} 已成功打卡!") else: print(f"[错误] 未找到JSON文件: {final_json}") # 处理失败时也恢复采集 except subprocess.CalledProcessError as e: print(f"[错误] main.py 执行失败: {e}") except Exception as e: print(f"[错误] 处理过程中发生异常: {str(e)}") finally: # 无论处理成功与否,都恢复空闲状态 self.idle.set() def stop(self): # """停止采集和处理""" self.stream_active = False self.idle.set() # 确保线程可以退出 if self.cap and self.cap.isOpened(): self.cap.release() def stop_stream(self): """停止视频流""" if self.player: self.player.stop() self.stream_active = False self.connect_button.config(text="启动监控") self.snapshot_button.config(state=tk.DISABLED) self.clock_button.config(state=tk.DISABLED) self.status_var.set("已停止视频流") # 清空员工信息 self.name_value.config(text="") self.id_value.config(text="") self.dept_value.config(text="") self.show_default_avatar() def take_snapshot(self): """抓拍当前帧""" if self.stream_active: timestamp = time.strftime("%Y%m%d_%H%M%S") filename = f"snapshot_{timestamp}.png" self.player.video_take_snapshot(0, filename, 0, 0) messagebox.showinfo("抓拍成功", f"已保存截图: {filename}") self.status_var.set(f"截图已保存: {filename}") def show_employee_avatar(self, avatar_path): """显示员工头像""" try: colors = ["#3498db", "#2ecc71", "#e74c3c", "#f39c12", "#9b59b6"] color = random.choice(colors) img = Image.new('RGB', (180, 200), color=color) draw = ImageDraw.Draw(img) try: font = ImageFont.truetype("arial.ttf", 20) except: font = ImageFont.load_default() draw.text((40, 85), "员工照片", fill="white", font=font) photo = ImageTk.PhotoImage(img) self.avatar_label.config(image=photo) self.avatar_label.image = photo except Exception as e: print(f"头像加载错误: {e}") def clock_in(self): """员工打卡""" emp_id = self.id_value.cget("text") emp_name = self.name_value.cget("text") emp_dept = self.dept_value.cget("text") task = {"type": "clock_in", "emp_id": emp_id, "emp_name": emp_name, "emp_dept": emp_dept} self.gui_queue.put(task) def clock_in_task(self, emp_id, emp_name, emp_dept): """执行打卡逻辑""" current_time = time.strftime("%H:%M:%S") hour = int(time.strftime("%H")) minute = int(time.strftime("%M")) status = "迟到" if (hour > 9 or (hour == 9 and minute > 0)) else "正常" record = (current_time, emp_id, emp_name, emp_dept, status) self.attendance_records.append(record) self.record_tree.insert("", tk.END, values=record) self.status_value.config(text=f"已打卡 ({status})") self.status_var.set(f"{emp_name} 打卡成功! 时间: {current_time}") self.clock_button.config(state=tk.DISABLED) self.record_tree.see(self.record_tree.get_children()[-1]) if __name__ == "__main__": root = tk.Tk() app = EmployeeClockSystem(root) root.mainloop()为什么一直在进行截图,没有进入main处理
08-26
import os import tempfile import pythoncom import win32com.client import threading import shutil import tkinter as tk from tkinter import filedialog, ttk, messagebox, scrolledtext from docx import Document from PyPDF2 import PdfMerger, PdfReader, PdfWriter from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import letter from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.lib.colors import red, black, white from reportlab.platypus import Table, TableStyle from io import BytesIO from datetime import datetime import wx from collections import defaultdict class PDFConverterApp: def __init__(self, root): self.root = root self.root.title("audio_data") self.root.geometry("800x650") # 增加窗口高度以容纳新控件 self.folders = [] self.log_messages = [] self.output_path = "" # 存储自定义输出路径 self.backup_mode = tk.BooleanVar(value=True) # 添加备份模式开关 self.point_22_mode = tk.BooleanVar(value=False) # 新增22号点位开关 self.rename_mode = tk.BooleanVar(value=True) # +++ 新增重命名开关 +++ self.output_filename = tk.StringVar(value="听筒磁干扰_Simulation_Result") # 默认文件名 self.create_widgets() def create_widgets(self): # 创建顶部框架 top_frame = ttk.Framae(self.root, padding=10) top_frame.pack(fill=tk.X) output_frame = ttk.LabelFrame(self.root, text="输出设置", padding=10) output_frame.pack(fill=tk.X, padx=10, pady=(0, 5)) # 文件名输入框 ttk.Label(output_frame, text="文件名:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5)) filename_entry = ttk.Entry(output_frame, textvariable=self.output_filename, width=30) filename_entry.grid(row=0, column=1, sticky=tk.W, padx=5) # 输出路径选择 ttk.Label(output_frame, text="输出路径:").grid(row=0, column=2, sticky=tk.W, padx=(20, 5)) self.path_entry = ttk.Entry(output_frame, width=40, state='readonly') self.path_entry.grid(row=0, column=3, sticky=tk.EW, padx=5) browse_btn = ttk.Button(output_frame, text="浏览...", command=self.choose_output_path) browse_btn.grid(row=0, column=4, padx=(5, 0)) # 设置网格列权重 output_frame.columnconfigure(3, weight=1) # +++ 创建复选框容器框架 +++ check_frame = ttk.Frame(output_frame) check_frame.grid(row=1, column=0, columnspan=5, sticky=tk.W, padx=0, pady=5) # 22号点位复选框 self.point_22_check = ttk.Checkbutton( check_frame, text="2号点位", variable=self.point_22_mode ) self.point_22_check.pack(side=tk.LEFT, padx=(0, 15)) # 报告存档复选框 self.backup_check = ttk.Checkbutton( check_frame, text="报告存档", variable=self.backup_mode ) self.backup_check.pack(side=tk.LEFT, padx=(0, 15)) # 启用重命名复选框 self.rename_check = ttk.Checkbutton( check_frame, text="启用重命名", variable=self.rename_mode ) self.rename_check.pack(side=tk.LEFT) # 添加文件夹按钮 add_btn = ttk.Button(top_frame, text="添加文件夹", command=self.add_folder) add_btn.pack(side=tk.LEFT, padx=5) # 移除文件夹按钮 remove_btn = ttk.Button(top_frame, text="移除选中", command=self.remove_selected) remove_btn.pack(side=tk.LEFT, padx=5) # 清空列表按钮 clear_btn = ttk.Button(top_frame, text="清空列表", command=self.clear_list) clear_btn.pack(side=tk.LEFT, padx=5) # 处理按钮 process_btn = ttk.Button(top_frame, text="开始处理", command=self.start_processing) process_btn.pack(side=tk.RIGHT, padx=5) # 创建文件夹列表 list_frame = ttk.LabelFrame(self.root, text="待处理文件夹", padding=10) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # 滚动条 scrollbar = ttk.Scrollbar(list_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # 文件夹列表 self.folder_list = tk.Listbox( list_frame, selectmode=tk.EXTENDED, yscrollcommand=scrollbar.set, height=10 ) self.folder_list.pack(fill=tk.BOTH, expand=True) scrollbar.config(command=self.folder_list.yview) # 创建日志区域 log_frame = ttk.LabelFrame(self.root, text="处理日志", padding=10) log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) # 日志文本框 self.log_text = scrolledtext.ScrolledText( log_frame, wrap=tk.WORD, state=tk.DISABLED ) self.log_text.pack(fill=tk.BOTH, expand=True) # 进度条 self.progress = ttk.Progressbar( self.root, orient=tk.HORIZONTAL, mode='determinate' ) self.progress.pack(fill=tk.X, padx=10, pady=5) def choose_output_path(self): """选择输出文件夹""" path = filedialog.askdirectory(title="选择输出文件夹") if path: self.output_path = path self.path_entry.config(state='normal') self.path_entry.delete(0, tk.END) self.path_entry.insert(0, path) self.path_entry.config(state='readonly') self.log(f"已设置输出路径: {path}") # +++ 新增方法:应用重命名规则 +++ def apply_rename_rule(self, original_files): """应用重命名规则:使用文件夹名称作为文件名(删除最后一个横杠及其后内容)""" renamed_files = [] folder_file_counts = defaultdict(int) # 记录每个文件夹下的文件数量 for file_path in original_files: # 获取文件所在文件夹的路径和名称 folder_path = os.path.dirname(file_path) folder_name = os.path.basename(folder_path) # +++ 新增处理逻辑:删除最后一个横杠及其后内容 +++ if '-' in folder_name: # 找到最后一个横杠的位置 dash_index = folder_name.rfind('-') # 删除横杠及其后所有字符 folder_name = folder_name[:dash_index] self.log(f"已处理文件夹名: {os.path.basename(folder_path)} → {folder_name}") # 获取文件扩展名 _, ext = os.path.splitext(file_path) # 更新文件夹文件计数器 file_count = folder_file_counts[folder_path] + 1 folder_file_counts[folder_path] = file_count # 生成新文件名:文件夹名称 + 序号(如果多于一个文件) if file_count == 1: new_name = f"{folder_name}{ext}" # 单文件不加序号 else: new_name = f"{folder_name}_{file_count}{ext}" # 多文件加序号 new_path = os.path.join(folder_path, new_name) # 确保新文件名不冲突 counter = 1 while os.path.exists(new_path): # 如果文件名冲突,添加后缀序号 new_name = f"{folder_name}_{file_count}_{counter}{ext}" new_path = os.path.join(folder_path, new_name) counter += 1 # 执行重命名 try: os.rename(file_path, new_path) self.log(f"已将 '{os.path.basename(file_path)}' 重命名为 '{new_name}'") renamed_files.append(new_path) except Exception as e: self.log(f"重命名失败: {str(e)}") renamed_files.append(file_path) # 保留原文件路径 return renamed_files def add_folder(self): """添加要处理的文件夹""" folders = filedialog.askdirectory( title="选择要处理的文件夹", mustexist=True ) if folders: self.folders.append(folders) self.folder_list.insert(tk.END, folders) self.log(f"已添加文件夹: {folders}") def remove_selected(self): """移除选中的文件夹""" selected = self.folder_list.curselection() for index in selected[::-1]: folder = self.folder_list.get(index) self.folder_list.delete(index) self.folders.remove(folder) self.log(f"已移除文件夹: {folder}") def clear_list(self): """清空文件夹列表""" self.folder_list.delete(0, tk.END) self.folders = [] self.log("已清空文件夹列表") def log(self, message): """向日志区域添加消息""" timestamp = datetime.now().strftime("%H:%M:%S") log_entry = f"[{timestamp}] {message}" self.log_messages.append(log_entry) self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, log_entry + "\n") self.log_text.config(state=tk.DISABLED) self.log_text.yview(tk.END) # 自动滚动到底部 self.root.update_idletasks() def start_processing(self): """启动处理过程""" if not self.folders: messagebox.showwarning("警告", "请先添加要处理的文件夹") return # 禁用处理按钮 self.root.title("Word 转 PDF 合并工具 - 处理中...") self.progress["value"] = 0 # 在新线程中处理,避免界面冻结 thread = threading.Thread(target=self.process_folders) thread.daemon = True thread.start() # +++ 修改方法:备份时获取校准数据 +++ def backup_data_files(self, folder_path, backup_dir): """递归查找并备份所有.xlsx和.csv文件并返回校准数据""" self.log(f"开始在文件夹中搜索所有Excel和CSV文件: {folder_path}") backup_count = 0 calibration_data = {'j2': None, 'j3': None} # 存储校准数据 for root, dirs, files in os.walk(folder_path): for file in files: if file.lower().endswith(('.xlsx', '.csv')): file_path = os.path.join(root, file) file_name = os.path.basename(file_path) dest_path = os.path.join(backup_dir, file_name) try: os.makedirs(backup_dir, exist_ok=True) shutil.copy2(file_path, dest_path) backup_count += 1 self.log(f"备份成功: {file_path} → {dest_path}") except Exception as e: self.log(f"备份失败 {file_path}: {str(e)}") self.log(f"共找到并备份 {backup_count} 个Excel和CSV文件") def process_folders(self): """处理多个文件夹中的Word文件""" try: # 提前初始化 output_folder if self.output_path: output_folder = self.output_path else: output_folder = next((p for p in self.folders if os.path.isdir(p)), os.getcwd()) self.log(f"开始处理 {len(self.folders)} 个文件夹...") # 获取所有文件夹中的Word文件 word_files = self.get_all_word_files(self.folders) if not word_files: self.log("没有找到任何Word文档") return # +++ 应用重命名规则 +++ if self.rename_mode.get(): self.log("应用重命名规则...") word_files = self.apply_rename_rule(word_files) self.log(f"共找到 {len(word_files)} 个Word文档") self.progress["maximum"] = len(word_files) + 5 # 文件数 + 合并步骤 backup_root = os.path.join(output_folder, "报告存档") # 统一备份根目录 if self.backup_mode.get(): os.makedirs(backup_root, exist_ok=True) # 创建临时目录存储转换后的PDF with tempfile.TemporaryDirectory() as temp_dir: pdf_files_with_header = [] toc_entries = [] all_tables = {} current_page = 1 # 处理每个Word文件 for i, word_file in enumerate(word_files): self.progress["value"] = i + 1 file_name = os.path.splitext(os.path.basename(word_file))[0] display_name = file_name # 修改Word文档逻辑 modified_word_path = word_file if self.point_22_mode.get() or "GSM" in file_name.upper(): # 创建临时副本进行修改 temp_word_path = os.path.join(temp_dir, os.path.basename(word_file)) shutil.copy2(word_file, temp_word_path) if self.modify_word_spec(temp_word_path): modified_word_path = temp_word_path original_pdf = os.path.join(temp_dir, f"{file_name}_original.pdf") pdf_with_header = os.path.join(temp_dir, f"{file_name}_with_header.pdf") if self.backup_mode.get(): try: # 为每个Word文件创建备份目录 dest_dir = os.path.join(backup_root, file_name) os.makedirs(dest_dir, exist_ok=True) # 备份Word文件 word_dest = os.path.join(dest_dir, os.path.basename(modified_word_path)) shutil.copy2(modified_word_path, word_dest) self.log(f"Word文件备份成功: {word_file} → {word_dest}") # +++ 备份数据文件并获取校准数据 +++ folder_path = os.path.dirname(word_file) except OSError as e: self.log(f"文件备份失败: {e}") except Exception as e: self.log(f"未知错误: {e}") # 提取表格数据 tables = self.extract_spec_table(modified_word_path) if tables: all_tables[display_name] = tables self.log(f"已从 {display_name} 中提取 {len(tables)} 个数据表格") # 转换为PDF if self.word_to_pdf(modified_word_path, original_pdf): # 添加内联标题 if self.add_inline_header(original_pdf, display_name, pdf_with_header): pdf_files_with_header.append(pdf_with_header) toc_entries.append((display_name, current_page)) current_page += self.get_pdf_page_count(pdf_with_header) else: pdf_files_with_header.append(original_pdf) toc_entries.append((display_name, current_page)) current_page += self.get_pdf_page_count(original_pdf) else: self.log(f"跳过 {display_name},转换失败") # 更新进度条 self.progress["value"] = len(word_files) + 1 if not pdf_files_with_header: self.log("没有成功转换的PDF文件,无法进行合并") return # 获取输出路径 if self.output_path: output_folder = self.output_path else: output_folder = next((p for p in self.folders if os.path.isdir(p)), os.getcwd()) # 获取文件名 report_name = self.output_filename.get().strip() if not report_name: report_name = self.get_folder_name_parts(self.folders[0]) # 使用默认规则 output_pdf = os.path.join(output_folder, f"{report_name}.pdf") # 合并PDF success = self.merge_pdfs_with_summary( pdf_files_with_header, toc_entries, all_tables, output_pdf ) self.progress["value"] = len(word_files) + 3 if success: self.log(f"处理完成!输出文件: {output_pdf}") messagebox.showinfo("完成", f"处理完成!输出文件: {output_pdf}") else: self.log("处理失败") messagebox.showerror("错误", "处理过程中出现错误") self.root.title("Word 转 PDF 合并工具") except Exception as e: self.log(f"处理过程中出现错误: {str(e)}") messagebox.showerror("错误", f"处理过程中出现错误: {str(e)}") self.root.title("Word 转 PDF 合并工具") # 以下是原有的处理函数,保持不变但添加为类方法 def extract_spec_table(self, word_path): """从Word文档中提取SPEC(dB)、Simulation和Pass/Fail数据表格""" try: doc = Document(word_path) tables = [] for table in doc.tables: headers = [cell.text.strip() for cell in table.rows[0].cells] if "SPEC(dB)" in headers and "Simulation" in headers and "Pass/Fail" in headers: table_data = [] table_data.append(headers) for row in table.rows[1:]: row_data = [cell.text.strip() for cell in row.cells] table_data.append(row_data) tables.append(table_data) return tables except Exception as e: self.log(f"提取 {os.path.basename(word_path)} 中的表格时出错: {str(e)}") return [] def modify_word_spec(self, word_path): try: doc = Document(word_path) filename = os.path.basename(word_path).upper() has_gsm = "GSM" in filename # 移动到try块内部 # 确定SPEC基准值 if self.point_22_mode.get(): # 默认22号点位 spec_value = 20 if has_gsm else 18 else: # 2号点位未启用 spec_value = 22 if has_gsm else 20 modified = False # 初始化修改标志 # 遍历文档所有表格 for table in doc.tables: headers = [cell.text.strip() for cell in table.rows[0].cells] try: spec_index = headers.index("SPEC(dB)") # 定位SPEC列 sim_index = headers.index("Simulation") # 定位Simulation列 pf_index = headers.index("Pass/Fail") # 定位Pass/Fail列 except ValueError: continue # 跳过不含目标列的表 # 标记已找到可修改表格 modified = True # 修改每行数据 for row in table.rows[1:]: cells = row.cells # 更新SPEC值 if spec_index < len(cells): cells[spec_index].text = str(spec_value) # 更新Pass/Fail状态 if sim_index < len(cells) and pf_index < len(cells): try: sim_value = float(cells[sim_index].text) new_status = "PASS" if sim_value < spec_value else "FAIL" cells[pf_index].text = new_status except ValueError: pass # 忽略格式错误 # 保存修改后的文档 if modified: doc.save(word_path) self.log(f"已修改 {os.path.basename(word_path)} 的SPEC值为{spec_value}") return modified except Exception as e: self.log(f"修改 {os.path.basename(word_path)} 失败: {str(e)}") return False def add_inline_header(self, pdf_path, title, output_path): """在PDF的第一页顶部添加一行红色加粗的标题""" try: reader = PdfReader(pdf_path) writer = PdfWriter() if len(reader.pages) > 0: first_page = reader.pages[0] packet = BytesIO() can = canvas.Canvas(packet, pagesize=letter) width, height = letter font_name = "Helvetica-Bold" try: pdfmetrics.registerFont(TTFont('SimSun', 'simsun.ttc')) pdfmetrics.registerFont(TTFont('SimSun-Bold', 'simsun.ttc')) font_name = "SimSun-Bold" except: pass can.setFont(font_name, 14) can.setFillColor(red) can.drawString(50, height - 50, title) can.save() packet.seek(0) title_reader = PdfReader(packet) title_page = title_reader.pages[0] first_page.merge_page(title_page) writer.add_page(first_page) for page in reader.pages[1:]: writer.add_page(page) with open(output_path, "wb") as f: writer.write(f) return True return False except Exception as e: self.log(f"PDF添加标题失败: {str(e)}") return False # +++ 修改方法:创建Summary页(核心修改) +++ def create_summary_page(self, toc_entries, all_tables, output_path): """创建包含三列数据的Summary页(无Calibration列)""" try: c = canvas.Canvas(output_path, pagesize=letter) width, height = letter font_name = "Helvetica" try: pdfmetrics.registerFont(TTFont('SimSun', 'simsun.ttc')) font_name = "SimSun" except: pass # Summary标题 c.setFont(font_name, 24) c.setFillColor(red) c.drawCentredString(width / 2.0, height - 50, "Summary") c.setFillColor(black) y_position = height - 100 # 添加数据汇总表格 if all_tables: c.setFont(font_name, 16) c.drawString(50, y_position, "Data Summary:") y_position -= 30 c.setFont(font_name, 10) table_width = width - 100 for doc_name, tables in all_tables.items(): c.setFont(font_name, 12) c.setFillColor(red) c.drawString(60, y_position, f"Document: {doc_name}") y_position -= 20 c.setFillColor(black) c.setFont(font_name, 10) # 处理每个表格 for table_data in tables: # 确保表格有数据行 if len(table_data) < 2: # 至少包含表头+1行数据 continue # 表头格式(三列) headers = ["SPEC(dB)", "Simulation", "Pass/Fail"] # 提取第一行原始数据(跳过表头) data_row = table_data[1] if len(table_data) > 1 else ["N/A"] * 3 # 确保数据行有足够列 while len(data_row) < 3: data_row.append("N/A") # 创建数据行:三列 new_row = [ data_row[0], # SPEC(dB)值 data_row[1], # Simulation值 data_row[2], # Pass/Fail值 ] # 表格数据:表头+数据行 modified_table = [headers, new_row] # 设置三列等宽布局 col_widths = [table_width / 3] * 3 table = Table(modified_table, colWidths=col_widths) # 设置表格样式 style = TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), white), ('TEXTCOLOR', (0, 0), (-1, 0), black), ('ALIGN', (0, 0), (-1, -1), 'CENTER'), ('FONTNAME', (0, 0), (-1, 0), font_name), ('FONTNAME', (0, 1), (-1, -1), font_name), ('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BACKGROUND', (0, 1), (-1, -1), white), ('GRID', (0, 0), (-1, -1), 1, black) ]) table.setStyle(style) # 计算表格高度并绘制 table_height = table.wrap(0, 0)[1] if y_position - table_height < 50: c.showPage() y_position = height - 50 c.setFont(font_name, 24) c.setFillColor(red) c.drawCentredString(width / 2.0, y_position, "Summary") y_position -= 50 c.setFillColor(black) table.drawOn(c, 50, y_position - table_height) y_position -= (table_height + 20) c.save() return output_path except Exception as e: self.log(f"创建Summary页失败: {str(e)}") return None def word_to_pdf(self, word_path, pdf_path): """将Word文档转换为PDF""" pythoncom.CoInitialize() try: word = win32com.client.Dispatch("Word.Application") word.Visible = False doc = word.Documents.Open(os.path.abspath(word_path)) doc.SaveAs(os.path.abspath(pdf_path), FileFormat=17) doc.Close() word.Quit() self.log(f"已将 {os.path.basename(word_path)} 转换为PDF") return True except Exception as e: self.log(f"转换 {os.path.basename(word_path)} 时出错: {str(e)}") return False finally: pythoncom.CoUninitialize() def get_pdf_page_count(self, pdf_path): """获取PDF文件的页数""" try: reader = PdfReader(pdf_path) return len(reader.pages) except: return 0 def merge_pdfs_with_summary(self, pdf_files, toc_entries, all_tables, output_path): """合并PDF文件并添加Summary页""" try: with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as summary_file: summary_path = summary_file.name # 调用修改后的create_summary_page,传入三个参数 self.create_summary_page(toc_entries, all_tables, summary_path) summary_page_count = self.get_pdf_page_count(summary_path) updated_toc_entries = [(title, page_num + summary_page_count) for title, page_num in toc_entries] merger = PdfMerger() merger.append(summary_path) current_page = summary_page_count for pdf, (title, _) in zip(pdf_files, updated_toc_entries): merger.append(pdf) merger.add_outline_item(title, current_page) current_page += self.get_pdf_page_count(pdf) merger.write(output_path) merger.close() os.remove(summary_path) self.log(f"已成功合并 {len(pdf_files)} 个PDF文件") return True except Exception as e: self.log(f"合并PDF时出错: {str(e)}") return False def get_all_word_files(self, folder_paths): """获取所有Word文件""" word_extensions = ['.docx', '.doc'] word_files = [] for folder_path in folder_paths: if not os.path.isdir(folder_path): continue for file in os.listdir(folder_path): file_ext = os.path.splitext(file)[1].lower() if file_ext in word_extensions: word_path = os.path.join(folder_path, file) word_files.append(word_path) return word_files def get_folder_name_parts(self, folder_paths): """生成报告文件名""" if not folder_paths: return "听筒磁干扰仿真报告" folder_path = folder_paths[0] norm_path = os.path.normpath(folder_path) parts = [p for p in norm_path.split(os.sep) if p] if len(parts) >= 3: return f"{parts[-3]}_{parts[-2]}_{parts[-1]}" elif len(parts) == 2: return f"{parts[-2]}_{parts[-1]}" elif len(parts) == 1: return parts[0] return "听筒磁干扰仿真报告" if __name__ == "__main__": root = tk.Tk() app = PDFConverterApp(root) root.mainloop() # 添加这行启动事件循环 D:\tes\PythonProject\.venv\Scripts\python.exe D:\tes\PythonProject\10-pdf.py Traceback (most recent call last): File "D:\tes\PythonProject\10-pdf.py", line 736, in <module> app = PDFConverterApp(root) File "D:\tes\PythonProject\10-pdf.py", line 34, in __init__ self.create_widgets() ~~~~~~~~~~~~~~~~~~~^^ File "D:\tes\PythonProject\10-pdf.py", line 38, in create_widgets top_frame = ttk.Framae(self.root, padding=10) ^^^^^^^^^^ AttributeError: module 'tkinter.ttk' has no attribute 'Framae'. Did you mean: 'Frame'?
10-03
OSError Traceback (most recent call last) Cell In[1], line 63 60 det[col] = pd.to_datetime(det[col], errors="coerce").dt.strftime("%Y-%m-%d") 62 return det ---> 63 det = read_detail(INPUT_XL) 65 # 1. 补空列(不存在就创建) 66 for col in ["Net", "Budget_Media_RMB", "Budget_Production_RMB", "Import_Remark"]: Cell In[1], line 16, in read_detail(file_path) 15 def read_detail(file_path: pathlib.Path) -> pd.DataFrame: ---> 16 raw = pd.read_excel(file_path, sheet_name="BB sheet", header=None) 17 idx = raw[raw.iloc[:, 0].astype(str).str.contains("OOH ISP", na=False)].index[0] 18 df = pd.read_excel(file_path, sheet_name="BB sheet", header=idx+1) File C:\ProgramData\miniforge3\lib\site-packages\pandas\io\excel\_base.py:495, in read_excel(io, sheet_name, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skiprows, nrows, na_values, keep_default_na, na_filter, verbose, parse_dates, date_parser, date_format, thousands, decimal, comment, skipfooter, storage_options, dtype_backend, engine_kwargs) 493 if not isinstance(io, ExcelFile): 494 should_close = True --> 495 io = ExcelFile( 496 io, 497 storage_options=storage_options, 498 engine=engine, 499 engine_kwargs=engine_kwargs, 500 ) 501 elif engine and engine != io.engine: 502 raise ValueError( 503 "Engine should not be specified when passing " 504 "an ExcelFile - ExcelFile already has the engine set" 505 ) File C:\ProgramData\miniforge3\lib\site-packages\pandas\io\excel\_base.py:1550, in ExcelFile.__init__(self, path_or_buffer, engine, storage_options, engine_kwargs) 1548 ext = "xls" 1549 else: -> 1550 ext = inspect_excel_format( 1551 content_or_path=path_or_buffer, storage_options=storage_options 1552 ) 1553 if ext is None: 1554 raise ValueError( 1555 "Excel file format cannot be determined, you must specify " 1556 "an engine manually." 1557 ) File C:\ProgramData\miniforge3\lib\site-packages\pandas\io\excel\_base.py:1402, in inspect_excel_format(content_or_path, storage_options) 1399 if isinstance(content_or_path, bytes): 1400 content_or_path = BytesIO(content_or_path) -> 1402 with get_handle( 1403 content_or_path, "rb", storage_options=storage_options, is_text=False 1404 ) as handle: 1405 stream = handle.handle 1406 stream.seek(0) File C:\ProgramData\miniforge3\lib\site-packages\pandas\io\common.py:882, in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options) 873 handle = open( 874 handle, 875 ioargs.mode, (...) 878 newline="", 879 ) 880 else: 881 # Binary mode --> 882 handle = open(handle, ioargs.mode) 883 handles.append(handle) 885 # Convert BytesIO or file objects passed with an encoding OSError: [Errno 22] Invalid argument: 'ODO*.xlsx'
最新发布
10-31
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值