ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于爬虫与协同过滤的高考志愿推荐系统设计与实现

基于爬虫与协同过滤的高考志愿推荐系统设计与实现

简介:这套基于爬虫的高考志愿智能推荐系统是面向Java毕设的完整项目资源,采用Spring Boot+Vue前后端分离架构,并集成Python爬虫采集中国教育在线高校录取数据。系统覆盖考生端与后台管理端,包含院校专业查询、位次查询、MBTI职业性格测试、模拟志愿填报、志愿收藏、招生计划管理、咨询管理等功能,并借助知识图谱与智能匹配算法实现个性化推荐。压缩包共671个文件,以Java源码、Vue组件、JavaScript脚本、SQL数据库脚本等为主,附带说明文档、PPT和爬虫脚本,包体约34.56MB,按后端前端及爬虫模块分层组织,便于按需查阅。项目环境基于JDK1.8、MySQL5.7、Tomcat7,并提供安装运行脚本,可直接部署运行或二次开发。目前已有87人学习下载,适合作为毕业设计参考、爬虫与推荐系统整合实践的学习模板。

1. 爬虫高考志愿推荐系统:毕设选题里的“数据+算法”双料选手

第一次看到这个项目名,我以为是普通的 CRUD 毕设,翻完源码才意识到它是把爬虫、知识图谱、MBTI 测评、协同过滤推荐全部塞进了一个 SpringBoot 单体应用里。对于正在找毕设题目的同学来说,这属于“一个项目同时覆盖三个加分点”的类型:数据怎么来(爬虫)、数据怎么存(MySQL + 知识图谱)、数据怎么用(推荐算法)。系统面向考生端和管理员端,考生可以查院校、查位次、做性格测试、模拟填报,管理员则维护专业、院校、招生计划等核心表,后端基于 SpringBoot + MyBatis,前端是两个独立的 Vue 工程,分别跑在 8080 和 8081 端口,整体不复杂但五脏俱全。适合基础一般但想冲高分的同学,也适合想快速掌握“采集 + 存储 + 推荐”完整链路的从业者拿来练手。

2. 系统架构与核心技术选型:为什么是 SpringBoot + Vue + Scrapy 而不是别的

2.1 前后端分离的目录结构:admin 与 front 双工程怎么协同

解压项目后,你会看到两个完全独立的前端工程放在 SpringBoot 的 resources 目录下,这是很多毕设项目的常见做法——后端只负责 API 和静态资源托管,前端单独开发、单独构建,最后把产物放回 resources/static 里统一部署。

springboot2a1qfv22/src/main/resources/ ├── admin/admin/ # 后台管理端 Vue 工程 ├── front/front/ # 考生前台 Vue 工程 └── application.yml # 数据源、端口配置

两个前端工程共用同一套后端 API,只是路由和页面不同。admin 端处理院校、专业、咨询、MBTI 题目的增删改查,front 端处理登录、查询、测评、模拟填报和收藏。这种拆分方式让开发者可以独立启动任意一端调试,不用每次跑全量编译。

启动顺序上我会先起后端,再起前端。后端默认跑 8080,前端通过 devServer 代理把 /api 前缀的请求转发过去,所以前端代码里看不到具体的 IP 和端口,所有接口路径都从 /api 开头。如果改了后端端口,记得同步修改前端的 vue.config.js 里的 proxy 配置。

2.2 爬虫子系统的设计:Scrapy 采集高考录取数据,落库 MySQL

项目里单独放了一个 spider 目录,里面是 Scrapy 爬虫工程,包含 scrapy.cfg、爬虫主文件以及两个批处理脚本——安装依赖的 install.bat 和启动爬虫的 run.bat。爬虫目标是中国教育在线网站的各高校历年录取数据,包括院校名称、专业名称、录取人数、最低分、最低位次、年份等字段。

我一般会先在命令行里单独执行爬虫,确认数据能正常写入 MySQL 后再集成到 SpringBoot 里手动触发,这样出错时定位范围小很多。爬虫的核心逻辑是定义 Item 结构,然后在 pipeline 里直接写 MySQL,而不是输出到中间文件,这样省掉一步导入操作。

