☰
Python在线课堂考勤系统实战:Flask+OpenCV+Pandas
2026/10/26 7:59:32 网站建设 项目流程

简介:这份资源是一套基于Python的在线课堂考勤系统完整项目源码,面向具备一定Python基础、希望将深度学习落地到教育场景的开发者与学习者。项目以卷积神经网络为核心,结合OpenCV与TensorFlow或PyTorch完成人脸特征提取与识别,并借助Flask或Django搭建可视化界面,覆盖数据收集、预处理、模型构建、训练优化到测试部署的完整流程,可帮助读者理解人脸考勤从算法到系统的实现路径。压缩包共98个文件,约409.43MB,包含22个py源码、28张jpg样本图、15个xml配置、6张png界面图、3个pt模型权重及csv、dat等数据文件,目录按功能模块划分,便于对照学习。目前已有1461人学习下载,适合作为课程设计、毕业设计或AI入门项目的参考,读者可从中获取模型训练脚本、界面代码与项目结构组织思路。

1. 在线课堂考勤系统:为什么用 Python 做,比你想的更值得投入

线上课上了三年,最让老师崩溃的不是网络卡顿,而是点名。一个 60 人的直播间,挨个喊名字要花 8 分钟,中途还有人挂机、代刷、切屏。教务那边要的是可导出、可追溯、能对接课表的考勤记录,而不是一张截图。基于 Python 的在线课堂考勤系统,解决的就是这件事:把「谁在、谁不在、谁中途跑了」变成一条条带时间戳的数据,自动落库、自动统计、自动出报表。

它适合三类人:一是培训机构的技术负责人,想低成本搭一套能跑在自己服务器上的考勤模块;二是高校里带实验课的助教,需要按周导出出勤率;三是正在学 Python、想找一个有真实业务闭环的练手项目的人。这个方向的技术栈不玄学,核心就是 Flask/FastAPI 做接口、OpenCV 做人脸或活体校验、SQLite/MySQL 存记录、Pandas 出报表。下面按「先跑通最小闭环,再补防作弊,最后做报表」的顺序拆开讲,每一步都能直接抄。

2. 最小可用闭环:从签到接口到落库的完整链路

2.1 为什么选 Flask + SQLite 起步,而不是一上来就上微服务

很多教程一上来就教你搭 Django + Redis + Celery,结果环境配了两天,签到接口还没跑通。我的血泪经验是:考勤系统的核心链路只有三步——学生发起签到、服务端校验身份和时间窗口、写入数据库。这三步用 Flask 加 SQLite 在 100 行以内就能跑通,等并发真的上来了再换 PostgreSQL 和消息队列也不迟。

选 Flask 的理由很具体:路由写法直白,和前端对接时调试成本低;SQLite 零配置,一个文件就是数据库,方便你先把业务逻辑跑顺。等你要部署到多台机器、需要并发写入了,再把DATABASE配置换成 MySQL 的连接串,代码改动不超过十行。这就是「先跑通再优化」的落地路径,别被架构图吓住。

2.2 签到接口的最小实现与参数说明

下面这段代码是一个可运行的最小签到服务,包含发起签到和提交签到两个接口。你把它保存为app.py,装好 Flask 就能直接跑。

