
简介基于爬虫的高考志愿智能推荐系统是一套完整的Java毕设项目采用SpringBoot Vue前后端分离架构面向计算机相关专业毕业生及Java学习者。系统整合爬虫、知识图谱与智能算法可实时抓取中国教育在线高考录取数据结合考生成绩与MBTI性格测试结果生成个性化志愿推荐并支持模拟填报、院校专业查询、院校收藏、志愿管理等操作适合作为毕业设计选题、课程项目或Java全栈实战练手参考。资源包共671个文件大小约34.56MB包含Java后端源码、Vue前端页面、Python爬虫脚本、SQL数据库、PPT与说明文档等同时提供多个bat一键安装/运行脚本便于快速部署调试。目前已有87人学习涉及专业管理、院校管理、招生计划管理、位次管理等后台功能环境涵盖JDK1.8、Tomcat7、MySQL5.7及常见IDE。压缩包目录结构清晰从爬虫模块到双端应用均有完整实现可帮助读者深入理解前后端分离项目开发流程及推荐系统落地细节。1. 用爬虫把高考志愿填报做成“推荐引擎”高考志愿填报这个场景最痛的不是没有数据而是数据散在几本书和几十个网站上等把位次、历史线、招生计划抄齐时时间已经过半。看起来是“爬虫高考志愿智能推荐系统”的毕设实际是把一条数据业务串起来了爬虫负责抓取历年录取数据MySQL 做存储Spring Boot 提供查询和推荐接口Vue 分别做考生端和后台管理端再加上知识图谱和 MBTI 性格测试做个性化因子。适合两类人想用一套前后端分离的 Java 全栈项目撑起毕设的同学和对推荐逻辑、爬虫入库有兴趣的开发者。跑通它不难真正有价值的是理解并修改它的推荐链路。2. 系统全貌从考生端到后台管理一条完整的数据链路2.1 考生端查询、测评、模拟填报三件事考生端核心功能围绕“决策前查询—决策中测评—决策后填报”设计。查询模块包括专业、院校、位次、历史录取数据、高考咨询五类检索前端按条件筛选后端返回分页数据。历史录取数据是最高频的查询因为考生要先知道自己这个位次能摸到哪些学校才会进入下一步。所以这类接口必须支持年份、省份、科类的组合筛选否则每次都要翻半天。测评模块是 MBTI 职业性格测试。前端出题每道题对应 E/I、S/N、T/F、J/P 四个维度的加减分提交后由后端统计出性格类型再映射到适合的专业方向。这里不是简单把 16 种类型硬编码而是把专业方向做成一张推荐表管理员在后台可以调整映射关系。填报模块包含模拟志愿填报、志愿管理、院校收藏和院校识别。模拟填报是核心输入分数和位次后系统按“冲、稳、保”三档给建议志愿管理则把考生填的志愿表保存下来允许反复修改。一个典型使用路径是这样的考生注册登录后先查目标院校近三年的录取位次做 MBTI 测评得到适合自己的专业方向再输入自己的分数和位次拿到推荐列表把感兴趣的院校加入收藏最后进入模拟填报生成一份完整志愿表。这条路径顺畅走通系统的主流程就没有大问题。2.2 管理端给数据兜底的后台管理端功能包括专业管理、院校管理、招生计划管理、位次管理、高考咨询管理、用户管理和 MBTI 测评管理。这些模块单独看都是常规 CRUD但组合起来就是整个推荐系统的数据闭环。管理员负责校正爬虫抓回来的脏数据维护招生计划和位次审核咨询内容配置测评题目和性格与专业的映射关系。数据准不准直接决定推荐结果可不可信。实际开发里最容易翻车的也是这里爬虫数据没有清洗就直接用了导致前端列表出现空值、重复院校、专业名称前后不一致。我在处理类似项目时习惯在管理端列表加一个“来源标记”字段区分爬虫数据和人工维护数据审核通过后再进入推荐候选池。管理员的工作不是天天盯数据而是定期把异常数据处理掉保证推荐接口查询到的每一行都可用。2.3 技术栈与目录结构技术栈是一套典型的 Java 前后端分离毕设组合后端 Spring Boot Maven MySQL 5.7 JDK 1.8前端用 Vue爬虫用 Scrapy。项目里能看到springboot2a1qfv22目录保存后端spider目录放爬虫和安装运行脚本。前端不是一份而是admin和front两套admin是后台管理端front是考生端共用同一份后端接口。目录结构大致如下。项目根目录/ ├── springboot2a1qfv22/ │ ├── src/main/java/ # 后端源码 │ ├── src/main/resources/ │ │ ├── admin/admin/ # 后台管理前端工程 │ │ ├── front/front/ # 考生端前端工程 │ │ ├── application.yml # 后端配置 │ │ └── ... │ └── pom.xml ├── spider/ │ ├── scrapy.cfg # Scrapy 项目配置 │ ├── 安装.bat / 运行.bat │ └── ... └── SQL脚本 / 说明文档 / 论文 / PPT前端被放在src/main/resources下面这个结构很多人第一次看到会迷惑。常见做法是后端和前端分开两个工程但毕设项目为了方便统一启动和答辩展示会把前端工程直接放进来。这样做的另一个好处是最后能用mvn package把前端构建产物一起打进 jar访问根路径就能看到页面代价是前后端耦合变高理解成“前端是后端的一部分静态资源”就好。2.4 知识图谱在系统里怎么落地这里的知识图谱不一定要引入图数据库。看整个项目的数据组织方式更合适的实现是用节点和关系两张表来表达图谱再通过 Service 层做路径查询。节点表维护高校、专业、位次数据、就业前景关系表维护院校与专业的“开设”、专业与位次的“录取”、高校与咨询的“归属”等关系。前端展示图谱时从关系表查出邻接节点渲染关系图。相比直接引入 Neo4j这种表结构的优势是部署成本低和 Spring Boot 自带的 ORM 配合更顺。如果数据量到几十万级关系查询会变慢但毕设场景下完全能接受。扩展新实体也很简单节点表加一行记录关系表加一条边即可。学习知识图谱概念的同学可以用这个表结构先理解三元组是怎么存储的。2.5 一次推荐请求会经过哪些环节考生进入推荐页后提交分数、位次、MBTI 类型、兴趣方向后端接收后先查位次表拿到历年录取区间再过滤出该考生“冲稳保”范围内的高校和专业接着把 MBTI 映射的专业方向作为权重加成最后按综合得分排序返回。推荐接口的返回结构一般长这样。GET /api/recommend?rank45000provinceA省year2024 { code: 0, msg: success, data: [ { school: 某高校, major: 计算机类, score: 82.5, tier: 稳, minRank: 44000 } ] }rank是考生位次province和year决定用哪一年的数据tier表示冲稳保分类minRank是该校专业上一年最低录取位次。推荐接口返回的是排序后的候选列表不是单一答案。考生拿这份列表去做模拟填报比从零开始翻学校效率高很多。2.6 前后端接口约定与统一返回格式前后端分离的项目里接口规范比接口数量更重要。这套系统的后端接口统一使用code msg data的返回结构code为 0 时代表成功非 0 代表业务异常。前端 axios 拦截器会在code ! 0时弹出错误提示这样 Controller 里不需要每个接口都手动处理异常分支。前端两个工程通过vue.config.js里的 devServer 代理转发请求。开发环境下考生端访问/api代理会转发到后端8080端口生产环境则把dist目录放进后端静态资源。后端接口的鉴权用拦截器处理考生端和管理端各有一套登录态部分接口需要请求头携带 token。理解了这套约定排查接口问题时基本能定位到三层前端代理、后端拦截器、数据库查询。3. 核心模块拆解一份数据从爬到推荐的全流程3.1 爬虫模块Scrapy 抓取历年录取数据爬虫部分放在spider目录入口是scrapy.cfg。安装和运行用批量脚本简化了但真正要关心的是 Spider、Pipeline 和 Settings 三个文件。一个典型的录取数据爬虫长这样。# spiders/admission.py import scrapy from ..items import AdmissionItem class AdmissionSpider(scrapy.Spider): name admission def start_requests(self): # 按省份、年份构造查询列表页 base_url https://data-source.example.org/search?province{}year{} for province in [A省, B省, C省]: for year in [2022, 2023, 2024]: yield scrapy.Request( urlbase_url.format(province, year), callbackself.parse_list, headers{User-Agent: self.settings.get(USER_AGENT)} ) def parse_list(self, response): # 列表页提取每所院校的详情页链接 for school_url in response.css(.school-name a::attr(href)).getall(): yield scrapy.Request( urlresponse.urljoin(school_url), callbackself.parse_detail, ) def parse_detail(self, response): item AdmissionItem() item[school_name] response.css(.school-name::text).get() item[major_name] response.css(.major-name::text).get() item[min_score] response.css(.min-score::text).get() item[min_rank] response.css(.min-rank::text).get() item[year] response.url.split(year)[-1] yield itemSpider 用start_requests根据省份和年份参数生成请求parse_list提取院校详情页链接parse_detail提取录取分数和位次字段并封装成 Item。这里的 URL 和数据站结构是演示写法真实环境需要用浏览器开发者工具确认页面结构再替换 CSS 选择器。参数min_score是最低分min_rank是最低位次这两项是后续推荐的核心依据宁可爬空也不要爬错字段。Pipeline 负责把 Item 写入 MySQL。Scrapy 的 Item Pipeline 按顺序执行写入数据库的代码一般在process_item方法里完成。# pipelines.py import pymysql class MySQLPipeline: def open_spider(self, spider): self.conn pymysql.connect( hostlocalhost, userroot, password123456, databasecollege_recommend, charsetutf8mb4, ) def process_item(self, item, spider): try: with self.conn.cursor() as cursor: sql INSERT INTO admission_data (school_name, major_name, min_score, min_rank, year) VALUES (%s, %s, %s, %s, %s) cursor.execute(sql, (item[school_name], item[major_name], item[min_score], item[min_rank], item[year])) self.conn.commit() except Exception as e: self.logger.error(f写入失败: {e}) return item def close_spider(self, spider): self.conn.close()Pipeline 在open_spider里建立数据库连接process_item里逐条写入失败时记录日志但不中断整批爬取。这样设计是考虑到抓取量大时中断重跑比单条失败更浪费时间。实际运行前要把数据库连接参数改成自己环境的值。建议密码放到settings.py或环境变量里不要写死在 Pipeline 中。3.2 知识图谱的表结构设计与查询项目里的知识图谱实现更接近一种“轻量图谱表结构”。核心是节点表和关系表。CREATE TABLE graph_node ( node_id BIGINT PRIMARY KEY AUTO_INCREMENT, node_type VARCHAR(50) NOT NULL COMMENT 院校/专业/位次/就业前景, node_name VARCHAR(200) NOT NULL, attrs TEXT COMMENT 扩展属性JSON格式 ); CREATE TABLE graph_relation ( relation_id BIGINT PRIMARY KEY AUTO_INCREMENT, from_node BIGINT NOT NULL, to_node BIGINT NOT NULL, relation_type VARCHAR(50) NOT NULL COMMENT 开设/录取/位于/就业, weight DOUBLE DEFAULT 1 COMMENT 关系权重用于推荐打分, update_time DATETIME DEFAULT CURRENT_TIMESTAMP );graph_node把所有实体统一成节点graph_relation记录节点之间的关系。weight字段是推荐算法的关键比如“专业与兴趣方向匹配”权重高一点“位次满足录取区间”权重高一点。查询某高校的全部专业和录取位次时用from_node或to_node关联查询即可。// GraphService.java public ListGraphRelation getRelationsByNode(Long nodeId) { return graphRelationMapper.findByFromNode(nodeId); }逻辑说明getRelationsByNode查出从某个节点出发的所有关系前端拿到关系列表后遍历取出to_node对应的节点信息就能渲染出“高校—专业—位次”的图谱视图。扩展新实体不用改表结构新增一行记录就行坏处是复杂查询需要多次 join但数据量在万级以下表现稳定。3.3 推荐算法基于位次的多维加权打分推荐算法的核心在 Service 层。一个常见实现是先把候选记录查出来再逐条打分排序。// RecommendService.java public ListRecommendResult recommend(RecommendRequest req) { // 1. 根据考生位次与历年位次区间筛选候选 ListAdmissionLine lines admissionLineMapper.findByRankRange( req.getMinRank() * 0.8, req.getMinRank() * 1.2, req.getProvince(), req.getYear()); // 2. 逐个候选计算推荐分 ListRecommendResult results new ArrayList(); for (AdmissionLine line : lines) { double score 0; // 位次吻合度越接近该校最低位次得分越高 score closenessScore(req.getMinRank(), line.getMinRank()); // MBTI 匹配专业方向 if (majorMatches(req.getMbtiType(), line.getMajorName())) { score 15; } // 兴趣标签命中 if (hasInterest(req.getInterestTags(), line.getMajorName())) { score 10; } // 冲稳保判断 String tier tierOf(req.getMinRank(), line.getMinRank()); results.add(new RecommendResult(line, score, tier)); } results.sort((a, b) - Double.compare(b.getScore(), a.getScore())); return results.subList(0, Math.min(20, results.size())); }findByRankRange拿到位次附近的录取记录范围 0.8 到 1.2 倍可以灵活调整closenessScore计算位次接近程度通常用差值百分比换算成百分制MBTI 匹配和兴趣标签是加分项最后用tierOf判断候选属于冲、稳、保哪一档。这里的参数都是可调的候选范围倍数、加分项分值、返回数量。实际运行时优先优化的不是代码而是 SQL建议给(min_rank, year, province)建联合索引。推荐结果返回tier字段后前端会把“冲”的院校放最上面但分数只用于排序不直接告诉考生“必须报这个”。这是产品边界系统给参考决策权还是考生自己。改推荐逻辑时优先调权重不要一上来就推翻候选生成条件。3.4 模拟填报与志愿管理模拟填报的核心是“冲稳保梯度”。系统给出一组候选后考生手动选择意向院校和专业保存成志愿表。表结构上是主从表主表存考生 id 和志愿表名称从表存志愿项和顺序。保存志愿表的逻辑如下。public void saveVolunteer(VolunteerVO vo) { volunteerMapper.insert(vo.getMain()); for (int i 0; i vo.getItems().size(); i) { VolunteerItem item vo.getItems().get(i); item.setVolunteerId(vo.getMain().getId()); item.setOrderNo(i 1); volunteerItemMapper.insert(item); } }插入主表后循环插入从表orderNo用列表下标加一生成保证志愿顺序和前端展示一致。前端还需要校验志愿数量是否超过上限常见做法是根据院校批次设置不同上限比如普通批最多 6 个院校志愿、每个院校最多 6 个专业志愿。校验不通过时返回给前端具体提示不能只给“操作失败”。3.5 MBTI 测评与专业方向映射测评模块分题库、计分、映射三部分。题库由管理员在后台维护每道题属于一个维度选项带分值考生提交后后端统计四个维度的得分每个维度取倾向明显的一侧拼出 MBTI 类型最后查映射表得到推荐专业方向。后端处理接口参数和逻辑如下。public MBTIResult analyze(ListAnswer answers) { int[] scores new int[8]; for (Answer a : answers) { Option opt optionMapper.selectById(a.getOptionId()); scores[opt.getDimensionIndex()] opt.getScore(); } String type ; type scores[0] scores[1] ? E : I; type scores[2] scores[3] ? S : N; type scores[4] scores[5] ? T : F; type scores[6] scores[7] ? J : P; return mbtiMapper.findDegreeByType(type); }scores数组存八个维度的累计分每道题只增加所属维度。最终比较配对维度得到四位类型码。findDegreeByType查出适合的专业方向列表推荐模块再拿它去匹配候选专业。如果后面想加霍兰德测评或学科能力测评只需要在这一层增加一个新的测评服务不影响推荐主流程。4. 本地部署从空环境到前后端跑通的完整过程4.1 环境清单与版本约定运行这套项目需要准备 JDK 1.8、Maven 3.6、MySQL 5.7、Node.js 14 或 16、Python 3.6 以及 Scrapy。数据库管理工具可以用图形化客户端也可以用命令行导入。后端是 Spring BootJDK 1.8 环境下不需要单独装 Tomcat内嵌容器启动。两个 Vue 前端工程分别安装依赖、启动开发服务共用同一个后端接口。项目自带的说明文档里有更详细的环境对照表先看一遍再动手能少踩很多坑。4.2 初始化数据库把项目根目录下的 SQL 文件导入 MySQL。不建议在图形化工具里双击打开直接执行更稳妥的做法是新建一个空数据库字符集选utf8mb4然后执行 SQL 文件。mysql -uroot -p123456 -e CREATE DATABASE IF NOT EXISTS college_recommend DEFAULT CHARSET utf8mb4; mysql -uroot -p123456 college_recommend sql/college_recommend.sql如果 SQL 文件是手动从别的库导出的要注意表名和数据库名是否与后端配置一致。导入完成后重点检查admission_data、graph_node、graph_relation、volunteer这几张表有没有数据。没数据时先别急着启动后端否则接口返回全空容易误判成代码问题。4.3 修改后端配置文件后端配置在springboot2a1qfv22/src/main/resources/application.yml。数据库账号、密码、端口都要改成本地环境。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/college_recommend?useUnicodetruecharacterEncodingutf8mb4serverTimezoneGMT%2B8 username: root password: 123456这段配置指定服务端口、数据库驱动、连接地址和账号密码。characterEncodingutf8mb4是防中文乱码的关键缺少这个参数爬虫写入的中文专业名很容易变成问号。serverTimezoneGMT%2B8解决时区报错。如果项目里还用到了 Redis 做验证码缓存确认本地 Redis 已启动没有用到的话把相关依赖注释掉避免启动报错。4.4 启动后端服务用 IDE 打开后端工程等待 Maven 下载依赖。推荐先执行 Maven 打包再用 jar 启动能提前暴露编译期问题。cd springboot2a1qfv22 mvn clean package -DskipTests java -jar target/springboot2a1qfv22-0.0.1-SNAPSHOT.jar-DskipTests跳过单元测试加速打包。启动日志出现Started SpringBootApplication就说明后端起来了。如果启动失败优先看最上面几行报错常见的是数据库连不上和 MyBatis 映射文件路径找不到。如果端口被占用先排查是不是已有项目占了 8080再决定改端口还是杀进程。4.5 启动前后端两个 Vue 工程两个前端都在src/main/resources下。安装依赖用npm install启动开发服务用npm run dev项目里的install.bat、run.bat、build.bat本质就是这些命令的封装。cd springboot2a1qfv22/src/main/resources/admin/admin npm install npm run dev # 考生端另开一个终端窗口 cd ../../front/front npm install npm run dev启动成功后开发服务器默认端口通常是 8081 或 8082。这里必须检查vue.config.js里的代理配置把/api代理到后端http://localhost:8080。前端页面虽然能打开但接口 404 的大概率原因就是代理没配对或后端端口与代理目标不一致。改了后端端口要同步改这里。4.6 爬虫数据采集流程后端和前端跑通之后数据库里可能还是演示数据。想抓取新数据需要运行爬虫。cd spider pip install -r requirements.txt scrapy crawl admission先安装依赖再启动admission爬虫。运行前要检查settings.py里的数据库连接、下载延迟和 User-Agent。抓取量大的情况建议设置DOWNLOAD_DELAY 1避免请求频率过高被限流。爬虫运行结束后回到后台的院校管理或录取数据页面刷新就能看到新增记录。4.7 部署后的验证路径最终验收时按这条路径走一遍打开考生端登录页注册一个测试账号登录后先查询院校信息和历史录取数据看数据是否完整做一遍 MBTI 测评确认推荐专业有返回进入模拟填报输入目标分数保存志愿表切到管理端用管理员账号查看院校管理列表确认爬虫写入的数据已经可见。这条路径全部通过说明前后端、数据库、爬虫四个模块已经协同工作。5. 常见问题与避坑指南5.1 前端页面能打开接口全部 404现象页面正常渲染但所有列表接口返回 404 或 Proxy error。原因开发模式下 Vue 的 devServer 没有把接口请求转发到后端或者转发路径和后端接口前缀对不上。解决检查前端工程根目录的vue.config.js确认devServer.proxy配置。常见写法是把/api代理到http://localhost:8080同时 axios 的 baseURL 也设为/api两边必须一致。从前端 console 看到请求路径再反查代理配置即可。5.2 爬虫跑完数据库里没有数据现象scrapy crawl admission正常结束但表里一条新记录都没有。原因页面选择器写错了parse_detail里拿到的节点全部为空Pipeline 保存了空值或直接跳过也可能是目标站返回了验证码页Spider 没带必要的请求头。解决先用 Scrapy Shell 请求一个详情页逐步调试选择器确认页面能拿到字段后设置合适的 User-Agent 和下载延迟。如果目标站反爬严格先降低并发再考虑增加请求间隔。5.3 中文乱码院校和专业名称变成问号现象爬虫写入或手动导入的数据显示为???页面渲染乱码。原因数据库表字符集不是utf8mb4或 JDBC 连接串没有指定characterEncoding。解决建库时用DEFAULT CHARSET utf8mb4导入前检查表字符集application.yml里数据库 URL 加上useUnicodetruecharacterEncodingutf8mb4如果已经写入乱码把表 drop 掉重新导入不要只改连接串已损坏的数据不会自动恢复。5.4 Spring Boot 启动报数据库连接错误现象启动日志抛出连接异常或者提示Access denied for user。原因配置里的用户名、密码与本地 MySQL 不一致或驱动版本不兼容。解决先手动执行mysql -uroot -p123456确认本地账号密码驱动版本方面JDK 1.8 MySQL 5.7 推荐使用 8.x 驱动检查 pom 里mysql-connector-java版本。如果本地装的是 MySQL 8com.mysql.cj.jdbc.Driver同样适用时区参数必须配置。5.5 数据库有数据但推荐结果为空现象模拟填报接口返回空列表但录取数据表里明显有记录。原因候选查询过滤条件过严比如位次范围 0.8 到 1.2 倍没有命中任何数据或省份、科类、年份参数不匹配。解决先在数据库里用同样的条件手动查询确认结果是否存在如果没有放大findByRankRange的范围倍数或把前端筛选条件的默认值改成数据库中存在的数据。推荐功能拿不到数据时先怀疑条件再怀疑代码。6. 二次开发把推荐规则改成你自己的版本二次开发最值得动的就是推荐算法接口。当前RecommendService是一次性写死的打分逻辑改造时把打分拆成几个可插拔的策略每种策略实现同一个接口。比如定义RecommendStrategy下面有RankStrategy位次优先、InterestStrategy兴趣优先、MbtiStrategy性格优先再用 Spring 注入一个策略列表在 Service 里按顺序组合。Component public class RecommendService { private final ListRecommendStrategy strategies; public RecommendService(ListRecommendStrategy strategies) { this.strategies strategies; } public ListRecommendResult recommend(RecommendRequest req) { ListRecommendResult results recommendCandidates(req); for (RecommendStrategy strategy : strategies) { strategy.applyScore(results, req); } results.sort(Comparator.comparing(RecommendResult::getScore).reversed()); return results.subList(0, req.getTopN()); } }这样改完之后加一个新策略只需要写一个实现类不用动主流程。验证方法也简单拿前一年的位次数据去跑推荐把推荐结果前 5 个院校和当年实际录取最低分做对比命中率能说明策略是否有效。常见效果是从“用位次框范围”升级为“位次框范围 专业热度 地域偏好”的组合推荐。知识图谱那边同样有扩展空间。现在用graph_node/graph_relation存实体和关系后续可以加“院校热度”“就业率”作为节点属性在推荐打分时读取。爬虫侧可以新增一个 Spider 专门抓招生计划再通过 Pipeline 写入招生计划表。改数据源时注意统一字段映射不要直接把新站字段塞进库里先做一个转换层后面维护会轻松很多。改完推荐逻辑之后我养成了一个习惯每次调参前先导出一份历史数据快照用同一批数据跑一遍旧逻辑和新逻辑对比结果差异。推荐这类功能最容易出现“改一处崩一片”的情况从那以后我每次都会强制走一遍全链路验证用测试账号走完查询、测评、模拟填报、保存志愿表四步。设定合理的验证指标比写出优雅的代码更重要。希望帮到你。本文还有配套的精品资源点击获取