# spider/pipelines.py 中核心入库片段 import pymysql from .items import SchoolItem, ScoreLineItem class MysqlPipeline(object): def open_spider(self, spider): self.conn = pymysql.connect( host='localhost', port=3306, user='root', password='123456', database='gaokao_db', charset='utf8mb4' ) self.cursor = self.conn.cursor() def process_item(self, item, spider): if isinstance(item, SchoolItem): sql = "INSERT INTO school (name, province, level, type) VALUES (%s, %s, %s, %s)" self.cursor.execute(sql, (item['name'], item['province'], item['level'], item['type'])) elif isinstance(item, ScoreLineItem): sql = "INSERT INTO score_line (school_id, year, min_score, min_rank) VALUES (%s, %s, %s, %s)" self.cursor.execute(sql, (item['school_id'], item['year'], item['min_score'], item['min_rank'])) self.conn.commit() return item

pymysql 的 charset 参数必须是 utf8mb4,否则遇到生僻院校名或特殊符号会直接报错中断爬虫。process_item 里根据 Item 类型分表写入,如果同时爬多张表,建议把数据库连接放到 open_spider 里只初始化一次,不要每来一条数据都新建连接。数据量大的时候还要考虑幂等:重复执行同一个爬虫任务时,最好在写入前先查一下主键是否已存在,或者直接用 INSERT IGNORE,否则多跑几次会出现大量重复记录。

2.3 知识图谱模块:用图结构组织高校、专业、就业前景的关联关系

很多人看到“知识图谱”四个字就慌,其实在这里它表现得比较朴素。系统预先构建了三类实体——高校、专业、就业岗位,以及它们之间的关联关系,比如高校-专业属于关系、专业-岗位推荐关系、岗位-就业前景描述关系。这些数据存在 MySQL 的几张关联表里,后台管理员可以维护,前端查询时通过接口把关系链拉出来展示。

在数据库设计上,知识图谱没有用 Neo4j 而是用传统的关系表配合 JSON 字段,这是毕设项目里非常务实的取舍:Neo4j 学习成本高,且不好演示 CRUD,而关系表结构直观,写出来的 SQL 也好解释。比如你查“计算机科学与技术”专业,接口会返回这个专业所属的高校列表、推荐就业岗位、近三年的薪资范围和发展趋势,这些在页面上展示成卡片流,视觉效果比普通列表丰富得多。

推荐匹配环节则把考生的 MBTI 测评结果映射到专业标签上,例如 INFJ 类型匹配教育类、心理学类专业,ISTP 类型匹配机械类、计算机类专业。这个映射表是可以后台配置的,这样系统就具备了“性格测试 → 专业倾向 → 院校筛选”的完整链路,推荐结果不是拍脑袋,而是有测评依据的。

3. 核心功能拆解:从模拟填报到 MBTI 测评,前端页面与后端接口的对应关系

3.1 考生端功能清单:登录、查院校、查位次、模拟填报、收藏一条线走通

考生端的核心流程是:注册登录 → 输入成绩和位次 → 系统按冲稳保策略推荐院校 → 考生查看详情并收藏 → 生成志愿表。这个流程对应后端多个接口,我列一下主要的映射关系,方便你上手时对着代码找:

功能模块前端页面后端接口路径核心数据表
用户注册登录Login.vue / Register.vue/api/user/login, /api/user/registeruser
院校查询SchoolSearch.vue/api/school/list, /api/school/detailschool
专业查询MajorSearch.vue/api/major/listmajor
位次查询RankSearch.vue/api/rank/listrank_info
模拟填报MockFill.vue/api/recommend/mockschool_score_line
志愿管理VolunteerList.vue/api/volunteer/save, /api/volunteer/listvolunteer
MBTI测评MBTITest.vue/api/mbti/submit, /api/mbti/resultmbti_question, mbti_result
院校收藏FavoriteList.vue/api/favorite/add, /api/favorite/listfavorite

模拟填报的算法逻辑在后端 RecommendService 里,核心思路是取考生分数对应的位次,再查历年数据中录取位次小于考生位次 5% 的院校作为“冲”,位次范围在考生位次上下 10% 以内的作为“稳”,位次大于考生位次 15% 以上的作为“保”。这个百分比是写死在代码里的常量,你可以改成从配置表读取。

3.2 管理员端功能:专业、院校、招生计划、MBTI 题库的维护逻辑

