【Python高级编程】学习通签到统计工具

目录

一、引言

二、第一阶段:需求分析与功能规划

[1. 核心业务需求](#1. 核心业务需求)

[2. 非功能需求](#2. 非功能需求)

三、第二阶段:技术选型

四、第三阶段:架构设计

五、第四阶段:分模块详细开发

[1. 初始化与基础配置(__init__方法)](#1. 初始化与基础配置(__init__方法))

[2. UI 组件开发(create_widgets 方法)](#2. UI 组件开发(create_widgets 方法))

[3. 日志系统开发(log_message/clear_log 方法)](#3. 日志系统开发(log_message/clear_log 方法))

[4. 文件导入模块开发](#4. 文件导入模块开发)

(1)临时文件过滤(is_temp_file)

[(2)单文件 / 批量 / 文件夹导入](#(2)单文件 / 批量 / 文件夹导入)

[5. 核心数据处理模块开发](#5. 核心数据处理模块开发)

(1)动态查找表头行(find_header_row)

(2)单个文件处理(process_single_file)

(3)批量处理文件(process_files)

[6. 汇总表生成模块(generate_summary_button)](#6. 汇总表生成模块(generate_summary_button))

[7. 重置功能(reset_all)](#7. 重置功能(reset_all))

六、第五阶段:测试与优化

[1. 核心测试场景](#1. 核心测试场景)

[2. 关键优化点](#2. 关键优化点)

七、学习通签到统计工具的Python代码完整实现

八、程序运行部分截图展示

九、打包项目

[步骤 1:创建并激活纯净虚拟环境打开 Anaconda Prompt,执行以下命令(复制粘贴即可)](#步骤 1:创建并激活纯净虚拟环境打开 Anaconda Prompt,执行以下命令(复制粘贴即可))

[步骤 2:切换目录并快速打包](#步骤 2:切换目录并快速打包)

[步骤 3:验证结果](#步骤 3:验证结果)

十、总结


一、引言

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

项目体验地址:针对高校教师统计学习通签到数据的通用工具资源-CSDN下载

二、第一阶段:需求分析与功能规划

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

1. 核心业务需求

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

2. 非功能需求

  • 易用性: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 渲染,业务逻辑封装在独立方法中;
  • 异常分层处理:批量处理时捕获单个文件的异常,不影响其他文件处理;
  • 用户反馈闭环:操作→进度条更新→状态提示→日志记录,全程可视化。

五、第四阶段:分模块详细开发

1. 初始化与基础配置(__init__方法)

python 复制代码
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()
  • 初始化主窗口的标题、尺寸,定义核心全局变量;
  • 过滤第三方库的无关警告,避免日志 / 控制台出现冗余信息。

2. UI 组件开发(create_widgets 方法)

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

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

关键实现细节:

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

3. 日志系统开发(log_message/clear_log 方法)

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

python 复制代码
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方法提供日志清空功能,同时记录 "日志已清空" 的操作,保证日志链完整。

4. 文件导入模块开发

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

(1)临时文件过滤(is_temp_file)

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

python 复制代码
def is_temp_file(self, file_path):
    filename = os.path.basename(file_path)
    return filename.startswith('~$') or filename.startswith('.')  # 过滤Excel临时文件/隐藏文件
(2)单文件 / 批量 / 文件夹导入
  • import_single_file:调用filedialog.askopenfilename选择单个文件,校验后加入列表;
  • import_multiple_files:调用askopenfilenames选择多文件,批量校验、统计导入 / 跳过数量;
  • import_folder:调用askdirectory选择文件夹,通过os.walk遍历所有 Excel/CSV 文件,批量导入;
  • 所有导入方法最终调用update_file_list更新 Listbox 显示,并通过日志反馈导入结果。

关键设计:

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

5. 核心数据处理模块开发

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

(1)动态查找表头行(find_header_row)

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

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

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

  1. 读取文件 :根据后缀(xlsx/csv)选择pd.read_excel/read_csv,先读取全量数据找表头;

  2. 校验表头 :若未找到 "签到状态" 行,抛出ValueError

  3. 重新读取 + 列名清理:以找到的表头行重新读取,清理列名空格(避免 "姓名" 和 "姓名" 被识别为不同列);

  4. 关键列校验 :检查是否包含['姓名', '学号/工号', '签到状态']等必填列,缺失则抛出KeyError

  5. 多维度统计

    python 复制代码
    # 已签统计:仅"已签"算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(便于后续排查异常);
  6. 保存统计结果

    • 优先保存到原文件同目录(命名为 "原文件名_统计结果.xlsx");
    • 若原路径无权限(如文件在只读文件夹),弹出保存对话框让用户选择路径;
  7. 缓存汇总数据 :将当前文件的统计结果存入summary_data,为后续汇总表做准备。

(3)批量处理文件(process_files)

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

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

6. 汇总表生成模块(generate_summary_button)

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

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

7. 重置功能(reset_all)

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

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

六、第五阶段:测试与优化

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

1. 核心测试场景

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

2. 关键优化点

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

七、学习通签到统计工具的Python代码完整实现

python 复制代码
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:创建并激活纯净虚拟环境打开 Anaconda Prompt,执行以下命令(复制粘贴即可)

bash 复制代码
# 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/

步骤 2:切换目录并快速打包

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

步骤 3:验证结果

打包时间:仅需 1-2 分钟即可完成;

exe 位置:dist / 学习通签到统计工具 / 目录下的学习通签到统计工具.exe;

体积:约 60-90MB(对比全局环境打包的 1GB+,大幅精简);

功能:双击 exe,测试文件导入、签到统计、汇总表生成等核心功能,和原代码完全一致。

十、总结

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

相关推荐
小二·2 小时前
Python Web 开发进阶实战:可持续计算 —— 在 Flask + Vue 中构建碳感知应用(Carbon-Aware Computing)
前端·python·flask
Java程序员威哥2 小时前
【包教包会】SpringBoot依赖Jar指定位置打包:配置+原理+避坑全解析
java·开发语言·spring boot·后端·python·微服务·jar
小饼干超人2 小时前
如何兼容不同版本的 scikit-learn(sklearn)库,统一获取“均方根误差(RMSE)”的计算函数
python·scikit-learn·sklearn
Java程序员威哥2 小时前
Java微服务可观测性实战:Prometheus+Grafana+SkyWalking全链路监控落地
java·开发语言·python·docker·微服务·grafana·prometheus
UR的出不克2 小时前
基于PyTorch的MNIST手写数字识别系统 - 从零到实战
人工智能·python·数字识别
one____dream2 小时前
【算法】大整数数组连续进位
python·算法
one____dream2 小时前
【算法】合并两个有序链表
数据结构·python·算法·链表
程序员敲代码吗2 小时前
持续集成/持续部署(CI/CD) for Python
jvm·数据库·python
人工智能AI技术2 小时前
【Agent从入门到实践】16 接口与网络:API调用、HTTP请求,Agent与外部交互的基础
人工智能·python