from flask import Flask, request, jsonify import sqlite3, time, uuid app = Flask(__name__) DB = "attendance.db" def init_db(): conn = sqlite3.connect(DB) conn.execute(""" CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, course_id TEXT NOT NULL, start_ts INTEGER NOT NULL, end_ts INTEGER NOT NULL )""") conn.execute(""" CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, student_id TEXT NOT NULL, sign_ts INTEGER NOT NULL, status TEXT NOT NULL, UNIQUE(session_id, student_id) )""") conn.commit() conn.close() @app.route("/session/open", methods=["POST"]) def open_session(): data = request.get_json() course_id = data["course_id"] duration = int(data.get("duration", 300)) # 默认 5 分钟签到窗口 sid = str(uuid.uuid4()) now = int(time.time()) conn = sqlite3.connect(DB) conn.execute("INSERT INTO sessions VALUES (?,?,?,?)", (sid, course_id, now, now + duration)) conn.commit(); conn.close() return jsonify({"session_id": sid, "expire_in": duration}) @app.route("/session/sign", methods=["POST"]) def sign(): data = request.get_json() sid, stu = data["session_id"], data["student_id"] now = int(time.time()) conn = sqlite3.connect(DB) row = conn.execute("SELECT end_ts FROM sessions WHERE session_id=?", (sid,)).fetchone() if not row: return jsonify({"ok": False, "msg": "签到不存在"}), 404 if now > row[0]: return jsonify({"ok": False, "msg": "签到已截止"}), 403 try: conn.execute("INSERT INTO records(session_id,student_id,sign_ts,status)" " VALUES (?,?,?,?)", (sid, stu, now, "present")) conn.commit() return jsonify({"ok": True}) except sqlite3.IntegrityError: return jsonify({"ok": False, "msg": "请勿重复签到"}), 409 finally: conn.close() if __name__ == "__main__": init_db() app.run(host="0.0.0.0", port=5000)

逻辑说明:open_session负责生成一个带有效期的签到会话,duration参数控制窗口长度,默认 300 秒,你可以按课堂节奏改成 60 或 120。sign接口先查会话是否存在、是否过期,再用数据库的UNIQUE约束挡住重复签到——这比在代码里先查后插更可靠,因为并发下「查了再插」会有竞态。status字段先固定为present,后面做迟到判定时会用到。

参数上最需要留意的是duration。太短学生来不及反应,太长等于没考勤。我一般按「老师喊开始后 90 秒内有效」来设,配合前端倒计时,出勤率数据最真实。

2.3 用 curl 验证接口是否真的通了

写完接口别急着写前端,先用命令行把链路走一遍,确认数据库里真的落了数据。

# 开启一个 120 秒的签到 curl -X POST http://127.0.0.1:5000/session/open \ -H "Content-Type: application/json" \ -d '{"course_id":"CS101","duration":120}' # 拿到 session_id 后提交签到 curl -X POST http://127.0.0.1:5000/session/sign \ -H "Content-Type: application/json" \ -d '{"session_id":"上一步返回的id","student_id":"2023001"}' # 直接查库确认 sqlite3 attendance.db "SELECT * FROM records;"

如果第二条命令返回{"ok": true},第三条能看到记录,最小闭环就成了。这一步的意义在于:把「接口通不通」和「前端好不好看」解耦,出问题时你能立刻定位是服务端还是页面。很多新手跳过这步直接写 Vue,结果报错时两头猜,浪费一整天。

3. 防代签与活体校验:OpenCV 人脸比对怎么接进考勤流程

3.1 纯账号密码签到为什么一定会被代刷

只要签到靠学号加密码,就一定会出现「一个宿舍互相代签」。这不是学生坏,是机制有漏洞。要堵住它,得让签到动作绑定「本人当下在场」这个事实。常见做法有三档:一是签到码加 IP 限制,成本最低但挡不住同网段代签;二是人脸比对,需要学生提前上传底照;三是活体检测,要求眨眼或转头,防照片翻拍。落地时我建议从第二档起步,因为 OpenCV 的人脸比对已经足够成熟,代码量可控。

3.2 用 OpenCV 做 1:1 人脸比对的落地代码

思路是:学生首次录入一张底照存到faces/学号.jpg,签到时前端抓一张当前照片传上来,服务端用 OpenCV 提取特征做相似度比对,超过阈值才写记录。

