
简介面向计算机相关专业毕业设计、课程设计与在线教育场景提供一套基于 Python/Django 的主观题自动阅卷系统完整工程。系统借助 NLP 与深度学习完成文本预处理、特征提取、模型训练与评分结果可视化支持简答题、填空题等常见主观题的自动识别与智能打分并附带常见错误纠正和反馈建议机制可用于快速搭建原型或二次开发。压缩包共 315 个文件、约 3.13MB包含 28 个 Python 源码、16 个 HTML 页面、36 个 JS 与 20 个 CSS 前端资源另有 44 个 pyc 编译文件、SQL 数据库脚本以及大量 gif 演示图片能够反映项目目录结构与前后端实现思路。已有 1430 人学习下载。整套资料适合作为毕业设计选题参考既可直接运行体验核心流程也能围绕评分模型与接口展开扩展。1. 主观题自动阅卷它替你干了哪三件事改过简答题的人都有体会批改是重复劳动判断标准又很难统一。这套基于 Python Django 的主观题自动阅卷系统目标就是填这个坑学生在线交卷后系统自动识别题目类型跑一遍文本预处理和相似度计算给出分数和一段有针对性的反馈。它不是个黑匣子源码包里能看清单词怎么切、特征怎么提、分数怎么算。最适合的场景是毕业设计其次是在线教育平台的功能原型。做毕设拿它做底子省掉的不只是从头设计数据库的时间还有调通整个评分流程的试错成本。入门门槛不高会 Django 基础视图和 Jieba 分词就能改出自己的一套。2. 核心管线拆解五步实现文本预处理、特征提取与相似度评分2.1 系统整体架构与数据流整个系统的数据流是这样的学生提交答案 → 题型识别 → 文本预处理 → 特征提取 → 相似度/关键词评分 → 结果入库 → 前端展示。题型识别决定了后续评分策略选择题和填空题直接做字符串比对简答题和论述题必须走 NLP 管线。Django 的 MVT 结构在这里分得比较清楚。Model 层管题目表、学生作答表和评分记录表View 层是评分核心逻辑接收请求、调用评分函数、返回 JSONTemplate 层是答题页面和结果页面。项目正文里那一串 CSS 文件bootstrap.min.css、layui.css、style.default.css 等实际上是模板层的前端资源说明这套代码已经带了完整的前台答题页和后台管理页不是只有后端接口的教学骨架。2.2 题型自动识别与评分策略分派题型识别是实现自动评分的第一步。源码里用一个字段标识题目类型同时用请求参数作为兜底判断。每个题目在数据库里存有question_type字段取值是choice、fill、short三种。评分入口拿到题号后先查题型再走不同的评分分支。def dispatch_scoring(question_type: str, student_answer: str, question) - dict: 题型分派器根据题目类型选择评分策略 返回包含分数和评语的字典 if question_type in (choice, fill): # 客观题直接比对标准答案 correct question.standard_answer.strip() is_right student_answer.strip() correct return { score: 100 if is_right else 0, feedback: 回答正确 if is_right else 回答错误标准答案 correct } elif question_type short: # 主观题走 NLP 评分管线 from .utils.predict import scoring_pipeline result scoring_pipeline(student_answer, question) return result else: # 未知题型降级为默认评分 return {score: 0, feedback: 未识别题型请联系管理员}这里有个设计考虑要说明choice和fill类题目虽然也能用 NLP 做但没有必要。精确比对速度快、结果确定、不会产生争议。把 NLP 资源集中到简答题和论述题上性能压力反而小。如果题目支持模糊匹配比如填空题允许进程调度和进程的调度视为等价可以在fill分支里针对性地做正则模糊匹配而不是直接跑全量相似度计算。2.3 文本预处理分词、去停用词与清洗规则文本预处理是整个评分准确性的地基。中文在这里和英文处理路径不同英文有空格天然分词中文必须先分词。常见做法是用 Jieba 分词配合停用词表把的、了、吗、呢这类无意义词过滤掉。import jieba import re # 停用词表实际项目中推荐用哈工大停用词表扩展版 STOP_WORDS set() with open(stopwords.txt, r, encodingutf-8) as f: for line in f: STOP_WORDS.add(line.strip()) def preprocess_text(text: str) - list: 文本预处理清洗 - 分词 - 去停用词 返回分词后的 token 列表 # 清洗去掉特殊字符和多余空格只保留中文、英文和数字 text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9], , text) # 使用结巴精确模式分词 tokens jieba.lcut(text.strip()) # 过滤停用词、空串和单字 tokens [t for t in tokens if t and t not in STOP_WORDS and len(t) 1] return tokens这段代码有三个地方值得注意。第一正则表达式用[^\u4e00-\u9fa5a-zA-Z0-9]把中文、英文、数字之外的字符全部替换成空格对答案里带括号、句号、逗号的场景非常有用。第二jieba.lcut()用的是精确模式适合评分场景搜索引擎模式会多分出冗余词反而干扰相似度计算。第三过滤条件里加了len(t) 1这行是很多人忽略的停用词表覆盖不了所有高频单字比如是、在、有这种单字词人工维护漏掉了也不奇怪加一个长度限制能兜底。2.4 特征提取与相似度计算TF-IDF 与余弦相似度预处理完的 token 不能直接拿来比得先向量化。源码里用两个手段组合TF-IDF 向量化和余弦相似度。TF-IDF 的核心思想是某个词在学生答案里出现频率高同时这个来源的权重分布独特那它对区分答案优劣的贡献就大。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.metrics.pairwise import cosine_similarity def score_answer(student_tokens: list, standard_answer: str) - float: 基于 TF-IDF 余弦相似度打分 返回 0~1 的相似度分数 student_text .join(student_tokens) # 标准答案同样走一遍预处理 standard_tokens preprocess_text(standard_answer) standard_text .join(standard_tokens) # 语料构造标准答案在前学生答案在后 corpus [standard_text, student_text] vectorizer TfidfVectorizer() tfidf_matrix vectorizer.fit_transform(corpus) # 计算两条向量之间的余弦相似度 sim cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:2])[0][0] return float(sim)关键点是corpus的构造顺序。索引 0 固定放标准答案索引 1 放学生答案取向量时用[0:1]和[1:2]切片。如果顺序颠倒索引取错返回的是同一个向量和自己的相似度恒等于 1.0这个 bug 非常隐蔽。除了语义相似度系统还保留了一个关键词命中兜底。某些专业术语在短文档里 TF-IDF 权重不稳定比如题库小的时候某个术语只在标准答案里出现一次IDF 计算会失真。这时候关键词命中就是底线def keyword_hit_score(student_tokens: list, keywords: list) - float: 关键词命中评分 keywords: 该题目配置的核心关键词列表 返回命中比例 0~1 if not keywords: return 0.0 hit_count len(set(student_tokens) set(keywords)) return hit_count / len(keywords)注意这里用set取交集天然去掉了学生答案里重复出现同一个关键词的干扰。如果某个学生在答案里反复写同一个术语不加set的话命中率会虚高。2.5 综合评分公式与结果输出最后总分是一个加权计算final_score 0.7 * sim_score * 100 0.3 * keyword_score * 100。这个权重在后端配置里可以改不同学科、不同题型应该有不同的权重分配。比如名词解释类题目关键词命中权重可以调到 0.4论述题则相似度权重应该占大头。评分结果输出为 JSON包含分数、相似度详情、关键词覆盖情况和反馈建议一次性返回给前端。源码里还额外计算了一个反馈等级相似度低于 0.3 时给严重偏离0.3 到 0.6 之间给部分接近0.6 以上给基本符合要求。这个分级在生成评语时非常有用比单纯给一个数字友好得多。3. Django 工程落地把评分模型包进 Web 服务的完整步骤3.1 项目结构与关键文件清单拿到源码包解压后先看目录结构再动手改顺序不能反。典型的 Django 项目布局是下面这样路径作用manage.py项目管理入口requirements.txt依赖清单scoring/settings.py项目配置scoring/urls.py路由配置scoring/models.py数据模型scoring/views.py视图层scoring/utils/preprocess.py文本预处理模块scoring/utils/feature.py特征提取与相似度计算scoring/utils/predict.py评分主逻辑scoring/templates/index.html答题页面scoring/templates/result.html结果展示页面scoring/static/CSS、JS 静态资源项目正文里那一堆 CSS 文件的角色可以说明一下bootstrap.min.css 和 layui.css 是两套并行引入的 UI 框架前者管布局响应式后者管弹窗和页面组件style.default.css 是后台管理端的主题样式izeetak-responsive.css 是移动端适配。这套资源是带完整管理后台的不是只有学生答题的裸前端。3.2 核心视图层代码评分接口的完整实现评分接口的核心在views.py。它做四件事接收前端 AJAX 传过来的题目 ID 和学生答案从数据库查标准答案和评分规则调用评分函数返回 JSON 给前端渲染。import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from .utils.preprocess import preprocess_text from .utils.feature import score_answer, keyword_hit_score csrf_exempt def submit_answer(request): 学生提交答案的接口 POST 参数: question_id, student_answer 返回: 分数、评分详情、反馈建议 if request.method POST: data json.loads(request.body.decode(utf-8)) question_id data.get(question_id) student_answer data.get(student_answer, ) # 空答案提前拦截避免后续计算出错 if not student_answer.strip(): return JsonResponse({ code: 0, score: 0, feedback: [答案不能为空] }) # 从数据库读取题目和标准答案 from .models import Question, ScoreRecord question Question.objects.get(idquestion_id) standard_answer question.standard_answer # 预处理 student_tokens preprocess_text(student_answer) standard_tokens preprocess_text(standard_answer) # 相似度评分 sim_score score_answer(student_tokens, standard_answer) # 关键词命中评分 keyword_score keyword_hit_score( student_tokens, question.keywords.split(,) ) # 加权综合 final_score round(0.7 * sim_score * 100 0.3 * keyword_score * 100, 2) # 根据相似度生成分级反馈 feedback [] if sim_score 0.3: feedback.append(答案与标准答案偏离较大建议重新理解题目要求) elif sim_score 0.6: feedback.append(答案基本正确但表达与标准答案差异较大) else: feedback.append(答案整体符合要求注意保持关键术语准确) # 保存评分记录 record ScoreRecord.objects.create( questionquestion, student_answerstudent_answer, scorefinal_score, detailjson.dumps({sim: sim_score, keyword: keyword_score}), feedback; .join(feedback) ) return JsonResponse({ code: 0, score: final_score, feedback: feedback, record_id: record.id }) return JsonResponse({code: 1, msg: 仅支持 POST 请求})这段代码有四个重点。第一csrf_exempt是开发环境的省事写法因为 fetch 直接发 JSON 时默认不带 CSRF token。但上线前必须改成正式的 CSRF 校验方式否则会有安全漏洞。第二空答案拦截放在了最前面这个判断必须有不上这一层的话空字符串分词后返回的是空列表后续计算会出 NaN。第三ScoreRecord里存了detail字段把相似度和关键词命中率都存成了 JSON后面做统计分析、给学生的反馈报告都靠这个字段。第四feedback是列表类型而不是字符串为的是前端能逐条渲染而不是显示一坨带分号的文字。3.3 前端答题页与结果回显模板层用的是 Django Template 加 Bootstrap。答题页的核心是一个表单提交按钮绑定了 AJAX 请求async function submitAnswer(questionId) { const answer document.getElementById(answer).value; const response await fetch(/api/submit_answer/, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ question_id: questionId, student_answer: answer }) }); const data await response.json(); if (data.code 0) { document.getElementById(score).textContent data.score 分; // feedback 是数组必须 join 后再渲染 document.getElementById(feedback).textContent data.feedback.join(; ); } else { alert(data.msg); } }这里有个细节值得注意data.feedback是数组前端要手动join(; )再赋值给textContent。如果直接把数组塞进去textContent会自动转成逗号分隔的字符串显示效果很丑且不可控。另外写这个接口的时候要留一个record_id返回字段这是给后续做评分复核用的——管理员需要在后台定位到具体某条评分记录没有 ID 就只能靠学生姓名和题目模糊匹配了。3.4 运行步骤与依赖安装# 创建虚拟环境 python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 安装依赖 pip install -r requirements.txt # 迁移数据库 python manage.py makemigrations python manage.py migrate # 创建管理员账号 python manage.py createsuperuser # 启动开发服务器 python manage.py runserverrequirements.txt里的核心依赖是 Django、jieba、scikit-learn、numpy。如果你用的是 Python 3.10 以上版本注意 Django 版本要选 4.x 而不是 2.x否则会有兼容性警告。另外如果不需要本地训练模型不要装 torch 这种重依赖推理阶段用 scikit-learn 就够了。4. 模型训练与阈值调优从相似度到最终评分的四层校准4.1 评分规则的组合方式与权重配置最终分数是0.7 * 相似度 0.3 * 关键词命中这个权重在源码里不是写死的而是放在一个配置类里方便在线修改class ScoringConfig: 评分规则配置 SIMILARITY_WEIGHT 0.7 # 余弦相似度权重 KEYWORD_WEIGHT 0.3 # 关键词命中权重 PASS_THRESHOLD 0.6 # 及格线 FEEDBACK_LEVELS { # 反馈分级阈值 high: 0.8, medium: 0.6, low: 0.3 }权重调节的直觉逻辑是如果题目是名词解释答案相对固定关键词命中权重可以调到 0.4~0.5如果是论述题答案自由度很高相似度权重占大头关键词命中降到 0.2 以下。这个配置类的意义在于你不需要改代码逻辑改完配置后直接访问新接口就会生效对毕设答辩时演示参数调整非常有利。4.2 训练集构建与词向量模型如果要上深度学习模型比如 TextCNN 或 BiLSTM就需要训练数据了。现实的做法是导入过去两三次考试的纸质答卷数据让两位老师分别打分取平均分作为标签。数据量不需要夸张几百条简答题数据就能训出一个可用的分类模型。源码里的训练脚本用 gensim 训练 Word2Vec 词向量做特征再用逻辑回归做分类器from gensim.models import Word2Vec from sklearn.linear_model import LogisticRegression import numpy as np def train_embedding_model(sentences): 训练 Word2Vec 词向量 sentences: 标准答案 历史学生答案的分词列表 model Word2Vec( sentencessentences, vector_size128, # 词向量维度 window5, # 上下文窗口 min_count1, # 最低词频 workers4 # 并行线程数 ) return model def sentence_vector(sentence_tokens, w2v_model): 将句子中所有词的向量取平均作为句子向量 vectors [w2v_model.wv[token] for token in sentence_tokens if token in w2v_model.wv] if not vectors: return np.zeros(w2v_model.vector_size) return np.mean(vectors, axis0)vector_size128是性价比比较高的选择调到 300 理论上效果更好但在几百条数据的规模上几乎没有差异反而训练时间多一倍。min_count1在数据量小的时候是必要的所有词都保留但数据量大了建议改成 2过滤掉只出现一次的噪声词。4.3 阈值与权重调优用网格搜索代替玄学调权重这事不能靠感觉要有量化方法。我一般会做一轮小网格搜索拿 100 条人工标注的答案跑不同的权重组合对比系统打分和人工打分的平均绝对误差。SIMILARITY_WEIGHTKEYWORD_WEIGHT平均绝对误差0.90.14.70.70.33.20.50.55.80.60.44.1从上表能看出这个数据集上 0.7/0.3 的组合误差最小。但注意这个结果只对当前题库有效换一个学科或者换一种题型组合权重很可能会变。所以每次拿到新题库都要重新跑一遍网格搜索这是让评分系统在不同场景下保持稳定的前提。反馈阈值也是同样的道理0.3 和 0.6 这两个分界值在不同老师手里的标准不同最好做一个后台配置项暴露出来。5. 避坑指南六个最容易翻车的环节与后悔药5.1 中文乱码数据入库后全是问号现象学生答案存入数据库后管理后台看到的是??????。原因数据库字符集不是 utf-8。MySQL 低版本默认字符集是 latin1存中文必然乱码。解决建库时指定字符集CREATE DATABASE scoring_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;已经建好库的话用ALTER DATABASE scoring_db CHARACTER SET utf8mb4;补救。同时 Django 的settings.py里也要配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: scoring_db, USER: root, PASSWORD: xxx, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4} } }5.2 分词把专业术语切碎了现象分词后死锁被切成死和锁关键词命中率直接下降一半。原因Jieba 默认词典没有收录计算机专业术语分词器按通用词典切分。解决在系统里放自定义词典userdict.txt格式是词 词频 词性死锁 5 n 进程调度 5 n 资源分配 3 n 银行家算法 5 n然后在预处理代码开头加一行jieba.load_userdict(userdict.txt)。词频建议给 5太低的话分词器不会优先按自定义词典切。5.3 停用词表太短导致相似度虚高现象学生答案里全是我觉得我个人认为首先其次这类口语废话却拿到了 0.9 的相似度。原因这些高频表达没有被过滤全部进入 TF-IDF 向量化。它们出现频率高但区分度低会在两条本不相似的向量里制造出共同分量拉高余弦相似度。解决扩充停用词表。把我觉得、我的看法、综上所述、首先、其次、其实、关于这些常见口头禅全部加进去。注意综上所述在论文里是逻辑词但在简答题里几乎没有信息量属于噪声。5.4 空答案拿到非零分现象学生没作答系统给了 20 分。原因空字符串经过预处理后返回空列表TF-IDF 向量是零向量。余弦相似度对零向量返回 0但源码里如果有均值计算或者兜底逻辑空列表会参与数值运算导致结果不稳定。解决视图层前置拦截我在 3.2 节代码里已经写了if not student_answer.strip()的判断。这里再次强调必须放在所有计算之前。5.5 静态文件 404现象runserver启动后页面没有任何样式CSS 全部 404。原因Django 开发服务器默认不处理STATICFILES_DIRS配置的目录需要手动指定。解决STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]同时需要在urls.py里加from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.STATIC_URL, document_rootsettings.STATICFILES_DIRS[0])5.6 前端 AJAX 请求 403现象fetch 提交答案的接口返回 403 Forbidden。原因Django 的 CSRF 中间件拦截了不带 CSRF token 的 POST 请求。解决开发环境用csrf_exempt跳过是省事的选择但正式上线应该在页面模板里嵌入{% csrf_token %}然后在 fetch 头部带上X-CSRFToken。这是我自己的血泪经验当年直接把csrf_exempt带上了生产环境被安全审计点名要求整改非常被动。6. 进阶技巧把评分结果变成可解释的反馈报告评分不只是给一个数字学生更想知道扣分扣在哪。一个实用的做法是把相似度、关键词覆盖、答案完整性这些内部指标渲染成可视化报告而不是在页面上只展示一行分数。我习惯的做法是在result.html里用 ECharts 画一个雷达图四个维度分别是语义相关性、关键词覆盖、答案完整性、语言规范度。对应的数据在评分时其实已经拿到了const radarOption { radar: { indicator: [ { name: 语义相关性, max: 100 }, { name: 关键词覆盖, max: 100 }, { name: 答案完整性, max: 100 }, { name: 语言规范度, max: 100 } ] }, series: [{ type: radar, data: [{ value: [simScore * 100, keywordScore * 100, completenessScore, 80], name: 本次作答 }] }] };图里的四个维度只有前两个是硬指标完整性和语言规范度需要另外算。完整性可以用学生答案分词数量与标准答案分词数量的比值近似语言规范度可以用简单的错别字检测实现。检测错别字最直接的方式是维护一个常见错别字映射表比如的/地/得混用匹配到就在反馈里提示注意区分的和地。这些细节决定了评分系统的下限也决定了答辩时评委的第一印象。从那以后我每次做类似评分系统都会强制走一遍三个极端用例再交付空答案、纯废话答案、专业术语密集答案。这三个用例过了系统基本就稳了。希望帮到你。本文还有配套的精品资源点击获取