管理员端的页面基于 ElementUI,功能点都是常规的 CRUD,但有一个设计值得学习:MBTI 题库不是写死的,而是数据表驱动。每条题目包含题号、题目文本、选项 A/B、以及每个选项对应的性格维度加分项。考生提交测评后,后端遍历所有题目,累计每个维度的分数,最后输出四维组合。

这种设计的好处是,管理员可以随时增删题目或调整选项分值,前端零改动。如果你在论文里写“系统支持动态调整测评题目”,这个实现方式是完全撑得住的。

管理员端还有一个“高考咨询管理”模块,本质是个内容发布系统,管理员发布报考政策、院校解读、专业分析等文章,考生在前台按分类查看。这样系统里就有了“静态数据(院校/专业)+ 动态内容(咨询文章)+ 用户行为(收藏/测评/志愿)”三类数据,推荐算法可以综合使用,不至于无米下锅。

3.3 模拟填报的推荐策略:冲稳保公式中的几个关键参数

推荐策略这块我建议你重点看 service 层,因为它是整个项目的智力核心。它接收的参数是考生分数、科类、省份,返回的是一个按风险等级分组的院校列表。我梳理一份参数含义表,方便你二次开发时分清每个字段的作用:

参数名含义默认值调整建议
deltaUp冲档位次比例0.05值越小,冲的院校越少
deltaStable稳档位次范围0.10值越大,稳的院校越多
deltaDown保档位次比例0.15值越小,保底越保守
topN每个档位返回数量10可根据前端展示区域调整

这个策略属于“基于规则的推荐”,不涉及机器学习,但对于毕设来说完全够用,而且在论文里特别好解释——每个参数都能对应到一个实际业务含义。如果你想加亮点,可以在这套规则结果上再做一层协同过滤,把“相似考生”的最终志愿作为二次排序依据,代码量不大但能体现推荐算法的多样性。

3.4 前后端接口对接:代理配置与跨域问题的处理方式

两个前端工程在开发模式下都通过 webpack devServer 做代理,以 front 工程为例,vue.config.js 里配置如下:

// front/vue.config.js module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }

这里要注意 pathRewrite 的作用——前端请求 /api/user/login 时,代理会把 /api 前缀剥离,转发到后端的 /user/login,所以后端 Controller 里不要写 /api 前缀,否则两层前缀叠加会 404。我见过不少人踩这个坑,前端代理配了,后端接口也写了 /api,结果请求全挂在 /api/api 上。

如果跨域问题出现在生产环境,不要把前端打好的 dist 单独扔到 nginx 上然后手动配跨域,更简单的做法是直接把 dist 复制到 SpringBoot 的 resources/static 目录下,让后端自己托管静态资源,同源访问不存在跨域问题。

4. 从零启动完整项目:环境安装、数据库导入与三个启动脚本的执行顺序

4.1 环境准备:JDK1.8、MySQL5.7、Maven、Node 版本兼容性说明

项目对环境的锁定比较严格,JDK 必须是 1.8,MySQL 必须是 5.7,Maven 最好是 3.6 以内。如果你本机装的是 JDK17 或 MySQL8,建议装一个虚拟机或者用 Docker 开一个对应版本的环境,不要硬着头皮用高版本跑,大概率会花一晚上解决兼容问题。

Node 版本推荐 14 或 16,两个前端工程用的是 Vue2 + ElementUI,Node18 下有些依赖会报 OpenSSL 相关的错误。真遇到 Node18 跑不起来的情况,可以用 nvm 切到 14 再装依赖,不要花时间去找兼容包版本,切版本是性价比最高的解法。

MySQL 的数据导入用 Navicat 执行项目附带的 .sql 文件即可。注意导入前先建库,库名要和 application.yml 里的配置一致,默认是 gaokao_db,如果建错了库名,后端启动时会一直报连接失败,且报错信息比较隐晦,容易误判成端口问题。

4.2 数据库表结构解读:用户表、院校表、分数位次表、志愿表的设计要点

我打开数据库粗略数了一下,核心表有十几张,挑几个关键的讲。user 表除了常规的 username、password,还存了考生的省份、科类、分数、位次,这些字段供推荐算法直接调用,不需要外键关联查询。