import cv2, numpy as np, os # 使用 OpenCV 自带的 LBPH 人脸识别器,无需额外下载模型 recognizer = cv2.face.LBPHFaceRecognizer_create() CASCADE = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml") def extract_face(img_path): img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = CASCADE.detectMultiScale(gray, 1.2, 5) if len(faces) == 0: return None x, y, w, h = faces[0] return cv2.resize(gray[y:y+h, x:x+w], (200, 200)) def verify(student_id, live_path, threshold=70): base_path = f"faces/{student_id}.jpg" if not os.path.exists(base_path): return False, "未录入底照" base = extract_face(base_path) live = extract_face(live_path) if base is None or live is None: return False, "未检测到人脸" # 训练一个只含该学生底照的临时模型 recognizer.train([base], np.array([0])) label, confidence = recognizer.predict(live) # LBPH 的 confidence 越小越像,低于阈值判定为同一人 return confidence < threshold, f"置信度 {confidence:.1f}"

逻辑说明:extract_face负责把人脸区域裁出来并统一到 200x200,尺寸统一是比对稳定的前提。verify里每次只训练一张底照,虽然看起来「浪费」,但避免了维护全局模型文件的麻烦,适合学生数量在几百人级别的课堂。threshold是关键参数,LBPH 的confidence是距离值,越小越像,70 是我在室内光线下的经验值;光线偏暗时放宽到 80,强逆光时收紧到 60。

提示:cv2.face需要安装opencv-contrib-python,只装opencv-python会报模块不存在。这是新手最常翻车的地方。

3.3 把校验接进签到接口的改法与失败处理

把上一节的verify嵌进sign接口,在写库前加一道校验:

ok, msg = verify(stu, live_path) if not ok: return jsonify({"ok": False, "msg": f"人脸校验未通过:{msg}"}), 403

失败时不要只返回「失败」,要把msg带出去,前端才能提示学生「光线太暗」还是「未录入底照」。另外要留一条人工通道:老师端可以手动把某条记录改成present,防止设备问题导致学生明明到了却签不上。这条通道要有日志,谁改的、什么时候改的,都记进records的备注字段,否则考勤数据就失去了公信力。

4. 考勤数据统计与报表导出:Pandas 按周出勤率怎么算

4.1 出勤率的口径必须先定死,否则报表天天吵架

做报表前先和教务对齐三个口径:迟到怎么算、请假算不算出勤、中途退出算不算缺勤。我的做法是在records表里用status字段区分present、late、leave、absent,签到时间超过开始后 60 秒记late,请假由老师端单独录入leave。口径定死后,统计代码只认字段,不再做模糊判断,报表就不会每次跑出来数字不一样。

4.2 用 Pandas 生成按周出勤率报表

import pandas as pd, sqlite3 conn = sqlite3.connect("attendance.db") df = pd.read_sql(""" SELECT r.student_id, r.status, s.course_id, r.sign_ts FROM records r JOIN sessions s ON r.session_id = s.session_id """, conn) df["date"] = pd.to_datetime(df["sign_ts"], unit="s") df["week"] = df["date"].dt.isocalendar().week # 出勤 = present + late,请假不计入分母 df["attended"] = df["status"].isin(["present", "late"]) report = df.groupby(["course_id", "week", "student_id"]).agg( total=("status", "count"), attended=("attended", "sum") ).reset_index() report["rate"] = (report["attended"] / report["total"] * 100).round(1) report.to_excel("weekly_report.xlsx", index=False) print(report.head())

逻辑说明:isocalendar().week按 ISO 周编号分组,跨年时不会串周。attended把present和late都算作出勤,leave和absent不算,这样请假不会拉低出勤率,符合多数学校口径。rate保留一位小数,导出 Excel 后老师可以直接贴进教务系统。如果课程多,把course_id加进groupby就能一次出全部课程的报表。

4.3 报表里最容易被忽略的两个坑

第一个坑是时区。sign_ts存的是 Unix 时间戳,pd.to_datetime默认按 UTC 解析,如果你服务器在东八区,报表日期会整体偏一天。解决办法是加utc=True后再tz_convert("Asia/Shanghai")。第二个坑是重复签到。虽然数据库有UNIQUE约束,但如果你后来改了表结构把约束去掉了,total会被重复记录撑大,出勤率虚高。跑报表前先SELECT session_id, student_id, COUNT(*) FROM records GROUP BY 1,2 HAVING COUNT(*)>1查一遍,有结果就说明约束失效了。

