基于Spring Boot爬虫与位次匹配的高考志愿智能推荐系统实现

基于Spring Boot爬虫与位次匹配的高考志愿智能推荐系统实现 简介高考志愿智能推荐系统是一套基于SpringBoot与爬虫技术开发的完整Java毕设项目适合毕业设计、课程设计及Java Web进阶学习者使用。系统覆盖用户注册登录、学生信息录入、院校与专业数据库、智能推荐算法及志愿填报模拟等核心模块支持学生、家长和老师多角色登录并可依据个人成绩、兴趣特长及院校录取数据生成个性化志愿方案与风险提示。资源共669个文件压缩包约26.3MB以Java后端源码、Vue前端页面、Python爬虫脚本、SQL数据库文件及配置文件为主同时包含毕业论文与答辩PPT并附启动运行脚本方便直接部署与二次开发。已有116人学习下载项目目录结构清晰从数据采集、智能计算到前端展示均有完整实现是理解整站开发流程和算法落地的优质参考。1. 高考志愿智能推荐为什么值得用 Spring Boot 重新做一遍6 月 23 日出分7 月初填报截止留给考生的决策时间往往不到一周。大部分考生手里只有一本厚几百页的报考指南查位次、翻院校、对比专业全靠手动。更麻烦的是每年的试题难度不同直接拿今年的分数比对去年分数线没有意义真正可比的是位次。把院校和专业的历史录取位次抓下来存进数据库再按考生位次做冲稳保匹配这个思路并不新鲜难点在于数据从哪来、匹配逻辑怎么写、以及系统能不能在填报高峰期扛住并发。本文要聊的这套“高考志愿智能推荐-JAVA-基于springBoot爬虫高考志愿智能推荐系统”是一个典型的 Spring Boot 全栈实践后端用 JAVA 和 Spring Boot 写推荐接口用爬虫把公开的历年录取数据抓进库里最后按位次输出冲稳保志愿列表并配套毕业论文和 PPT。适合正在做选题的计算机专业学生也适合想在一个项目里同时练爬虫、关系型数据库设计和推荐算法的工程师。2. 爬虫层怎么设计Spring Boot 里采集院校与分数线数据的可行姿势2.1 为什么在 Spring Boot 工程里直接写 Java 爬虫不少团队习惯把数据采集单独做成 Python 爬虫服务毕竟 requests 和 Scrapy 上手快。但这个项目是单体 Spring Boot 应用爬虫只是其中一个模块我一般建议直接用 Java 写理由很实际语言统一、部署简单、线程模型和主应用一致。爬虫抓到的数据要写入同一个 MySQL如果拆成两个服务还要考虑它们之间的接口对接、定时调度、异常重传毕设答辩时反而多出一堆解释成本。数据源选择上优先抓省教育考试院、阳光高考平台和院校官网公布的历年录取统计这些页面结构相对稳定而且公开信息在合理频率下抓取没有法律风险。不要碰需要登录才能看的会员数据也不要解析验证码、对抗前端加密那些内容超出了毕设范畴。这个项目的核心价值在于“把公开数据整理成可查询的结构化表格”不是秀反爬技巧。2.2 用 RestTemplate 加 Jsoup 写最小抓取代码Spring Boot 项目里加两个依赖就行spring-boot-starter-web自带的 RestTemplate 负责发请求org.jsoup:jsoup负责解析 HTML。下面这段代码演示抓取某个院校历年录取分数线表格的第一版实现Service public class ScoreCrawlerService { private final RestTemplate restTemplate new RestTemplate(); public ListAdmitScore crawlAdmitScores(String url) { // 1. 请求目标页面拿到 HTML 字符串 String html restTemplate.getForObject(url, String.class); if (html null || html.isBlank()) { return List.of(); } // 2. 用 Jsoup 解析成 Document按选择器定位到表格行 Document doc Jsoup.parse(html); ListAdmitScore list new ArrayList(); for (Element row : doc.select(table tr)) { Elements td row.select(td); // 约定表格列为年份、省份、院校、科类、最低分、最低位次 if (td.size() 6) { continue; } AdmitScore score new AdmitScore(); score.setYear(Integer.parseInt(td.get(0).text().trim())); score.setProvince(td.get(1).text().trim()); score.setSchoolName(td.get(2).text().trim()); score.setCategory(td.get(3).text().trim()); score.setMinScore(Integer.parseInt(td.get(4).text().trim())); score.setMinRank(Integer.parseInt(td.get(5).text().replace(,, ).trim())); list.add(score); } return list; } }这段代码是爬虫模块的骨架逻辑上分三步发请求拿 HTML、按 CSS 选择器定位数据行、逐列解析成实体对象。参数说明里有两个容易踩的坑td.get(4).text()拿到的分数可能是“589”也可能是“589.0”建议在实体里用 BigDecimal 或先转字符串再处理位次字段普遍带千分位逗号和小数点需要做兼容否则Integer.parseInt会直接抛异常。此外doc.select(table tr)只适合页面里只有一张数据表的情况如果页面里有多个表格要给目标 table 加 id 或 class再用doc.select(#scoreTable tr)精确定位。2.3 去重、增量更新与数据表怎么设计爬虫最容易产出的问题不是抓不到数据而是重复抓。定时任务每天跑一遍同一个院校同一个年份的分数线就会被插入两次。常见做法是在数据库层面建唯一索引兜底同时在代码里先查后插CREATE TABLE admit_score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, school_name VARCHAR(128) NOT NULL, province VARCHAR(32) NOT NULL, category VARCHAR(16) NOT NULL COMMENT 物理类/历史类/理科/文科, year INT NOT NULL, min_score INT NULL, min_rank INT NULL, avg_score INT NULL, batch VARCHAR(32) NULL COMMENT 本科批/提前批, update_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_school_province_year (school_name, province, category, year, batch) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;uk_school_province_year这个唯一索引是关键它直接挡住了同一院校同一省份同一科类同一年份的重复记录。第一次全量抓取时用INSERT IGNORE之后每天定时任务只抓最近一年的页面用INSERT ... ON DUPLICATE KEY UPDATE min_score VALUES(min_score)实现“存在即更新”的增量逻辑。注意VALUES()写法在 MySQL 8.0.20 之后被标记为废弃建议改成AS new ON DUPLICATE KEY UPDATE min_score new.min_score。院校信息表和专业录取表同理核心思路是“主键用自增 id业务唯一性靠联合唯一索引保证”。数据量级不需要分库分表单表百万行以内加好索引的 SQL 查询毫秒级返回。3. 推荐引擎按位次匹配院校专业冲稳保梯度怎么算3.1 位次法为什么比分数法可靠志愿填报行业里流传一句话分数看绝对值位次看相对值。2023 年理科一本线 520 分你考了 560 分对比 2022 年的一本线 505 分是不是更有把握很难说因为试卷难度和考生人数都变了。位次解决了这个问题你考了 560 分全省排名第 22000 名那就去看往年哪些院校的录取最低位次在 22000 名附近这些院校的录取难度和你是匹配的。推荐引擎的核心不是好看的前端页面而是这个位次映射逻辑。算法上不需要上机器学习用区间匹配就够了把考生今年位次转换成往年的等效位次再和历史录取位次区间做重叠度计算。数据清洗阶段需要特别注意大小年现象也就是某院校一年爆热一年爆冷单独拿最近一年的位次容易误判稳妥的做法是取近三年位次的加权平均值当年权重给高一些。3.2 冲稳保梯度计算的最小实现冲稳保的本质是把考生位次放到目标院校录取位次的三个不同相对位置上。专业课可以接受的经验系数是冲的院校考生位次比院校往年录取中位次高 10% 到 30%也就是考生排名比该校常见录取排名更靠前一点但优势不大稳的院校位次基本重合录取概率最高保的院校考生位次明显优于院校历史平均水平用来兜底。量化成区间就是冲[0.7, 0.95]、稳[0.95, 1.1]、保[1.1, 1.5]系数是“院校历史录取位次 / 考生位次”注意这里位次数字越小排名越靠前。public ListRecommendVO recommendByRank(int studentRank, String province, String category, int topN) { // 1. 查询该省份该科类近三年的录取统计按院校聚合 ListSchoolScoreStat stats admitScoreMapper.findAvgRankBySchool( province, category, 3); ListRecommendVO result new ArrayList(); for (SchoolScoreStat stat : stats) { int avgRank stat.getAvgMinRank(); // 近三年最低位次均值 double ratio (double) avgRank / studentRank; String strategy; if (ratio 1.1) { strategy 保; // 院校录取位次明显低于考生位次稳兜底 } else if (ratio 0.95) { strategy 稳; } else if (ratio 0.7) { strategy 冲; } else { continue; // 差距太大不推荐 } RecommendVO vo new RecommendVO(); vo.setSchoolName(stat.getSchoolName()); vo.setStrategy(strategy); vo.setProbability(computeProbability(ratio)); vo.setAvgMinRank(avgRank); result.add(vo); } // 2. 按院校层次和历史就业口碑降序控制返回数量 result.sort(Comparator.comparing(RecommendVO::getStrategyOrder) .thenComparing(RecommendVO::getAvgMinRank)); return result.stream().limit(topN).collect(Collectors.toList()); }补充strategyOrder是给排序用的冲稳保的展示顺序通常是“冲”在前、“保”在后所以可以把冲映射为 0、稳为 1、保为 2。computeProbability不需要太复杂用正态分布近似即可比率越接近 1概率越高这套算法解释起来很简单答辩时能讲清楚。3.3 新高考专业组与调档比例的处理现在的填报规则已经从“院校投档”过渡到“院校专业组”同一个学校的不同专业组录取位次可以差出一大截。所以推荐结果的粒度必须是“院校 专业组”而不是只给学校名。我的建议是爬虫阶段就把专业组信息抓下来推荐时对同一学校的不同专业组分别计算冲稳保概率这样系统推荐的内容才真正能用。另外一个被忽略的参数是调档比例。部分省份按 105% 调档意味着超录名额里的考生存在退档风险所以“冲”的推荐里要加上“同意专业服从调剂”的提示。这个规则不要硬编码在代码里放到配置表里维护写论文时可以作为一个“规则配置化”的亮点。4. Spring Boot 项目分层落地与论文 PPT 配套4.1 一个最小可用的后端工程结构整个系统按标准的三层结构组织controller 层处理 HTTP 请求service 层放推荐算法和爬虫调度mapper 层操作 MySQL。工程目录建议拆成crawler、recommend、sysuser三个模块包这样论文里画技术架构图的时候可以对应到代码实际结构。application.yml里最容易出问题的不是端口号而是数据库连接池和时区spring: datasource: url: jdbc:mysql://localhost:3306/gaokao_db ?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver task: scheduling: pool: size: 4 jackson: time-zone: Asia/ShanghaiserverTimezoneAsia/Shanghai是必填项MySQL 8.x 的驱动如果不带时区参数跑起来会报 UTC 时间差 8 小时。spring.task.scheduling.pool.size设成 4 是因为爬虫和定时统计任务同时跑的场景比较少数字再大也吃不满。如果你用的是 Spring Boot 3.x注意javax.servlet相关包全部换成了jakarta.servlet低版本的视频教程代码直接粘过来会编译报错。4.2 推荐接口的暴露方式与页面前端不理想的公司和毕设做法是 Thymeleaf 模板直接渲染页面后端把推荐结果塞进Model返回。我建议把推荐结果做成独立 JSON 接口再配一个简单的静态页面调用它这样论文截图和演示环境都能稳定复现。示例接口RestController RequestMapping(/api/recommend) public class RecommendController { private final RecommendService recommendService; public RecommendController(RecommendService recommendService) { this.recommendService recommendService; } GetMapping(/plan) public ApiResultListRecommendVO plan( RequestParam Integer rank, RequestParam String province, RequestParam(defaultValue 物理类) String category, RequestParam(defaultValue 30) int topN) { // 入参校验位次必须大于0省份不能为空 if (rank null || rank 0) { return ApiResult.fail(位次必须大于0); } ListRecommendVO list recommendService.recommendByRank( rank, province, category, topN); return ApiResult.ok(list); } }JAVA 基础不扎实的同学在这里最容易犯的错是直接把 Service 层实例化在 Controller 里而不是通过构造器注入。Spring Boot 的依赖注入会管理 Service 的生命周期手动new出来的对象拿不到 MyBatis 代理的 Mapper运行时会报空指针。接口设计上位次 rank 用整型传输省份和科类用字符串前端下钻查询时可以把位次换算规则也交回后端处理。4.3 毕业论文与 PPT 怎么和代码对应毕业论文和 PPT 是这套系统交付物的另一半最容易掉进去的坑就是论文写成“软件使用说明书”。合理的组织方式是绪论里把志愿填报的痛点讲清楚用数据说明填报时间窗口有多短技术选型部分对比 JAVA 和 Python 爬虫的差异落到 Spring Boot 上是因为它生态成熟、部署成本低、事务控制方便系统设计部分放整体架构图从爬虫模块到 MySQL 再到推荐算法一级一级拆核心实现在线展示代码和日志截图最后用一份模拟考生数据做结果验证。PPT 控制在 15 到 20 页都行页数太少会显得工作量不足太多又讲不完。答辩时老师关注的核心问题是“你的推荐为什么可靠”所以准备两三个典型考生案例比如高分段、中分段、压线生各一个用截图展示系统推荐的冲稳保列表是否符合直觉。5. 高峰期三个实战问题并发设计、反爬节奏与 Spring Boot 版本适配5.1 爬虫并发设计不要把抓取任务和查询接口混用一个线程池志愿填报前几天用户查询量和爬虫执行时间会撞在一起。推荐接口的 QPS 可能冲到每秒几百次而爬虫每次请求要等对方服务器响应耗时从几百毫秒到几秒不等如果两个任务共用默认线程池慢请求会直接把接口线程占满表现为页面转圈、超时。我一般会给爬虫单独定义一个线程池并且限制并发数Configuration public class CrawlerThreadPoolConfig { Bean(crawlerExecutor) public Executor crawlerExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(2); executor.setMaxPoolSize(4); executor.setQueueCapacity(20); executor.setThreadNamePrefix(crawler-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); executor.initialize(); return executor; } }核心参数按爬虫特性设corePoolSize 设 2 而不是 8因为网站服务器的并发容忍度有限开太大只会加快被封maxPoolSize 设 4 是峰值时的弹性上限队列容量 20多余的请求直接丢弃保证主流程不被拖垮。DiscardOldestPolicy在这里是合理的因为爬虫任务是可以重复执行的定时任务丢掉最旧一次问题不大和用户请求线程池的拒绝策略完全不同。5.2 被网站拦截时先检查节奏再配置重试爬虫跑着跑着突然全量失败日志里出现 403 或 429大概率是访问频率太高。第一反应不是去绕验证码而是把 HTTP 请求节奏降下来每次请求后随机 sleep 2 到 5 秒设置随机的 User-Agent 列表并且给每次请求写日志方便定位。重试次数控制在 3 次以内重试间隔用指数退避。private String doGet(String url, int retryCount) throws Exception { try { HttpHeaders headers new HttpHeaders(); headers.set(HttpHeaders.USER_AGENT, randomUserAgent()); HttpEntityString entity new HttpEntity(headers); ResponseEntityString resp restTemplate.exchange( url, HttpMethod.GET, entity, String.class); if (resp.getStatusCode().is2xxSuccessful()) { return resp.getBody(); } throw new RuntimeException(HTTP status: resp.getStatusCode()); } catch (Exception e) { if (retryCount 3) { throw e; } int sleepMs 1000 * (int) Math.pow(2, retryCount); Thread.sleep(sleepMs); return doGet(url, retryCount 1); } }这套重试策略在面试里能聊出东西为什么 sleep 用指数退避而不是固定时间为什么最多重试 3 次。固定时间会让多个失败请求在同一个时间点同时重试形成新的波峰指数退避让重试之间拉开差距配合随机延迟能有效降低同期请求密度。5.3 Spring Boot 版本太高引起的连环坑很多同学跟着教程新建项目时IDEA 默认选的 Spring Boot 3.x结果 JDK 还在 8跑起来直接报错。Spring Boot 3.x 强制要求 JDK 17如果机器上装的是 JDK 8用了很多兼容包的代码会全面编译失败。建议毕设团队统一使用 Spring Boot 2.7.x 加 JDK 8网上能找到的绝大多数教程和依赖版本都能对上愿意折腾的再用 JDK 17 配 Spring Boot 3.x。切换后注意数据库驱动和 JSON 库版本的连带变化比如某人在 2.7 里正常返回的 LocalDateTime 字段升级到 3.2 后序列化格式可能变成一串数字需要在application.yml里显式声明 Jackson 的日期格式把坑填上。本文还有配套的精品资源点击获取