school 表存院校基本属性,字段包括院校代码、名称、省份、层次(本科/专科)、办学类型(公办/民办)、院校标签(985/211/双一流)。score_line 表存历年录取数据,以 school_id 关联院校,每条记录有年份、批次、科类、最低分、最低位次。volunteer 表存考生填的志愿方案,主键是自增 id,外加考生 id 和按顺序排列的院校 id 列表。

这些表的设计没有过度范式化,很多冗余字段是故意保留的,比如 user 表直接存位次而不是通过分数去换算,因为换算逻辑复杂且容易出错。毕设项目讲究“能直接查的不关联”,这样 SQL 简单,加分点在于满足功能需求,而不是炫技。

4.3 启动后端:Maven 打包与 SpringBoot 起服务的完整流程

后端启动我用的是 IDEA,打开 springboot2a1qfv22 目录,右键 pom.xml 选择作为 Maven 工程导入。依赖下载完成后,在 Application 启动类上点运行,等控制台出现 Spring Boot 启动成功的日志即可。

# 如果你的环境偏好命令行启动 cd springboot2a1qfv22 mvn clean package -DskipTests java -jar target/gaokao-0.0.1-SNAPSHOT.jar

mvn clean package 会依次跑编译、测试、打包三个阶段。加上 -DskipTests 跳过测试是为了节省时间,如果打包过程中报依赖下载失败,检查一下 Maven 镜像是否配置成阿里云镜像,原版中央仓库在国内下载速度堪忧。启动类一般在 src/main/java 下,名字形如 XxxApplication,找到后直接运行即可。

4.4 启动前端:依赖安装、编译、浏览器访问的每一步

两个前端工程启动方式一致,我以 front 为例写一遍命令。首次运行要先执行 install.bat 或手动 npm install,依赖装完后 npm run dev 启动开发服务器。

cd front/front npm install npm run dev

npm install 如果卡在某个包的编译上,先检查 Node 版本,然后再看网络是否是公司内网限制了某些源。稳妥的做法是把 registry 切到国内镜像,再删掉 node_modules 重装:

npm config set registry https://registry.npmmirror.com rm -rf node_modules package-lock.json npm install

启动成功后浏览器访问 http://localhost:8081 就能看到考生前台,后台管理端同理,访问 http://localhost:8082(admin 工程的 devServer 端口)进入管理页面。两个端口的默认登录账号在说明文档里有写,如果文档丢失了,可以直接看数据库里 user 表的初始数据。

4.5 爬虫模块启动:安装依赖、执行爬取、数据落库验证

spider 目录下的 install.bat 会安装 Scrapy 和 pymysql,运行后确认命令行不报 import 错误。start 爬虫执行方式如下:

cd spider scrapy crawl gaokao

爬虫启动后控制台会持续输出 item 的写入日志,如果看到 Duplicate entry 或 Deadlock 错误,多半是数据表缺少唯一索引或爬虫脚本在并发写入。解决办法是在目标表添加唯一约束,并关闭 Scrapy 的并发设置(在 settings.py 里把 CONCURRENT_REQUESTS 调成 1)。爬完以后去 Navicat 里抽查 score_line 表的条数和年份分布,确定数据覆盖度再考虑接进系统。

5. 避坑指南:我从这个项目里踩过的七个典型问题

5.1 前端口号冲突导致页面白屏

现象:npm run dev 启动后浏览器打开是白屏,控制台报错信息指向端口占用。原因:8081 端口已被其他进程占用,devServer 自动换了端口但代理配置没跟着变,后端请求全部转发失败。解决:先查到占用进程并释放端口,或者启动前在 vue.config.js 里显式指定一个空闲端口,不要依赖自动切换。

5.2 MySQL 导入初始数据时报字符集错误

现象:SQL 导入执行到一半报错,字段里有乱码或直接中断。原因:数据库连接没有指定 utf8mb4,导致生僻字(比如某些院校名里的生僻汉字)无法写入。解决:建库时执行 SET NAMES utf8mb4,同时确保 .sql 文件本身的编码是 UTF-8,导入前用记事本或 VSCode 检查一下文件编码。

5.3 爬虫跑到一半被网站拦截

现象:爬虫运行 5~10 分钟后请求连续返回 403 或验证码页面。原因:网站有反爬机制,同一 IP 的请求频率过高被识别。解决:在 settings.py 里把 DOWNLOAD_DELAY 设为 1.5 秒,并开启 User-Agent 随机切换,加入项目附带的 user-agent 列表。数据量大概几千条,单线程跑也不慢,不需要上分布式。

