基于Python的匿名成绩登记与学情分析系统:Flask+Pandas实现

发布时间:2026/10/25 8:00:38
基于Python的匿名成绩登记与学情分析系统:Flask+Pandas实现 简介这是一套面向Python初学者与教学管理者的匿名考试成绩登记与学情分析系统源码适合作为课程设计、毕业设计或教学工具二次开发参考。项目以Python脚本承担数据处理、事件调度与主控逻辑配合HTML页面、JavaScript交互与CSS样式构建前端界面并借助SQLite3数据库实现成绩的持久化存储同时通过日志配置与匿名化设计兼顾数据安全与隐私保护。资源包共24个文件涵盖6个HTML页面、5个Python脚本、5个JavaScript文件、2个CSS样式表以及数据库、配置、字体与说明文档等压缩包约1.64MB目录结构清晰便于按模块阅读与调试。目前已有303人学习下载。读者可从中获取前后端协同的完整实现思路、匿名成绩录入与学情分析的落地方法以及数据库选型、日志管理与前端组件集成的实践经验适合用来快速理解小型教育管理系统的整体架构与开发流程。1. 匿名成绩登记系统为什么教务场景需要它以及这套 Python 方案能解决什么每到期中期末教务老师最头疼的不是改卷而是把成绩从各个 Excel 里汇总、核对、再分发给不同班级。传统做法是让课代表用共享表格填分数结果学号姓名全暴露谁考了多少一目了然学生之间互相打听、家长群里截图传播隐私问题年年出。更麻烦的是成绩收上来之后还要手动算平均分、及格率、分数段分布一个班五十人还能忍一个年级上千条记录光透视表就能折腾一下午。这套「基于 Python 的匿名考试成绩登记与学情分析」方案核心思路就两件事登记环节用匿名编号替代真实身份分析环节用 Pandas 自动出统计报表。它适合中小学教务、培训机构班主任、高校助教这类需要频繁处理成绩但又没有专业教务系统权限的角色。整套东西不依赖数据库一个 Flask 服务加几个 Python 脚本就能跑起来源码结构清晰改起来也方便。下面从环境搭建到匿名映射、再到学情分析把能复现的步骤和参数都拆开讲。2. 环境准备与项目骨架从零把 Flask Pandas 跑起来2.1 依赖选型为什么用 Flask 而不是 Django成绩登记这种场景功能边界很清楚一个表单页、一个提交接口、一个分析页。Django 自带 admin、ORM、模板引擎对这个小需求来说太重了光 settings.py 就要改半天。Flask 的轻量在这里是优势路由和视图函数一一对应新手看一遍就能改。核心依赖就四个库用途安装命令Flask提供 Web 服务和表单接口pip install flaskPandas成绩数据清洗与统计pip install pandasopenpyxl读写 Excel 文件pip install openpyxlhashlib生成匿名编号标准库无需安装Python 版本建议 3.8 以上Pandas 在 3.8 之后的版本对 Excel 读取的兼容性更稳。如果你用的是 python 3.8注意 openpyxl 要装 3.0.9 之后的版本否则读 xlsx 会报BadZipFile。# 创建虚拟环境避免污染全局包 python -m venv venv # Windows 激活 venv\Scripts\activate # macOS / Linux 激活 source venv/bin/activate # 一次性安装依赖 pip install flask pandas openpyxl装完之后用pip list确认一下版本Flask 2.x 和 3.x 在路由写法上略有差异下面代码以 Flask 2.3 为准3.x 也能跑。2.2 项目目录结构四个文件撑起整个系统不要一上来就搞蓝图、搞工厂模式这个体量用不上。我一般会这样组织score_system/ ├── app.py # Flask 主程序路由和接口 ├── anonymize.py # 匿名编号生成与映射 ├── analysis.py # 学情分析统计逻辑 ├── templates/ │ ├── index.html # 成绩录入页 │ └── report.html # 分析报表页 └── data/ ├── raw_scores.xlsx # 原始成绩含真实身份 └── anon_map.csv # 匿名映射表单独存放关键点是anon_map.csv必须和raw_scores.xlsx分开存。很多翻车案例就是把映射表和分析结果放同一个目录结果打包发给学生的时候连映射一起发出去了匿名等于没做。2.3 最小可运行版本先把 Flask 跑起来# app.py from flask import Flask, render_template, request, redirect, url_for import pandas as pd import os app Flask(__name__) DATA_DIR os.path.join(os.path.dirname(__file__), data) RAW_FILE os.path.join(DATA_DIR, raw_scores.xlsx) app.route(/) def index(): 渲染成绩录入表单页 return render_template(index.html) app.route(/submit, methods[POST]) def submit(): 接收表单提交追加写入 Excel # 从表单取字段字段名要和 HTML 里的 name 属性一致 student_id request.form.get(student_id, ).strip() name request.form.get(name, ).strip() score request.form.get(score, ).strip() # 基础校验学号、姓名、分数都不能为空 if not student_id or not name or not score: return 字段不完整请返回重填, 400 try: score_val float(score) except ValueError: return 分数必须是数字, 400 # 如果文件不存在就新建存在就追加 if os.path.exists(RAW_FILE): df pd.read_excel(RAW_FILE) else: df pd.DataFrame(columns[student_id, name, score]) new_row pd.DataFrame([{student_id: student_id, name: name, score: score_val}]) df pd.concat([df, new_row], ignore_indexTrue) df.to_excel(RAW_FILE, indexFalse) return redirect(url_for(index)) if __name__ __main__: # debugTrue 仅用于开发上线要关掉 app.run(debugTrue, host0.0.0.0, port5000)这段代码的逻辑很直白/返回录入页面/submit接收 POST 请求把学号、姓名、分数追加到 Excel。参数上注意host0.0.0.0是让同局域网的其他机器也能访问方便课代表在自己电脑上填port5000是 Flask 默认端口如果被占用改成 5001 即可。debugTrue会在代码改动后自动重启但生产环境必须关掉否则报错页面会暴露源码路径。对应的templates/index.html最简版本!DOCTYPE html html headmeta charsetutf-8title成绩录入/title/head body h2匿名成绩登记/h2 form action/submit methodpost label学号input typetext namestudent_id required/labelbr label姓名input typetext namename required/labelbr label分数input typenumber namescore step0.5 required/labelbr button typesubmit提交/button /form /body /htmlstep0.5允许输入 0.5 的倍数如果你们学校分数只取整数改成step1。required是浏览器层面的必填校验但后端仍然要再校验一次因为用户可以绕过前端直接发请求。3. 匿名编号生成把学号姓名从成绩表里剥离出去3.1 匿名映射的原理哈希加盐而不是简单替换有些教程教你直接把学号替换成 1、2、3 这样的序号问题是序号和学号一一对应稍微懂点规律的人拿到两份表就能反推。更稳的做法是用哈希函数加盐把「学号 姓名 盐值」算出一个固定长度的字符串作为匿名编号。# anonymize.py import hashlib import pandas as pd import os # 盐值每个学校或每个学期换一次不要写死在代码里提交到仓库 SALT your_school_2024_secret def make_anon_id(student_id: str, name: str) - str: 根据学号和姓名生成匿名编号取哈希前 10 位 raw f{student_id}|{name}|{SALT} return hashlib.sha256(raw.encode(utf-8)).hexdigest()[:10] def build_anon_map(raw_file: str, map_file: str): 读取原始成绩生成匿名映射表并保存 df pd.read_excel(raw_file) # 去重同一个学生可能多次提交只保留一条映射 df_unique df[[student_id, name]].drop_duplicates() df_unique[anon_id] df_unique.apply( lambda row: make_anon_id(str(row[student_id]), str(row[name])), axis1 ) df_unique.to_csv(map_file, indexFalse, encodingutf-8-sig) return df_uniqueSALT这个参数是安全性的关键。如果盐值泄露别人拿到匿名编号和学号列表就能暴力比对。实际部署时把盐值放到环境变量里代码里用os.environ.get(SCORE_SALT)读取不要硬编码。hexdigest()[:10]取前 10 位碰撞概率在几千人规模下可以忽略如果你们学校上万人取 12 位更保险。3.2 把匿名编号回填到成绩表生成映射之后下一步是把匿名编号合并回成绩数据然后丢掉学号和姓名列。def anonymize_scores(raw_file: str, map_file: str, output_file: str): 生成不含真实身份的匿名成绩表 df pd.read_excel(raw_file) mapping pd.read_csv(map_file, dtype{student_id: str}) # 统一 student_id 类型避免 int 和 str 合并失败 df[student_id] df[student_id].astype(str) mapping[student_id] mapping[student_id].astype(str) merged df.merge(mapping[[student_id, anon_id]], onstudent_id, howleft) # 只保留匿名编号和分数丢掉姓名和学号 anon_df merged[[anon_id, score]] anon_df.to_excel(output_file, indexFalse) return anon_df这里有个血泪经验student_id在 Excel 里经常被存成数字Pandas 读进来是 int64而映射表里是字符串直接 merge 会报You are trying to merge on int64 and object columns。解决办法就是两边都astype(str)这一步不做后面分析全是 NaN。3.3 匿名编号的验证确保没有遗漏和重复生成完匿名表之后跑一段校验代码确认三件事匿名编号数量等于学生数量、没有空值、没有重复。def validate_anon(anon_file: str): 校验匿名成绩表的完整性 df pd.read_excel(anon_file) total len(df) unique_ids df[anon_id].nunique() null_count df[anon_id].isna().sum() print(f总记录数{total}) print(f唯一匿名编号数{unique_ids}) print(f空匿名编号数{null_count}) if null_count 0: print(警告存在未匹配到匿名编号的记录检查学号格式是否一致) if unique_ids total: print(提示存在重复提交建议按学号去重后再分析)如果null_count大于 0八成是学号格式问题比如原始表里学号带空格或者有前导零被 Excel 吃掉了。解决办法是在读取时加dtype{student_id: str}并且在录入环节就用文本格式存学号。4. 学情分析核心逻辑平均分、及格率、分数段一次算清4.1 用 Pandas 做描述性统计五个指标覆盖大部分需求学情分析不需要多复杂的模型教务最关心的就是平均分、最高分、最低分、及格率、优秀率这五个。Pandas 的describe()能给一部分但及格率和优秀率要自己算。# analysis.py import pandas as pd def basic_stats(anon_file: str, pass_line: float 60, excellent_line: float 85): 计算基础统计指标 df pd.read_excel(anon_file) scores df[score].dropna() stats { 总人数: len(scores), 平均分: round(scores.mean(), 2), 最高分: scores.max(), 最低分: scores.min(), 标准差: round(scores.std(), 2), 及格率: round((scores pass_line).sum() / len(scores) * 100, 2), 优秀率: round((scores excellent_line).sum() / len(scores) * 100, 2), } return statspass_line和excellent_line做成参数是因为不同科目、不同学段的及格线和优秀线不一样。小学可能 60 分及格、90 分优秀高中可能 60 分及格、85 分优秀。做成参数之后换一个科目只需要改调用时的实参不用动函数体。4.2 分数段分布用 cut 做等距分组分数段分布是学情分析里最能看出问题的部分。哪个分数段扎堆说明教学难度可能偏了。def score_distribution(anon_file: str, binsNone): 按分数段统计人数和占比 if bins is None: # 默认分段0-59, 60-69, 70-79, 80-89, 90-100 bins [0, 60, 70, 80, 90, 101] labels [不及格, 及格, 中等, 良好, 优秀] df pd.read_excel(anon_file) df[段位] pd.cut(df[score], binsbins, labelslabels, rightFalse) dist df[段位].value_counts().reindex(labels, fill_value0) dist_pct (dist / dist.sum() * 100).round(2) result pd.DataFrame({人数: dist, 占比(%): dist_pct}) return resultrightFalse表示左闭右开也就是 60 分算进「及格」而不是「不及格」。这个参数很关键默认rightTrue的话 60 分会被分到 0-59 那一段教务看到会来找你。bins的最后一个值写 101 而不是 100是因为pd.cut的右边界是开区间写 100 的话满分 100 的学生会被排除在外。4.3 把分析结果渲染成报表页统计逻辑有了接下来接到 Flask 路由里渲染成 HTML 表格。# app.py 追加部分 from analysis import basic_stats, score_distribution app.route(/report) def report(): 渲染学情分析报表 anon_file os.path.join(DATA_DIR, anon_scores.xlsx) if not os.path.exists(anon_file): return 尚未生成匿名成绩表请先运行 anonymize.py, 404 stats basic_stats(anon_file) dist score_distribution(anon_file) # 把 DataFrame 转成 HTML 表格方便模板直接渲染 dist_html dist.to_html(classestable, border0) return render_template(report.html, statsstats, dist_htmldist_html)to_html生成的表格自带border属性传 0 去掉边框样式交给 CSS 控制。classestable方便你在 CSS 里统一调样式。如果数据量大to_html会生成很长的字符串但几千条记录完全没问题。对应的report.html!DOCTYPE html html headmeta charsetutf-8title学情分析报表/title/head body h2学情分析报表/h2 table {% for key, value in stats.items() %} trtd{{ key }}/tdtd{{ value }}/td/tr {% endfor %} /table h3分数段分布/h3 {{ dist_html | safe }} /body /html| safe是告诉 Jinja2 这段 HTML 是安全的不要转义。不加的话页面上会显示一堆table标签文本而不是表格。5. 避坑与排查匿名成绩系统最容易翻车的五个地方5.1 现象分析结果里出现大量 NaN平均分算出来是 nan原因匿名映射合并时student_id类型不一致Excel 读进来是 intCSV 读进来是 strmerge 之后匹配不上anon_id全是 NaN再往后score也跟着丢。解决所有涉及student_id的读取都加dtype{student_id: str}merge 之前再astype(str)一次。养成习惯学号永远当字符串处理不要让它变成数字。5.2 现象60 分的学生被算进不及格教务说统计错了原因pd.cut默认rightTrue区间是左开右闭60 分落在 (0, 60] 这个区间里被归到不及格段。解决显式传rightFalse让区间变成左闭右开60 分归入及格段。这个参数不写默认行为就是错的而且错得很隐蔽不仔细核对根本发现不了。5.3 现象多个课代表同时提交后提交的覆盖了先提交的原因pd.read_excel读进来追加一行再to_excel写回去。两个请求同时执行时A 读了文件B 也读了同一个文件A 写完 B 再写A 的数据就丢了。解决小规模场景加一个文件锁就行。用threading.Lock()包住读写操作或者更简单粗暴一点每个提交生成一个独立的 CSV 文件分析时再合并。Flask 默认是单进程多线程加锁能解决大部分并发问题。5.4 现象Excel 里的学号前导零丢了001 变成 1原因Excel 默认把纯数字字符串当数字处理前导零被吃掉。Pandas 读的时候如果没指定 dtype也会跟着丢。解决录入环节就用文本格式存学号在 Excel 里先把学号列设成文本格式再填。代码层面读取时加dtype{student_id: str}双保险。5.5 现象部署到服务器后app.run(debugTrue)报错页面暴露了文件路径原因开发模式下的 Werkzeug 调试器会在浏览器里显示完整的调用栈和源码片段包括服务器上的绝对路径。解决上线前把debug改成False用gunicorn或waitress这类 WSGI 服务器来跑。如果只是校内局域网用至少也要把debug关掉host改成具体的内网 IP 而不是0.0.0.0。6. 进阶技巧用箱线图快速定位异常分数和班级差异基础统计跑通之后下一步值得做的是可视化。不用搞太复杂的图表一个箱线图就能看出很多问题哪个班的分数分布更集中、有没有异常低分、中位数差距大不大。import matplotlib matplotlib.use(Agg) # 无图形界面服务器必须加这行 import matplotlib.pyplot as plt import pandas as pd # 中文字体设置Windows 用 SimHeimacOS 用 Arial Unicode MS plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False def plot_box(anon_file: str, group_file: str, output_png: str): 按班级分组画箱线图 scores pd.read_excel(anon_file) groups pd.read_csv(group_file, dtype{anon_id: str}) merged scores.merge(groups, onanon_id, howleft) fig, ax plt.subplots(figsize(10, 6)) merged.boxplot(columnscore, byclass_name, axax) ax.set_title(各班成绩分布箱线图) ax.set_ylabel(分数) plt.suptitle() # 去掉 pandas 自动加的副标题 plt.tight_layout() plt.savefig(output_png, dpi150) plt.close()matplotlib.use(Agg)这行是给没有显示器的服务器用的不加会报no display name错误。plt.suptitle()是去掉 Pandas 自动加的那行「Boxplot grouped by class_name」不然图上方会多一行字。dpi150保证导出的图片在 Word 里插入后不模糊。箱线图看完之后如果发现某个班的中位数明显偏低可以进一步用groupby算各班均分def class_compare(anon_file: str, group_file: str): 按班级对比平均分和及格率 scores pd.read_excel(anon_file) groups pd.read_csv(group_file, dtype{anon_id: str}) merged scores.merge(groups, onanon_id, howleft) result merged.groupby(class_name)[score].agg( 人数count, 平均分mean, 及格率lambda x: (x 60).sum() / len(x) * 100 ).round(2) return result.sort_values(平均分, ascendingFalse)agg里用 lambda 算及格率比先分组再循环简洁得多。sort_values按平均分降序排一眼就能看出哪个班考得好。这套东西跑下来从录入到出报表一个年级的数据半小时内能搞定比手动透视表快得多而且匿名映射单独存放发给学生看的只有匿名编号和分数隐私问题也解决了。我自己踩过最深的坑是盐值硬编码在代码里然后传了 Git 仓库后来改成环境变量才踏实。如果你打算把这套东西长期用下去盐值管理、映射表权限、定期清理原始数据这三件事比代码本身更值得花时间。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询