5. 部署与并发排查:签到高峰期接口超时的 4 个常见问题

5.1 现象:签到开始后 10 秒内大量请求超时

原因:Flask 自带的开发服务器是单线程的,60 个人同时点签到,请求排队,后面的直接超时。解决:用gunicorn起多进程,命令是gunicorn -w 4 -b 0.0.0.0:5000 app:app,-w 4表示 4 个 worker,按 CPU 核数设,一般核数*2+1。SQLite 在并发写入时会锁库,worker 多了反而报database is locked,所以要么把 worker 控制在 2 到 4 个,要么直接换 PostgreSQL。

5.2 现象:人脸比对接口偶发返回「未检测到人脸」

原因:前端抓拍的图片分辨率太低,或者学生背光。解决:前端抓拍时限制最短边不低于 480 像素,并在页面上提示「请正对光源」。服务端在extract_face里加一次直方图均衡化cv2.equalizeHist(gray),能明显提升暗光下的检出率。这个改动只有一行,但效果立竿见影。

5.3 现象:报表导出的 Excel 打开是乱码

原因:to_excel默认用 UTF-8,部分旧版 Excel 对中文列名识别有问题。解决:把列名改成英文再导出,或者在导出后用openpyxl显式指定编码。更省事的做法是同时导出一份 CSV,用encoding="utf-8-sig",Excel 双击打开就不会乱码。

5.4 现象:签到记录里出现同一学生同一秒的两条数据

原因:前端按钮没做防抖,学生连点两下,两个请求几乎同时到达,而数据库的UNIQUE约束在极端并发下可能因为事务隔离级别没拦住。解决:前端按钮点击后立即置灰,服务端在sign接口入口加一个基于session_id + student_id的内存锁,用threading.Lock或 Redis 的SETNX都行。双保险之后,这个问题基本绝迹。

注意:排查并发问题时,先看gunicorn的访问日志,确认是请求没进来还是进来了处理慢。前者查网络和端口,后者查数据库锁和代码里的同步阻塞调用。别一上来就改代码,方向错了越改越乱。

6. 进阶技巧:用签到时间序列反推课堂专注度

考勤做到后面,你会发现「在不在」只是最浅的一层。真正有价值的是签到时间分布:如果 80% 的学生在签到窗口开启后 5 秒内完成,说明大家盯着屏幕;如果签到集中在截止前 10 秒,说明很多人是收到提醒才回来。这个分布本身就是课堂专注度的代理指标。

具体做法是在records里多存一个delay字段,记录sign_ts - session_start_ts。然后用 Pandas 画一张按分钟的签到人数直方图:

df["delay_min"] = df["delay"] // 60 dist = df.groupby("delay_min").size() dist.plot(kind="bar", title="签到时间分布")

如果分布是「前高后低」,课堂节奏正常;如果是「前低后高」,下次课可以把签到窗口缩短到 60 秒,逼学生及时响应。这个技巧我用了两个学期,配合出勤率一起看,比单纯的点名数字有说服力得多。

另一个进阶方向是把考勤和课表打通。用argparse写一个命令行工具,老师输入课程号和日期,自动查出该时段应到名单和实到名单,差集就是缺勤。这样教务要数据时,你一条命令就能出结果,不用再翻后台。

import argparse, sqlite3 parser = argparse.ArgumentParser() parser.add_argument("--course", required=True) parser.add_argument("--date", required=True) # 格式 2024-03-01 args = parser.parse_args() # 后续按 course + date 查 sessions 和 records,输出缺勤名单

参数说明:--course传课程号,--date传日期,内部把日期转成当天的起止时间戳再查。这个工具的价值在于把「临时要数据」这件事从 10 分钟压缩到 10 秒,教务对你的评价会完全不一样。

最后说个我自己的习惯:每次上线新版本前,先用 10 个测试账号把签到、人脸、报表三条链路各跑一遍,确认数据库里records表的status分布符合预期再放给学生用。考勤数据一旦脏了,事后清洗的成本远高于事前多跑一遍。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询