5.4 前端访问后端接口 404

现象:前端页面能打开,但所有接口请求返回 404。原因:最常见的两种情况,一是前端代理的 pathRewrite 配置和后端 Controller 里的路径前缀叠加成了双前缀,二是后端还没启动完前端就开始发请求,浏览器缓存了旧的失败结果。解决:先确认后端控制台有没有对应的请求日志,有的话说明代理通但路径不对,没有就说明代理没生效,检查 vue.config.js 有没有配置到 devServer 节点下方。

5.5 模拟填报结果为空列表

现象:输入分数和位次后,推荐的冲稳保三档全部为空。原因:score_line 表里没有对应省份、科类和年份的数据,规则匹配查不到任何记录。解决:先用 SQL 手动查一下这个考生的条件组合下有没有数据,比如 SELECT * FROM score_line WHERE province='河南' AND type='理科' AND year=2022,确认数据范围能满足匹配条件,再调整 topN 参数。

5.6 MBTI测评结果和预期不一致

现象:同一个考生重复测试,两次结果不同,或者结果明显不符合预期。原因:题库的维度分值配置有误,比如某个选项本应给 E 维度加分,配置表里却给 I 维度加了分。解决:逐个核对后台维护的题目选项映射关系,把每题的四个维度分值加起来做一个全量校验,确保总分数是恒定值。

5.7 项目直接 import 到 IDEA 后依赖爆红

现象:导入后 pom.xml 大量报错,依赖下载失败。原因:IDEA 默认使用内置 Maven,本地仓库路径和镜像配置不对。解决:在 IDEA 的 Settings 里指定本机 Maven 的 settings.xml,确认 mirror 指向阿里云,然后重新 import 一次,等依赖索引刷新完即可。

提示:如果以上任意一步报错信息不够直观,优先去看后端启动日志,SpringBoot 的报错信息里通常已经包含了遗漏的关键线索,比盲搜网上的答案效率高得多。

6. 把毕设往上拔一个档次:给项目加一个基于用户行为的协同过滤二次推荐

如果你想在答辩时让老师眼前一亮,我给一个具体可落地的增强方案:在原有“冲稳保规则推荐”之上,增加一个基于相似用户的协同过滤层。逻辑是:当考生 A 完成了模拟填报并保存志愿表后,系统在后台找出与其分数、位次相近且已经保存志愿表的其他考生,统计这些考生的志愿院校交集和排序,作为 A 的候选推荐排序依据。

实现上,你可以复用现有的 volunteer 表,新增一张 user_similarity 表存储考生相似度矩阵,或者不建表、直接运行时计算。数据量小的时候,运行时计算更简单。我给出核心算法的伪代码:

# 协同过滤推荐核心逻辑 def collaborative_filter_recommend(user_id): # 获取当前考生的位次和省份 current = get_user_profile(user_id) # 找出分数段相近的其他考生 similar_users = find_similar_users(current) # 统计这些考生志愿中出现频率最高的院校,按出现次数排序 candidates = {} for uid in similar_users: volunteers = get_volunteers_by_user(uid) for school_id in volunteers: candidates[school_id] = candidates.get(school_id, 0) + 1 sorted_candidates = sorted(candidates.items(), key=lambda x: x[1], reverse=True) # 与规则推荐的结果做加权融合 rule_based = get_rule_based_recommendation(current) return merge_result(rule_based, sorted_candidates[:20])

这个增强点好在哪里?一是业务逻辑闭环,考生行为真正影响了推荐结果,不再是纯规则匹配;二是实现成本不高,SQL 里做按位次范围检索即可;三是论文里可以写“基于规则与协同过滤的混合推荐策略”,比单一方法明显高一个层次。

做完这个以后,我每次跑完整流程都会强制走一遍“爬虫采集 → 数据入库 → 前端模拟填报 → 推荐结果验证”这条链路,确认每一层的输出都符合预期再收工。这个检查习惯帮我避免了很多次答辩前才发现数据对不上的尴尬。

希望这篇拆解能帮你把这个项目彻底吃透,有任何一块想深入实践的,顺着源码往下挖就行,祝顺利。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进