基于Python开发的学习通签到数据统计工具

Written by

in

文章目录
  • 本文介绍的项目是一款面向学习通签到数据的可视化统计工具,核心目标是解决学习通导出的签到表格手动统计效率低、易出错的问题。下面将详细讲解学习通签到统计工具开发过程以及Python代码完整实现。
  • 开发前首先明确核心需求,确保工具贴合实际使用场景:
  • 结合需求与开发成本,选择轻量、易部署的技术栈: 技术 / 库 选型理由 Python 3.8+ 生态丰富,数据处理库成熟,跨平台,打包成 exe 后无需安装运行环境 tkinter + ttk Python 内置 GUI 库,无需额外安装,足够支撑轻量桌面应用;ttk 提升界面美观度 pandas 高效处理表格数据,支持 Excel/CSV 读写、分组统计,是 Python 表格处理的首选库 openpyxl pandas 写入 Excel 的依赖库,支持.xlsx 格式,处理大数据量更稳定 os/pathlib 处理文件路径、遍历文件夹,跨平台兼容 datetime 生成日志时间戳,记录操作时间 warnings 过滤 openpyxl 的样式警告,提升运行体验 pyinstaller(后续打包) 将代码打包成 exe,方便非技术用户使用
  • 采用面向对象(OOP) 设计,将功能封装为AttendanceApp类,核心优势是 “数据与逻辑封装、代码复用、便于维护”。类的核心结构如下: 核心设计思路: 全局变量:file_paths存储导入的文件路径,summary_data缓存汇总数据,避免重复计算; UI 与逻辑分离:create_widgets()只负责 UI 渲染,业务逻辑封装在独立方法中; 异常分层处理:批量处理时捕获单个文件的异常,不影响其他文件处理; 用户反馈闭环:操作→进度条更新→状态提示→日志记录,全程可视化。
  • 开发完成后,需覆盖多场景测试,修复问题并优化体验:
  • import tkinter as tk from tkinter import ttk, filedialog, messagebox, scrolledtext import pandas as pd import os from pathlib import Path import datetime import warnings class AttendanceApp: def __init__(self, root): self.root = root self.root.title(“学习通签到统计工具”) self.root.geometry(“900×700″) # 全局变量 self.file_paths = [] self.summary_data = [] # 过滤openpyxl的样式警告 warnings.filterwarnings(‘ignore’, category=UserWarning, module=’openpyxl’) # 创建UI组件 self.create_widgets() def create_widgets(self): # 顶部说明 ttk.Label(self.root, text=”学习通签到统计工具”, font=(“微软雅黑”, 16)).pack(pady=10) ttk.Label(self.root, text=”支持CSV/Excel文件导入,自动统计签到次数”).pack(pady=5) # 按钮区域 button_frame = ttk.Frame(self.root) button_frame.pack(pady=10, fill=tk.X, padx=20) ttk.Button(button_frame, text=”导入单个文件”, command=self.import_single_file).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text=”批量导入文件”, command=self.import_multiple_files).pack(side=tk.LEFT, padx=5) ttk.Button(button_frame, text=”导入文件夹”, command=self.import_folder).pack(side=tk.LEFT, padx=5) # 重置按钮 ttk.Button(button_frame, text=”重置所有”, command=self.reset_all).pack(side=tk.RIGHT, padx=5) ttk.Button(button_frame, text=”处理选中文件”, command=self.process_files).pack(side=tk.RIGHT, padx=5) # padx参数移到pack方法中(ttk.Button初始化不支持padx) ttk.Button(button_frame, text=”生成汇总表”, command=self.generate_summary_button).pack(side=tk.RIGHT, padx=10) # 文件列表显示 file_frame = ttk.Frame(self.root) file_frame.pack(pady=5, fill=tk.BOTH, expand=True, padx=20) ttk.Label(file_frame, text=”已导入文件列表:”).pack(anchor=tk.W) self.file_listbox = tk.Listbox(file_frame, height=8) self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # 滚动条 file_scroll = ttk.Scrollbar(file_frame, orient=tk.VERTICAL, command=self.file_listbox.yview) file_scroll.pack(side=tk.RIGHT, fill=tk.Y) self.file_listbox.config(yscrollcommand=file_scroll.set) # 进度条 self.progress = ttk.Progressbar(self.root, orient=tk.HORIZONTAL, length=400, mode=’determinate’) self.progress.pack(pady=5) # 状态标签 self.status_label = ttk.Label(self.root, text=”等待导入文件…”) self.status_label.pack(pady=5) # 日志区域 log_frame = ttk.Frame(self.root) log_frame.pack(pady=5, fill=tk.BOTH, expand=True, padx=20) ttk.Label(log_frame, text=”操作日志:”).pack(anchor=tk.W) self.log_text = scrolledtext.ScrolledText(log_frame, height=15, state=tk.DISABLED, wrap=tk.WORD) self.log_text.pack(fill=tk.BOTH, expand=True) # 清空日志按钮 ttk.Button(log_frame, text=”清空日志”, command=self.clear_log).pack(side=tk.RIGHT, pady=5) def log_message(self, message, level=”INFO”): “””添加日志信息””” # 生成时间戳 timestamp = datetime.datetime.now().strftime(“%Y-%m-%d %H:%M:%S”) # 格式化日志内容 log_entry = f”[{timestamp}] [{level}] {message}n” # 启用文本框,添加内容,然后禁用 self.log_text.config(state=tk.NORMAL) self.log_text.insert(tk.END, log_entry) # 自动滚动到最后 self.log_text.see(tk.END) self.log_text.config(state=tk.DISABLED) 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_message(“日志已清空”, “INFO”) def reset_all(self): “””重置所有状态(重新统计)””” # 确认重置操作 if messagebox.askyesno(“确认重置”, “是否确定重置所有数据?这将清空已导入的文件和统计数据!”): # 清空全局变量 self.file_paths = [] self.summary_data = [] # 清空文件列表 self.file_listbox.delete(0, tk.END) # 重置进度条 self.progress[‘value’] = 0 # 重置状态标签 self.status_label.config(text=”等待导入文件…”) # 日志记录 self.log_message(“已重置所有数据:清空文件列表和汇总数据”, “WARNING”) messagebox.showinfo(“重置完成”, “所有数据已重置,可重新导入文件进行统计”) def import_single_file(self): “””导入单个文件””” file_path = filedialog.askopenfilename( filetypes=[(“Excel文件”, “*.xlsx”), (“CSV文件”, “*.csv”)] ) if file_path and file_path not in self.file_paths and not self.is_temp_file(file_path): self.file_paths.append(file_path) self.update_file_list() self.log_message(f”成功导入单个文件:{os.path.basename(file_path)}”) elif self.is_temp_file(file_path): self.log_message(f”跳过临时文件:{os.path.basename(file_path)}”, “WARNING”) def import_multiple_files(self): “””批量导入多个文件””” files = filedialog.askopenfilenames( filetypes=[(“Excel文件”, “*.xlsx”), (“CSV文件”, “*.csv”)] ) imported_count = 0 skipped_count = 0 for file in files: if file not in self.file_paths and not self.is_temp_file(file): self.file_paths.append(file) imported_count += 1 elif self.is_temp_file(file): skipped_count += 1 self.update_file_list() self.log_message(f”批量导入完成:成功导入{imported_count}个文件,跳过{skipped_count}个临时文件”) def import_folder(self): “””导入整个文件夹””” folder_path = filedialog.askdirectory() if folder_path: imported_count = 0 skipped_count = 0 for root, _, files in os.walk(folder_path): for file in files: if file.endswith((‘.xlsx’, ‘.csv’)): file_path = os.path.join(root, file) if file_path not in self.file_paths and not self.is_temp_file(file_path): self.file_paths.append(file_path) imported_count += 1 elif self.is_temp_file(file_path): skipped_count += 1 self.update_file_list() self.log_message(f”文件夹导入完成:从{folder_path}导入{imported_count}个文件,跳过{skipped_count}个临时文件”) def is_temp_file(self, file_path): “””判断是否是临时文件(包含~或隐藏文件)””” filename = os.path.basename(file_path) return filename.startswith(‘~$’) or filename.startswith(‘.’) def update_file_list(self): “””更新文件列表显示””” self.file_listbox.delete(0, tk.END) for file in self.file_paths: self.file_listbox.insert(tk.END, os.path.basename(file)) self.status_label.config(text=f”已导入 {len(self.file_paths)} 个文件”) def process_files(self): “””处理所有导入的文件(仅生成单个文件统计结果,不汇总)””” if not self.file_paths: messagebox.showwarning(“警告”, “请先导入文件!”) self.log_message(“处理文件失败:未导入任何文件”, “ERROR”) return self.progress[‘value’] = 0 self.progress[‘maximum’] = len(self.file_paths) self.summary_data = [] # 重置汇总数据 self.log_message(f”开始处理{len(self.file_paths)}个文件…”) success_count = 0 fail_count = 0 for i, file_path in enumerate(self.file_paths): try: self.log_message(f”正在处理文件:{os.path.basename(file_path)}”) self.process_single_file(file_path) self.progress[‘value’] = i + 1 self.root.update_idletasks() success_count += 1 self.log_message(f”文件处理成功:{os.path.basename(file_path)}”) except PermissionError: fail_count += 1 error_msg = f”权限错误:无法访问文件{os.path.basename(file_path)},请确保文件未被打开且有读写权限” messagebox.showerror(“权限错误”, error_msg) self.log_message(error_msg, “ERROR”) except KeyError as e: fail_count += 1 error_msg = f”列名错误:文件{os.path.basename(file_path)}缺少关键列{str(e)}” messagebox.showerror(“列名错误”, error_msg) self.log_message(error_msg, “ERROR”) except Exception as e: fail_count += 1 error_msg = f”处理文件{os.path.basename(file_path)}时出错:{str(e)}” messagebox.showerror(“错误”, error_msg) self.log_message(error_msg, “ERROR”) self.progress[‘value’] = 0 self.status_label.config(text=f”文件处理完成:成功{success_count}个,失败{fail_count}个”) self.log_message(f”文件处理结束:成功{success_count}个,失败{fail_count}个”) if success_count > 0: messagebox.showinfo(“完成”, f”文件处理完成!成功{success_count}个,失败{fail_count}个n可点击’生成汇总表’按钮创建汇总文件”) def find_header_row(self, df): “””动态查找包含’签到状态’的表头行””” for i, row in df.iterrows(): if ‘签到状态’ in str(row.values): return i return None def process_single_file(self, file_path): “””处理单个签到文件””” # 读取整个文件,用于动态检测表头 if file_path.endswith(‘.xlsx’): full_df = pd.read_excel(file_path, header=None) elif file_path.endswith(‘.csv’): full_df = pd.read_csv(file_path, header=None) # 动态查找表头行 header_row = self.find_header_row(full_df) if header_row is None: raise ValueError(“未找到包含’签到状态’的表头行,请确认文件格式”) # 用找到的表头行重新读取文件 if file_path.endswith(‘.xlsx’): df = pd.read_excel(file_path, header=header_row) elif file_path.endswith(‘.csv’): df = pd.read_csv(file_path, header=header_row) # 清理列名中的空格和特殊字符 df.columns = [col.strip() for col in df.columns] # 验证关键列是否存在 required_columns = [‘姓名’, ‘学号/工号’, ‘学校’, ‘院系’, ‘专业’, ‘行政班级’, ‘签到状态’] for col in required_columns: if col not in df.columns: raise KeyError(col) # 生成已签统计 df[‘已签统计’] = df[‘签到状态’].apply( lambda x: 1 if str(x).strip() == ‘已签’ else 0 if str(x).strip() in [‘教师代签’, ‘迟到’, ‘未参与’] else None ) # 生成教师代签统计 df[‘教师代签统计’] = df[‘签到状态’].apply( lambda x: 1 if str(x).strip() == ‘教师代签’ else 0 if str(x).strip() in [‘已签’, ‘迟到’, ‘未参与’] else None ) # 生成迟到统计 df[‘迟到统计’] = df[‘签到状态’].apply( lambda x: 1 if str(x).strip() == ‘迟到’ else 0 if str(x).strip() in [‘已签’, ‘教师代签’, ‘未参与’] else None ) # 生成未参与统计 df[‘未参与统计’] = df[‘签到状态’].apply( lambda x: 1 if str(x).strip() == ‘未参与’ else 0 if str(x).strip() in [‘已签’, ‘教师代签’, ‘迟到’] else None ) # 生成最终签到统计(已签/教师代签/迟到都算1,未参与算0) df[‘最终签到统计’] = df[‘签到状态’].apply( lambda x: 1 if str(x).strip() in [‘已签’, ‘教师代签’, ‘迟到’] else 0 if str(x).strip() == ‘未参与’ else None ) # 保存处理后的文件 try: output_path = f”{os.path.splitext(file_path)[0]}_统计结果.xlsx” df.to_excel(output_path, index=False) self.log_message(f”统计文件已保存:{os.path.basename(output_path)}”) except PermissionError: # 如果原路径无法写入,让用户选择保存位置 self.log_message(f”原路径无写入权限,弹出保存对话框:{os.path.basename(file_path)}”, “WARNING”) output_path = filedialog.asksaveasfilename( defaultextension=”.xlsx”, initialfile=f”{os.path.splitext(os.path.basename(file_path))[0]}_统计结果.xlsx”, filetypes=[(“Excel文件”, “*.xlsx”)] ) if output_path: df.to_excel(output_path, index=False) self.log_message(f”统计文件已保存到指定位置:{os.path.basename(output_path)}”) else: raise PermissionError(“用户取消了文件保存操作”) # 收集汇总数据(包含所有需要的字段) file_name = os.path.basename(file_path) for _, row in df.iterrows(): self.summary_data.append({ ‘姓名’: row[‘姓名’], ‘学号/工号’: row[‘学号/工号’], ‘学校’: row[‘学校’], ‘院系’: row[‘院系’], ‘专业’: row[‘专业’], ‘行政班级’: row[‘行政班级’], ‘文件名’: file_name, ‘已签统计’: row[‘已签统计’], ‘教师代签统计’: row[‘教师代签统计’], ‘迟到统计’: row[‘迟到统计’], ‘未参与统计’: row[‘未参与统计’], ‘最终签到统计’: row[‘最终签到统计’] }) def generate_summary_button(self): “””独立的汇总表生成按钮处理函数””” if not self.summary_data: messagebox.showwarning(“警告”, “暂无汇总数据!请先处理文件”) self.log_message(“生成汇总表失败:暂无汇总数据”, “WARNING”) return self.log_message(“开始生成汇总表…”) try: summary_df = pd.DataFrame(self.summary_data) # 按分组对所有统计列求和 final_summary = summary_df.groupby( [‘姓名’, ‘学号/工号’, ‘学校’, ‘院系’, ‘专业’, ‘行政班级’] ).agg({ ‘已签统计’: ‘sum’, ‘教师代签统计’: ‘sum’, ‘迟到统计’: ‘sum’, ‘未参与统计’: ‘sum’, ‘最终签到统计’: ‘sum’ }).reset_index() # 重命名列名为”总XXX统计” final_summary.rename(columns={ ‘已签统计’: ‘总已签统计’, ‘教师代签统计’: ‘总教师代签统计’, ‘迟到统计’: ‘总迟到统计’, ‘未参与统计’: ‘总未参与统计’, ‘最终签到统计’: ‘总最终签到统计’ }, inplace=True) # 保存汇总表 save_path = filedialog.asksaveasfilename( defaultextension=”.xlsx”, initialfile=”学习通签到汇总表.xlsx”, filetypes=[(“Excel文件”, “*.xlsx”)] ) if save_path: final_summary.to_excel(save_path, index=False) self.log_message(f”汇总表生成成功:{os.path.basename(save_path)}”) self.log_message(f”汇总数据:共统计{len(final_summary)}名学生的签到情况,包含多维度签到统计”) messagebox.showinfo(“成功”, f”汇总表已保存!n文件路径:{save_path}n共统计{len(final_summary)}名学生n包含:总已签、总教师代签、总迟到、总未参与、总最终签到统计”) else: self.log_message(“用户取消了汇总表保存操作”, “WARNING”) except Exception as e: error_msg = f”生成汇总表时出错:{str(e)}” messagebox.showerror(“错误”, error_msg) self.log_message(error_msg, “ERROR”) if __name__ == “__main__”: root = tk.Tk() app = AttendanceApp(root) root.mainloop()
  • 本文介绍了一款基于Python开发的学习通签到数据统计工具,该工具通过GUI界面实现高效自动化处理,主要解决手动统计Excel/CSV签到表格效率低、易出错的问题。工具核心功能包括:支持单文件/多文件/文件夹导入;自动识别动态表头;多维度统计签到状态(已签、教师代签、迟到、未参与);生成带统计列的单个文件和汇总表。采用tkinter+ttk构建界面,pandas处理数据,具有异常处理、日志记录和进度反馈机制。经过测试验证,工具能准确处理学习通导出的各类签到文件,并通过pyinstaller打包为60-90MB的独立exe程序,方便非技术用户使用。 以上就是基于Python开发的学习通签到数据统计工具的详细内容,更多关于Python学习通签到统计的资料请关注风君子博客其它相关文章! 您可能感兴趣的文章: python3 chromedrivers签到的简单实现 利用Python实现网站自动签到 Python+OpenCV人脸识别签到考勤系统实现(附demo) 如何利用Python实现自动打卡签到的实践 Python利用Selenium实现网站自动签到功能
  • 目录
    • 一、引言
    • 二、第一阶段:需求分析与功能规划
      • 1. 核心业务需求
      • 2. 非功能需求
    • 三、第二阶段:技术选型
      • 四、第三阶段:架构设计
        • 五、第四阶段:分模块详细开发
          • 1. 初始化与基础配置(__init__方法)
          • 2. UI 组件开发(create_widgets 方法)
          • 3. 日志系统开发(log_message/clear_log 方法)
          • 4. 文件导入模块开发
            • (1)临时文件过滤(is_temp_file)
            • (2)单文件 / 批量 / 文件夹导入
          • 5. 核心数据处理模块开发
            • (1)动态查找表头行(find_header_row)
            • (2)单个文件处理(process_single_file)
            • (3)批量处理文件(process_files)
          • 6. 汇总表生成模块(generate_summary_button)
            • 7. 重置功能(reset_all)
            • 六、第五阶段:测试与优化
              • 1. 核心测试场景
                • 2. 关键优化点
                • 七、学习通签到统计工具的Python代码完整实现
                  • 八、程序运行部分截图展示
                    • 九、打包项目
                      • 步骤 1:创建并激活纯净虚拟环境打开 Anaconda Prompt,执行以下命令(复制粘贴即可)
                        • 步骤 2:切换目录并快速打包
                          • 步骤 3:验证结果
                          • 十、总结

                            本文介绍的项目是一款面向学习通签到数据的可视化统计工具,核心目标是解决学习通导出的签到表格手动统计效率低、易出错的问题。下面将详细讲解学习通签到统计工具开发过程以及Python代码完整实现。

                            开发前首先明确核心需求,确保工具贴合实际使用场景:

                            • 支持导入单文件、多文件、整个文件夹的学习通签到文件(Excel/CSV 格式);
                            • 自动识别文件中 “签到状态” 相关表头(解决学习通导出文件表头行不固定的问题);
                            • 多维度统计签到状态:区分 “已签、教师代签、迟到、未参与”,并计算 “最终签到统计”(前三者算有效签到,未参与算无效);
                            • 生成单个文件的统计结果(带新增统计列),并支持多文件汇总(按学生维度统计总次数);
                            • 处理过程中给出可视化反馈(进度条、状态提示),并记录操作日志(便于排查问题)。

                            • 易用性:GUI 界面操作,无需命令行,适配普通用户;
                            • 容错性:处理文件时捕获权限错误、列缺失、格式错误等异常,给出友好提示;
                            • 兼容性:支持 Excel(.xlsx)和 CSV 格式,过滤临时文件(如~$ 开头的 Excel 临时文件);
                            • 可回溯:日志记录所有操作,支持清空日志、重置所有数据。

                            结合需求与开发成本,选择轻量、易部署的技术栈:

                            技术 / 库 选型理由
                            Python 3.8+ 生态丰富,数据处理库成熟,跨平台,打包成 exe 后无需安装运行环境
                            tkinter + ttk Python 内置 GUI 库,无需额外安装,足够支撑轻量桌面应用;ttk 提升界面美观度
                            pandas 高效处理表格数据,支持 Excel/CSV 读写、分组统计,是 Python 表格处理的首选库
                            openpyxl pandas 写入 Excel 的依赖库,支持.xlsx 格式,处理大数据量更稳定
                            os/pathlib 处理文件路径、遍历文件夹,跨平台兼容
                            datetime 生成日志时间戳,记录操作时间
                            warnings 过滤 openpyxl 的样式警告,提升运行体验
                            pyinstaller(后续打包) 将代码打包成 exe,方便非技术用户使用

                            采用面向对象(OOP) 设计,将功能封装为AttendanceApp类,核心优势是 “数据与逻辑封装、代码复用、便于维护”。类的核心结构如下:

                            核心设计思路:

                            • 全局变量file_paths存储导入的文件路径,summary_data缓存汇总数据,避免重复计算;
                            • UI 与逻辑分离create_widgets()只负责 UI 渲染,业务逻辑封装在独立方法中;
                            • 异常分层处理:批量处理时捕获单个文件的异常,不影响其他文件处理;
                            • 用户反馈闭环:操作→进度条更新→状态提示→日志记录,全程可视化。

                            def __init__(self, root):
                                self.root = root
                                self.root.title("学习通签到统计工具")
                                self.root.geometry("900x700")  # 固定窗口大小,适配主流屏幕
                                # 全局变量初始化:存储文件路径和汇总数据
                                self.file_paths = []
                                self.summary_data = []
                                # 过滤openpyxl的样式警告(提升用户体验)
                                warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl')
                                # 初始化UI
                                self.create_widgets()
                            
                            • 初始化主窗口的标题、尺寸,定义核心全局变量;
                            • 过滤第三方库的无关警告,避免日志 / 控制台出现冗余信息。

                            按 “功能分区” 构建 UI,确保布局清晰、操作符合用户习惯:

                            组件区域 核心控件与作用
                            顶部说明区 Label 控件:显示工具名称、功能说明,字体放大提升辨识度
                            按钮操作区 Button 控件:导入文件(单 / 多 / 文件夹)、重置、处理文件、生成汇总表,按 “操作流程” 排列
                            文件列表区 Listbox + Scrollbar:显示已导入文件,支持滚动查看,适配多文件场景
                            进度反馈区 Progressbar + Label:显示处理进度、当前状态(如 “等待导入文件”)
                            日志记录区 ScrolledText + Button:滚动日志框(禁用手动编辑)、清空日志按钮

                            关键实现细节:

                            • 按钮区使用ttk.Frame包裹,通过side=tk.LEFT/RIGHT排列,保证布局整齐;
                            • 日志框设置为tk.DISABLED状态,避免用户误编辑,仅通过log_message方法写入;
                            • 所有控件的pack方法添加padx/pady(内边距),提升界面美观度。

                            日志是工具的 “调试与追溯” 核心,设计要点:

                            def log_message(self, message, level="INFO"):
                                timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")  # 精确到秒的时间戳
                                log_entry = f"[{timestamp}] [{level}] {message}n"  # 标准化日志格式
                                # 临时启用日志框,写入内容后禁用
                                self.log_text.config(state=tk.NORMAL)
                                self.log_text.insert(tk.END, log_entry)
                                self.log_text.see(tk.END)  # 自动滚动到最新日志
                                self.log_text.config(state=tk.DISABLED)
                            
                            • 日志包含 “时间戳 + 级别 + 内容”,级别分为 INFO/WARNING/ERROR,便于区分操作类型;
                            • 自动滚动到最新日志,无需用户手动翻页;
                            • clear_log方法提供日志清空功能,同时记录 “日志已清空” 的操作,保证日志链完整。

                            解决 “如何便捷导入文件并过滤无效文件” 的问题,核心是 3 个导入方法 + 1 个辅助方法:

                            学习通导出文件时可能生成临时文件(如~$xxx.xlsx),需过滤避免处理错误:

                            def is_temp_file(self, file_path):
                                filename = os.path.basename(file_path)
                                return filename.startswith('~$') or filename.startswith('.')  # 过滤Excel临时文件/隐藏文件
                            

                            • import_single_file:调用filedialog.askopenfilename选择单个文件,校验后加入列表;
                            • import_multiple_files:调用askopenfilenames选择多文件,批量校验、统计导入 / 跳过数量;
                            • import_folder:调用askdirectory选择文件夹,通过os.walk遍历所有 Excel/CSV 文件,批量导入;
                            • 所有导入方法最终调用update_file_list更新 Listbox 显示,并通过日志反馈导入结果。

                            关键设计:

                            • 校验文件是否已导入(避免重复)、是否为临时文件(避免无效处理);
                            • 统计 “成功导入 / 跳过” 数量,通过日志和状态标签反馈,提升透明度。

                            这是工具的 “业务核心”,解决 “如何解析学习通文件并统计数据” 的问题,分为 3 个核心方法:

                            学习通导出的签到文件表头行不固定(可能第 1 行是标题,第 2 行是表头),需动态定位包含 “签到状态” 的行:

                            def find_header_row(self, df):
                                for i, row in df.iterrows():
                                    if '签到状态' in str(row.values):  # 遍历每行,查找包含目标字段的行
                                        return i
                                return None  # 未找到则返回None,后续抛出异常
                            
                            • 先以header=None读取整个文件(无表头),遍历所有行查找 “签到状态”;
                            • 找到后再以该行为表头重新读取文件,确保列名正确。

                            这是数据处理的核心,步骤拆解:

                            1. 读取文件:根据后缀(xlsx/csv)选择pd.read_excel/read_csv,先读取全量数据找表头;
                            2. 校验表头:若未找到 “签到状态” 行,抛出ValueError
                            3. 重新读取 + 列名清理:以找到的表头行重新读取,清理列名空格(避免 “姓名” 和 “姓名” 被识别为不同列);
                            4. 关键列校验:检查是否包含['姓名', '学号/工号', '签到状态']等必填列,缺失则抛出KeyError
                            5. 多维度统计
                            # 已签统计:仅“已签”算1,其他状态算0
                            df['已签统计'] = df['签到状态'].apply(lambda x: 1 if str(x).strip() == '已签' else 0 if str(x).strip() in ['教师代签', '迟到', '未参与'] else None)
                            # 最终签到统计:已签/教师代签/迟到算1,未参与算0(核心统计维度)
                            df['最终签到统计'] = df['签到状态'].apply(lambda x: 1 if str(x).strip() in ['已签', '教师代签', '迟到'] else 0 if str(x).strip() == '未参与' else None)
                            
                            • 每个统计列仅针对目标状态标 1,其他有效状态标 0,未知状态标 None(便于后续排查异常);
                            1. 保存统计结果
                              • 优先保存到原文件同目录(命名为 “原文件名_统计结果.xlsx”);
                              • 若原路径无权限(如文件在只读文件夹),弹出保存对话框让用户选择路径;
                            2. 缓存汇总数据:将当前文件的统计结果存入summary_data,为后续汇总表做准备。

                            实现 “批量处理 + 异常隔离 + 进度反馈”:

                            1. 校验是否有导入文件,无则提示并返回;
                            2. 初始化进度条(最大值为文件数量),重置汇总数据;
                            3. 遍历所有文件,逐个调用process_single_file
                            4. 异常处理:捕获PermissionError(文件被占用)、KeyError(列缺失)、通用Exception,记录失败数,不中断批量处理;
                            5. 实时更新进度条(self.root.update_idletasks()强制刷新 UI);
                            6. 处理完成后,通过弹窗 + 日志反馈 “成功 / 失败数量”。

                            解决 “多文件按学生维度汇总” 的需求,核心逻辑:

                            1. 校验summary_data是否为空(无处理结果则提示);
                            2. 将汇总数据转为 DataFrame,按['姓名', '学号/工号', '学校', '院系', '专业', '行政班级']分组(确保学生唯一);
                            3. 对所有统计列求和(agg({'已签统计':'sum', ...})),并重命名为 “总 XXX 统计”;
                            4. 弹出保存对话框,将汇总结果保存为 Excel 文件;
                            5. 反馈汇总结果(统计学生数量、保存路径)。

                            支持用户 “清空所有数据重新操作”,设计要点:

                            • 弹出确认对话框(避免误操作);
                            • 清空全局变量(file_paths/summary_data)、文件列表、进度条、状态标签;
                            • 记录重置日志,反馈重置完成。

                            开发完成后,需覆盖多场景测试,修复问题并优化体验:

                            测试场景 测试目的 测试结果
                            导入单个 Excel/CSV 文件 验证文件读取、表头查找、统计是否正确 统计列生成正常,文件保存成功
                            导入临时文件(~$xxx.xlsx) 验证临时文件过滤逻辑 自动跳过,日志提示 “跳过临时文件”
                            导入缺失 “签到状态” 列的文件 验证列校验、异常提示 弹出错误提示,日志记录 KeyError
                            导入被占用的文件 验证权限错误处理、保存对话框弹出 提示权限错误,弹出保存对话框
                            批量导入 10 + 文件 验证进度条、批量处理、异常隔离 进度条实时更新,失败文件不影响其他
                            生成汇总表 验证分组求和、列重命名、保存 汇总数据正确,列名符合预期
                            重置所有数据 验证数据清空、UI 重置 所有数据 / UI 恢复初始状态

                            • 过滤 openpyxl 的样式警告(避免日志冗余);
                            • 日志自动滚动到最新条目(无需手动翻页);
                            • 进度条实时刷新(update_idletasks强制 UI 更新);
                            • 列名清理(col.strip()):解决学习通文件列名带空格的问题;
                            • 多维度统计:满足用户对不同签到状态的细分统计需求。

                            import tkinter as tk
                            from tkinter import ttk, filedialog, messagebox, scrolledtext
                            import pandas as pd
                            import os
                            from pathlib import Path
                            import datetime
                            import warnings
                            
                            class AttendanceApp:
                                def __init__(self, root):
                                    self.root = root
                                    self.root.title("学习通签到统计工具")
                                    self.root.geometry("900x700")
                            
                                    # 全局变量
                                    self.file_paths = []
                                    self.summary_data = []
                            
                                    # 过滤openpyxl的样式警告
                                    warnings.filterwarnings('ignore', category=UserWarning, module='openpyxl')
                            
                                    # 创建UI组件
                                    self.create_widgets()
                            
                                def create_widgets(self):
                                    # 顶部说明
                                    ttk.Label(self.root, text="学习通签到统计工具", font=("微软雅黑", 16)).pack(pady=10)
                                    ttk.Label(self.root, text="支持CSV/Excel文件导入,自动统计签到次数").pack(pady=5)
                            
                                    # 按钮区域
                                    button_frame = ttk.Frame(self.root)
                                    button_frame.pack(pady=10, fill=tk.X, padx=20)
                            
                                    ttk.Button(button_frame, text="导入单个文件", command=self.import_single_file).pack(side=tk.LEFT, padx=5)
                                    ttk.Button(button_frame, text="批量导入文件", command=self.import_multiple_files).pack(side=tk.LEFT, padx=5)
                                    ttk.Button(button_frame, text="导入文件夹", command=self.import_folder).pack(side=tk.LEFT, padx=5)
                                    # 重置按钮
                                    ttk.Button(button_frame, text="重置所有", command=self.reset_all).pack(side=tk.RIGHT, padx=5)
                                    ttk.Button(button_frame, text="处理选中文件", command=self.process_files).pack(side=tk.RIGHT, padx=5)
                                    # padx参数移到pack方法中(ttk.Button初始化不支持padx)
                                    ttk.Button(button_frame, text="生成汇总表", command=self.generate_summary_button).pack(side=tk.RIGHT, padx=10)
                            
                                    # 文件列表显示
                                    file_frame = ttk.Frame(self.root)
                                    file_frame.pack(pady=5, fill=tk.BOTH, expand=True, padx=20)
                            
                                    ttk.Label(file_frame, text="已导入文件列表:").pack(anchor=tk.W)
                                    self.file_listbox = tk.Listbox(file_frame, height=8)
                                    self.file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
                            
                                    # 滚动条
                                    file_scroll = ttk.Scrollbar(file_frame, orient=tk.VERTICAL, command=self.file_listbox.yview)
                                    file_scroll.pack(side=tk.RIGHT, fill=tk.Y)
                                    self.file_listbox.config(yscrollcommand=file_scroll.set)
                            
                                    # 进度条
                                    self.progress = ttk.Progressbar(self.root, orient=tk.HORIZONTAL, length=400, mode='determinate')
                                    self.progress.pack(pady=5)
                            
                                    # 状态标签
                                    self.status_label = ttk.Label(self.root, text="等待导入文件...")
                                    self.status_label.pack(pady=5)
                            
                                    # 日志区域
                                    log_frame = ttk.Frame(self.root)
                                    log_frame.pack(pady=5, fill=tk.BOTH, expand=True, padx=20)
                            
                                    ttk.Label(log_frame, text="操作日志:").pack(anchor=tk.W)
                                    self.log_text = scrolledtext.ScrolledText(log_frame, height=15, state=tk.DISABLED, wrap=tk.WORD)
                                    self.log_text.pack(fill=tk.BOTH, expand=True)
                            
                                    # 清空日志按钮
                                    ttk.Button(log_frame, text="清空日志", command=self.clear_log).pack(side=tk.RIGHT, pady=5)
                            
                                def log_message(self, message, level="INFO"):
                                    """添加日志信息"""
                                    # 生成时间戳
                                    timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                                    # 格式化日志内容
                                    log_entry = f"[{timestamp}] [{level}] {message}n"
                            
                                    # 启用文本框,添加内容,然后禁用
                                    self.log_text.config(state=tk.NORMAL)
                                    self.log_text.insert(tk.END, log_entry)
                                    # 自动滚动到最后
                                    self.log_text.see(tk.END)
                                    self.log_text.config(state=tk.DISABLED)
                            
                                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_message("日志已清空", "INFO")
                            
                                def reset_all(self):
                                    """重置所有状态(重新统计)"""
                                    # 确认重置操作
                                    if messagebox.askyesno("确认重置", "是否确定重置所有数据?这将清空已导入的文件和统计数据!"):
                                        # 清空全局变量
                                        self.file_paths = []
                                        self.summary_data = []
                                        # 清空文件列表
                                        self.file_listbox.delete(0, tk.END)
                                        # 重置进度条
                                        self.progress['value'] = 0
                                        # 重置状态标签
                                        self.status_label.config(text="等待导入文件...")
                                        # 日志记录
                                        self.log_message("已重置所有数据:清空文件列表和汇总数据", "WARNING")
                                        messagebox.showinfo("重置完成", "所有数据已重置,可重新导入文件进行统计")
                            
                                def import_single_file(self):
                                    """导入单个文件"""
                                    file_path = filedialog.askopenfilename(
                                        filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv")]
                                    )
                                    if file_path and file_path not in self.file_paths and not self.is_temp_file(file_path):
                                        self.file_paths.append(file_path)
                                        self.update_file_list()
                                        self.log_message(f"成功导入单个文件:{os.path.basename(file_path)}")
                                    elif self.is_temp_file(file_path):
                                        self.log_message(f"跳过临时文件:{os.path.basename(file_path)}", "WARNING")
                            
                                def import_multiple_files(self):
                                    """批量导入多个文件"""
                                    files = filedialog.askopenfilenames(
                                        filetypes=[("Excel文件", "*.xlsx"), ("CSV文件", "*.csv")]
                                    )
                                    imported_count = 0
                                    skipped_count = 0
                                    for file in files:
                                        if file not in self.file_paths and not self.is_temp_file(file):
                                            self.file_paths.append(file)
                                            imported_count += 1
                                        elif self.is_temp_file(file):
                                            skipped_count += 1
                                    self.update_file_list()
                                    self.log_message(f"批量导入完成:成功导入{imported_count}个文件,跳过{skipped_count}个临时文件")
                            
                                def import_folder(self):
                                    """导入整个文件夹"""
                                    folder_path = filedialog.askdirectory()
                                    if folder_path:
                                        imported_count = 0
                                        skipped_count = 0
                                        for root, _, files in os.walk(folder_path):
                                            for file in files:
                                                if file.endswith(('.xlsx', '.csv')):
                                                    file_path = os.path.join(root, file)
                                                    if file_path not in self.file_paths and not self.is_temp_file(file_path):
                                                        self.file_paths.append(file_path)
                                                        imported_count += 1
                                                    elif self.is_temp_file(file_path):
                                                        skipped_count += 1
                                        self.update_file_list()
                                        self.log_message(f"文件夹导入完成:从{folder_path}导入{imported_count}个文件,跳过{skipped_count}个临时文件")
                            
                                def is_temp_file(self, file_path):
                                    """判断是否是临时文件(包含~或隐藏文件)"""
                                    filename = os.path.basename(file_path)
                                    return filename.startswith('~$') or filename.startswith('.')
                            
                                def update_file_list(self):
                                    """更新文件列表显示"""
                                    self.file_listbox.delete(0, tk.END)
                                    for file in self.file_paths:
                                        self.file_listbox.insert(tk.END, os.path.basename(file))
                                    self.status_label.config(text=f"已导入 {len(self.file_paths)} 个文件")
                            
                                def process_files(self):
                                    """处理所有导入的文件(仅生成单个文件统计结果,不汇总)"""
                                    if not self.file_paths:
                                        messagebox.showwarning("警告", "请先导入文件!")
                                        self.log_message("处理文件失败:未导入任何文件", "ERROR")
                                        return
                            
                                    self.progress['value'] = 0
                                    self.progress['maximum'] = len(self.file_paths)
                                    self.summary_data = []  # 重置汇总数据
                            
                                    self.log_message(f"开始处理{len(self.file_paths)}个文件...")
                            
                                    success_count = 0
                                    fail_count = 0
                            
                                    for i, file_path in enumerate(self.file_paths):
                                        try:
                                            self.log_message(f"正在处理文件:{os.path.basename(file_path)}")
                                            self.process_single_file(file_path)
                                            self.progress['value'] = i + 1
                                            self.root.update_idletasks()
                                            success_count += 1
                                            self.log_message(f"文件处理成功:{os.path.basename(file_path)}")
                                        except PermissionError:
                                            fail_count += 1
                                            error_msg = f"权限错误:无法访问文件{os.path.basename(file_path)},请确保文件未被打开且有读写权限"
                                            messagebox.showerror("权限错误", error_msg)
                                            self.log_message(error_msg, "ERROR")
                                        except KeyError as e:
                                            fail_count += 1
                                            error_msg = f"列名错误:文件{os.path.basename(file_path)}缺少关键列{str(e)}"
                                            messagebox.showerror("列名错误", error_msg)
                                            self.log_message(error_msg, "ERROR")
                                        except Exception as e:
                                            fail_count += 1
                                            error_msg = f"处理文件{os.path.basename(file_path)}时出错:{str(e)}"
                                            messagebox.showerror("错误", error_msg)
                                            self.log_message(error_msg, "ERROR")
                            
                                    self.progress['value'] = 0
                                    self.status_label.config(text=f"文件处理完成:成功{success_count}个,失败{fail_count}个")
                                    self.log_message(f"文件处理结束:成功{success_count}个,失败{fail_count}个")
                            
                                    if success_count > 0:
                                        messagebox.showinfo("完成",
                                                            f"文件处理完成!成功{success_count}个,失败{fail_count}个n可点击'生成汇总表'按钮创建汇总文件")
                            
                                def find_header_row(self, df):
                                    """动态查找包含'签到状态'的表头行"""
                                    for i, row in df.iterrows():
                                        if '签到状态' in str(row.values):
                                            return i
                                    return None
                            
                                def process_single_file(self, file_path):
                                    """处理单个签到文件"""
                                    # 读取整个文件,用于动态检测表头
                                    if file_path.endswith('.xlsx'):
                                        full_df = pd.read_excel(file_path, header=None)
                                    elif file_path.endswith('.csv'):
                                        full_df = pd.read_csv(file_path, header=None)
                            
                                    # 动态查找表头行
                                    header_row = self.find_header_row(full_df)
                                    if header_row is None:
                                        raise ValueError("未找到包含'签到状态'的表头行,请确认文件格式")
                            
                                    # 用找到的表头行重新读取文件
                                    if file_path.endswith('.xlsx'):
                                        df = pd.read_excel(file_path, header=header_row)
                                    elif file_path.endswith('.csv'):
                                        df = pd.read_csv(file_path, header=header_row)
                            
                                    # 清理列名中的空格和特殊字符
                                    df.columns = [col.strip() for col in df.columns]
                            
                                    # 验证关键列是否存在
                                    required_columns = ['姓名', '学号/工号', '学校', '院系', '专业', '行政班级', '签到状态']
                                    for col in required_columns:
                                        if col not in df.columns:
                                            raise KeyError(col)
                            
                                    # 生成已签统计
                                    df['已签统计'] = df['签到状态'].apply(
                                        lambda x: 1 if str(x).strip() == '已签' else 0 if str(x).strip() in ['教师代签', '迟到', '未参与'] else None
                                    )
                                    # 生成教师代签统计
                                    df['教师代签统计'] = df['签到状态'].apply(
                                        lambda x: 1 if str(x).strip() == '教师代签' else 0 if str(x).strip() in ['已签', '迟到', '未参与'] else None
                                    )
                                    # 生成迟到统计
                                    df['迟到统计'] = df['签到状态'].apply(
                                        lambda x: 1 if str(x).strip() == '迟到' else 0 if str(x).strip() in ['已签', '教师代签', '未参与'] else None
                                    )
                                    # 生成未参与统计
                                    df['未参与统计'] = df['签到状态'].apply(
                                        lambda x: 1 if str(x).strip() == '未参与' else 0 if str(x).strip() in ['已签', '教师代签', '迟到'] else None
                                    )
                                    # 生成最终签到统计(已签/教师代签/迟到都算1,未参与算0)
                                    df['最终签到统计'] = df['签到状态'].apply(
                                        lambda x: 1 if str(x).strip() in ['已签', '教师代签', '迟到'] else 0 if str(x).strip() == '未参与' else None
                                    )
                            
                                    # 保存处理后的文件
                                    try:
                                        output_path = f"{os.path.splitext(file_path)[0]}_统计结果.xlsx"
                                        df.to_excel(output_path, index=False)
                                        self.log_message(f"统计文件已保存:{os.path.basename(output_path)}")
                                    except PermissionError:
                                        # 如果原路径无法写入,让用户选择保存位置
                                        self.log_message(f"原路径无写入权限,弹出保存对话框:{os.path.basename(file_path)}", "WARNING")
                                        output_path = filedialog.asksaveasfilename(
                                            defaultextension=".xlsx",
                                            initialfile=f"{os.path.splitext(os.path.basename(file_path))[0]}_统计结果.xlsx",
                                            filetypes=[("Excel文件", "*.xlsx")]
                                        )
                                        if output_path:
                                            df.to_excel(output_path, index=False)
                                            self.log_message(f"统计文件已保存到指定位置:{os.path.basename(output_path)}")
                                        else:
                                            raise PermissionError("用户取消了文件保存操作")
                            
                                    # 收集汇总数据(包含所有需要的字段)
                                    file_name = os.path.basename(file_path)
                                    for _, row in df.iterrows():
                                        self.summary_data.append({
                                            '姓名': row['姓名'],
                                            '学号/工号': row['学号/工号'],
                                            '学校': row['学校'],
                                            '院系': row['院系'],
                                            '专业': row['专业'],
                                            '行政班级': row['行政班级'],
                                            '文件名': file_name,
                                            '已签统计': row['已签统计'],
                                            '教师代签统计': row['教师代签统计'],
                                            '迟到统计': row['迟到统计'],
                                            '未参与统计': row['未参与统计'],
                                            '最终签到统计': row['最终签到统计']
                                        })
                            
                                def generate_summary_button(self):
                                    """独立的汇总表生成按钮处理函数"""
                                    if not self.summary_data:
                                        messagebox.showwarning("警告", "暂无汇总数据!请先处理文件")
                                        self.log_message("生成汇总表失败:暂无汇总数据", "WARNING")
                                        return
                            
                                    self.log_message("开始生成汇总表...")
                            
                                    try:
                                        summary_df = pd.DataFrame(self.summary_data)
                                        # 按分组对所有统计列求和
                                        final_summary = summary_df.groupby(
                                            ['姓名', '学号/工号', '学校', '院系', '专业', '行政班级']
                                        ).agg({
                                            '已签统计': 'sum',
                                            '教师代签统计': 'sum',
                                            '迟到统计': 'sum',
                                            '未参与统计': 'sum',
                                            '最终签到统计': 'sum'
                                        }).reset_index()
                            
                                        # 重命名列名为"总XXX统计"
                                        final_summary.rename(columns={
                                            '已签统计': '总已签统计',
                                            '教师代签统计': '总教师代签统计',
                                            '迟到统计': '总迟到统计',
                                            '未参与统计': '总未参与统计',
                                            '最终签到统计': '总最终签到统计'
                                        }, inplace=True)
                            
                                        # 保存汇总表
                                        save_path = filedialog.asksaveasfilename(
                                            defaultextension=".xlsx",
                                            initialfile="学习通签到汇总表.xlsx",
                                            filetypes=[("Excel文件", "*.xlsx")]
                                        )
                            
                                        if save_path:
                                            final_summary.to_excel(save_path, index=False)
                                            self.log_message(f"汇总表生成成功:{os.path.basename(save_path)}")
                                            self.log_message(f"汇总数据:共统计{len(final_summary)}名学生的签到情况,包含多维度签到统计")
                                            messagebox.showinfo("成功",
                                                                f"汇总表已保存!n文件路径:{save_path}n共统计{len(final_summary)}名学生n包含:总已签、总教师代签、总迟到、总未参与、总最终签到统计")
                                        else:
                                            self.log_message("用户取消了汇总表保存操作", "WARNING")
                            
                                    except Exception as e:
                                        error_msg = f"生成汇总表时出错:{str(e)}"
                                        messagebox.showerror("错误", error_msg)
                                        self.log_message(error_msg, "ERROR")
                            
                            if __name__ == "__main__":
                                root = tk.Tk()
                                app = AttendanceApp(root)
                                root.mainloop()
                            

                            # 1. 创建仅包含Python 3.10的纯净环境(兼容性最佳,适配tkinter/pandas)
                            conda create -n attendance_tool python=3.10 -y
                             
                            # 2. 激活该环境
                            conda activate attendance_tool
                             
                            # 3. 仅安装必需依赖(无任何冗余,清华源加速)
                            pip install pandas openpyxl pyinstaller -i https://pypi.tuna.tsinghua.edu.cn/simple/
                            

                            # 切换到你的学习通签到工具代码所在文件夹(替换为实际路径)
                            cd C:UsersABCPycharmProjectsAttendanceTool
                             
                            # 用目录模式打包(-D),速度快、体积小、稳定性高;-w隐藏控制台(GUI工具必备)
                            pyinstaller -w -n 学习通签到统计工具 -D 学习通签到统计工具.py
                            

                            打包时间:仅需 1-2 分钟即可完成;
                            exe 位置:dist / 学习通签到统计工具 / 目录下的学习通签到统计工具.exe;
                            体积:约 60-90MB(对比全局环境打包的 1GB+,大幅精简);
                            功能:双击 exe,测试文件导入、签到统计、汇总表生成等核心功能,和原代码完全一致。

                            本文介绍了一款基于Python开发的学习通签到数据统计工具,该工具通过GUI界面实现高效自动化处理,主要解决手动统计Excel/CSV签到表格效率低、易出错的问题。工具核心功能包括:支持单文件/多文件/文件夹导入;自动识别动态表头;多维度统计签到状态(已签、教师代签、迟到、未参与);生成带统计列的单个文件和汇总表。采用tkinter+ttk构建界面,pandas处理数据,具有异常处理、日志记录和进度反馈机制。经过测试验证,工具能准确处理学习通导出的各类签到文件,并通过pyinstaller打包为60-90MB的独立exe程序,方便非技术用户使用。

                            以上就是基于Python开发的学习通签到数据统计工具的详细内容,更多关于Python学习通签到统计的资料请关注风君子博客其它相关文章!

                            您可能感兴趣的文章:

                            • python3 chromedrivers签到的简单实现
                            • 利用Python实现网站自动签到
                            • Python+OpenCV人脸识别签到考勤系统实现(附demo)
                            • 如何利用Python实现自动打卡签到的实践
                            • Python利用Selenium实现网站自动签到功能

                            站内搜索