ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Hadoop+AI大模型构建兼职聚合推荐平台全解析

SpringBoot+Hadoop+AI大模型构建兼职聚合推荐平台全解析 去年帮一位学弟救火毕业设计题目就是“基于SpringBoot大数据爬虫Hadoop智能AI大模型的兼职聚合与个性化推荐平台”。一眼看过去SpringBoot、Hadoop、爬虫、AI大模型四个关键词堆在一起表面像个技术全家桶但真正拆解下来这套系统其实是一条非常完整的数据链路爬虫负责把分散的兼职信息抓下来Hadoop负责存储和离线统计SpringBoot对外提供API和业务能力最后AI大模型在数据基础上做个性化推荐。整个项目既有业务场景又有工程深度做出来放在简历里也非常能打。这篇博文我就把这个项目的整体设计、核心实现、以及那些在文档里根本不会写的坑完整梳理一遍给你一个可以直接套用的模板。1. 项目整体设计从业务痛点倒推技术选型1.1 兼职聚合到底要解决什么问题先想清楚业务场景。兼职市场需求一直存在但信息极度分散综合性招聘平台有自己的兼职频道垂直兼职网站有独立App本地生活社区、校园论坛、公众号也会不定期发一些零散的招人信息。对找兼职的学生和上班族来说逐个平台去翻效率太低而且不同平台的信息格式、更新频率、真实度都不一样。一个聚合平台的价值就在这里把分散的岗位信息统一采集、清洗、结构化让用户在一个入口就能看到全网可用的兼职机会。这个需求听起来简单落地却有讲究。聚合平台不是简单把数据堆在一个列表里还要考虑信息过期的问题。兼职岗位的生命周期很短今天挂出来的服务员、发单员、展会协助可能一周后就招满了。所以在做技术方案的时候就必须把“增量采集”和“过期下架”作为核心功能来设计而不是一次性爬完就完事。这也是为什么架构里需要一个定时任务调度器来驱动爬虫周期运行。1.2 技术栈的分工逻辑每一个组件都有明确职责这个项目技术栈多但如果把它们当成一个流水线来看逻辑就非常清晰。爬虫我用Python实现负责数据采集产出的是半结构化、带各种噪声的原始数据Hadoop负责对海量原始数据进行存储和离线分析跑出统计结果和数据快照SpringBoot作为后端主框架负责用户管理、职位查询、行为上报这些在线业务同时承担定时任务的调度入口AI大模型负责推荐引擎把用户的偏好和职位描述做语义匹配输出个性化的职位列表。整条数据流是定时调度触发爬虫 → 原始数据先落临时存储 → 写入HDFS → MapReduce离线清洗和统计 → 结构化结果写入MySQL → 用户通过SpringBoot API访问职位数据 → 用户行为浏览、收藏、投递记录回流 → 推荐服务读取行为数据和职位特征 → 大模型计算打分 → API返回推荐列表。这套链路设计下来每一个技术组件都找到了它不可替代的位置用这种思路去做答辩时解释技术选型也远比你背“因为××技术很流行”要有说服力得多。1.3 为什么用Hadoop而不是只用MySQL很多人会问只有一万多条数据MySQL完全能存为什么要绕一圈Hadoop这个问题在答辩时也几乎必问。我的理解是Hadoop在这个项目里承担的是“离线数据仓库”的角色。爬虫采集的原始数据量级虽然不大但格式很脏JSON、HTML片段、半截描述混在一起不适合直接喂给业务数据库。把这些原始数据放在HDFS里既保留了最底层的原始现场又能用MapReduce批量处理成干净的结构化数据。另一方面平台如果后续扩充数据源每天新增几万条原始记录MySQL直接扛业务读写就会吃力但HDFS不需要管索引和事务横向扩展也容易。从学习和演示的角度来说Hadoop跑通一条完整的“落地→清洗→分析→回读”链路本身就是这个项目最有含金量的部分。2. 数据采集层爬虫模块的设计与数据治理2.1 采集目标与页面解析策略爬虫第一步是确定数据源。我用的是几个公开招聘平台的兼职频道和两个垂直兼职站点全部是公开信息页面。需要注意爬虫只抓取公开可访问的列表页和详情页不涉及需要登录才能看到的非公开信息更不碰用户个人数据这个边界从一开始就要在代码里明确下来除了合规问题也避免平台方的法律风险。解析方案用的是Requests XPath。Requests负责发请求拿HTMLXPath负责定位页面节点。比如职位列表页的每条记录通常都包在一个特定的div或li节点里通过XPath路径就能精确抽取标题、公司、薪资、地点这些字段。XPath的核心逻辑可以这样写import requests from lxml import html def parse_job_list(page_html): tree html.fromstring(page_html) items tree.xpath(//div[contains(class, job-item)]) jobs [] for item in items: title item.xpath(.//span[contains(class, job-title)]/text()) company item.xpath(.//div[contains(class, company-name)]/a/text()) salary item.xpath(.//span[contains(class, salary)]/text()) if title and company and salary: jobs.append({ title: title[0].strip(), company: company[0].strip(), salary: salary[0].strip(), source_url: item.base_url }) return jobs解析策略上有一个经验先抓列表页拿到每条职位的详情页链接再并发请求详情页补充描述、标签等信息。但详情页请求量大频率控制不好容易被封所以我的做法是列表页正常抓详情页只对前N条做增量补充剩下的字段用规则从列表页信息中扩展。2.2 数据清洗与结构化存储原始数据里能直接扔进数据库的很少。薪资字段是最典型的例子“200-300元/天”“面议”“15元/小时”“月薪4K-6K”各种格式都有必须统一解析成最低值和最高值字段。地点字段也要归一化把“北京朝阳望京”“朝阳区”统一映射到城市和区域的标准化格式。标签抽取使用关键词库加简单规则匹配比如描述里出现“日结”就打上“日结”标签出现“学生”就打上“学生兼职”标签。清洗后的数据模型大概是这样字段说明示例job_id唯一标识URL哈希生成8f3a2c9etitle职位名称展会现场协助company公司名称某某文化传媒salary_min最低日薪200salary_max最高日薪300location标准化城市/区域北京-朝阳tags逗号分隔标签日结,学生,短期publish_time抓取到的发布时间2025-06-12source来源平台标识58source_url原始链接https://...清洗之后的数据分成两条路一条写入MySQL供在线业务查询一条保留原始格式传入HDFS作为后续离线分析的输入。这里是整个项目数据流中承上启下的一环写代码的时候不要把清洗逻辑和采集逻辑写在一起采集程序只负责产出原始数据清洗任务单独跑这样即便解析规则变化也不需要重新抓取。2.3 反爬应对与采集稳定性保障爬虫最怕的就是跑两天被限制访问。我的经验是优先做好“伪装”和“节流”。User-Agent随机轮换是必须的最好维护一个常见的浏览器UA池每次请求间隔控制在2到5秒随机既不会太慢也不会触发频率限制。如果设计目标是一万条数据以单线程加随机延时的速度大概需要几个小时跑完完全可以接受不必追求高并发。代码里还需要做失败重试机制。网络超时、页面结构临时调整、服务器返回错误码都是爬虫家常便饭。重试两次、间隔递增是比较稳妥的方案。采集过程中我用了简单的断点记录每爬完一个列表页就把当前页码记录到本地文件进程意外终止后可以接着跑不用从头开始这个细节在实际调试中省了很多时间。3. 大数据处理层Hadoop在项目里的真实角色3.1 HDFS存储与MapReduce离线分析Hadoop在毕设项目里最常见的误区是想让它“全干”既做存储又做实时查询结果什么都做不好。这个项目里我给它定的角色非常明确HDFS负责保存爬虫采集的原始数据文件和系统运行日志MapReduce负责跑离线统计分析。统计分析的任务也很具体统计各城市职位数量并排序、统计各薪资区间的职位分布、统计各标签的频次Top20、按周统计新增职位趋势。这些统计结果输出到HDFS的指定目录SpringBoot再通过HDFS API读取结果文件提供给前端做可视化展示。这个设计让Hadoop真正参与到业务中而不是装个环境跑个wordcount就结束。3.2 伪分布式环境搭建与配置细节Hadoop搭建版本选的是3.3.x直接跑伪分布式模式这样一台机器就能完整运行NameNode、DataNode、ResourceManager和NodeManager四个核心进程。装好JDK配置JAVA_HOME后核心工作都在四个配置文件和三个启动步骤上core-site.xml里设置HDFS文件系统的访问地址configuration property namefs.defaultFS/name valuehdfs://localhost:9000/value /property /configurationhdfs-site.xml里设置副本数为1伪分布式只有一个DataNode默认3会一直报副本缺失告警configuration property namedfs.replication/name value1/value /property property namedfs.namenode.name.dir/name value/home/hadoop/data/namenode/value /property property namedfs.datanode.data.dir/name value/home/hadoop/data/datanode/value /property /configurationyarn-site.xml里把资源调度和节点管理器的地址配好configuration property nameyarn.nodemanager.aux-services/name valuemapreduce_shuffle/value /property property nameyarn.resourcemanager.hostname/name valuelocalhost/value /property /configuration启动顺序是大坑第一次启动前必须先执行 hdfs namenode -format 格式化然后依次用 start-dfs.sh 和 start-yarn.sh 启动。很多人格式化之前没注意配置文件已经改好启动后NameNode一直起不来其实八成是core-site.xml的地址和格式化时的默认配置不一致导致的。启动完成后用 jps 命令检查进程能看到四个核心进程就说明环境没问题。3.3 清洗后的Hadoop落地流程爬虫采集的原始JSON文件我通过HDFS命令行工具直接上传hdfs dfs -mkdir -p /data/job/raw hdfs dfs -put ./raw_jobs_20250612.json /data/job/raw/然后提交MapReduce任务做清洗统计。MapReduce任务在纯Java环境下写起来比较啰嗦但Hadoop生态支持更轻量的方案比如直接用Hive或者PySpark。不过考虑到这个项目本身就是SpringBoot为主我更推荐用Hadoop自带的Streaming接口用Python直接写Mapper和Reducer代码量小、读起来也直观。Mapper端按城市维度做计数#!/usr/bin/env python3 import sys for line in sys.stdin: data line.strip().split(,) if len(data) 5: city data[3] print(f{city}\t1)Reducer端做汇总#!/usr/bin/env python3 import sys city_count {} for line in sys.stdin: city, count line.strip().split(\t) city_count[city] city_count.get(city, 0) int(count) for city, count in sorted(city_count.items(), keylambda x: x[1], reverseTrue)[:20]: print(f{city}\t{count})运行命令hadoop jar /path/to/hadoop-streaming.jar \ -input /data/job/raw/ \ -output /data/job/stat_city \ -mapper mapper.py \ -reducer reducer.py \ -file mapper.py -file reducer.py注意输出目录不能预先存在否则MapReduce会报错。这是Hadoop的一个常见限制每次跑之前先删掉上一次的输出目录或者用动态时间戳命名输出路径。4. 推荐引擎设计AI大模型如何实现个性化推荐4.1 从传统推荐到大模型增强的路线选择推荐模块是整份设计里加分最大的部分。传统做法是协同过滤或者基于内容的标签匹配但这类方法有明显的瓶颈用户偏好和职位描述都是短文本、稀疏矩阵协同过滤在用户行为数据不够时几乎无法生效。所以我在项目里走了“传统推荐打底大模型增强排序”的混合路线。具体做法是先用内容匹配从职位库里召回一个候选集比如80条再调用AI大模型对候选集进行细粒度打分排序。这样既避免了大模型逐个扫描全量职位的性能问题也解决了协同过滤冷启动收到干扰的问题。这个设计在答辩时讲出来技术深度明显高一个层次。4.2 用户画像与行为特征工程用户画像的数据来源有三个注册时的主动选择、行为记录、以及推荐后的反馈。注册表单里让用户选城市、意向岗位类型、期望薪资区间、可工作时段这是最干净的偏好来源。行为记录包括浏览、收藏、投递三种权重各不相同投递权重最高收藏次之浏览最低。用户向量由这些行为触达的职位向量累加获得。职位向量则来自职位标签的One-Hot编码加描述文本的语义向量拼接。这个设计即使不用大模型单做协同过滤也具备一定的推荐合理性大模型是在这个基础上做增强。4.3 大模型接入的三种实现方案我实测下来有三条路线可以走成本和效果各有取舍。方案一调用云端API。将职位描述和用户偏好拼接成提示词让模型输出一个打分。这种方式效果最好但每次请求都有网络耗时和费用。适合原型演示不适合高并发场景。我当时用的逻辑是先用传统算法筛出Top50再调用API对这50条做精排控制在每次请求只处理一批用户体感基本无感。方案二本地部署开源模型。在本地跑一个参数量较小的模型例如Qwen系列或ChatGLM系列的小版本用于文本向量化和意图理解。该方案免费、离线可用但需要一台配置过得去的机器至少在16GB内存以上才能流畅运行7B级别的模型。模型加载后常驻内存通过HTTP接口提供服务。方案三折中方案只把大模型用于“推荐理由生成”和“语义扩展”。推荐排序用传统算法大模型只负责给每条推荐结果生成一句个性化的推荐理由比如“根据你最近浏览的展会协助类岗位推荐这个朝阳区的短期活动执行职位”。这样大模型的调用频次大幅降低响应时间可控但用户在页面上能直观感受到“AI推荐”的存在感产品体验提升明显。我在正式版里实现的是方案三同时在后台预留了方案一的接口。核心打分逻辑可以这样简化描述// 推荐服务伪代码 public ListJob recommend(Long userId, int topN) { // 1. 召回基于标签匹配取候选集 ListJob candidates jobService.matchByTags(userId, 80); // 2. 粗排基于行为加权打分 candidates.sort((a, b) - scoreByBehavior(userId, b) - scoreByBehavior(userId, a)); ListJob topK candidates.subList(0, Math.min(50, candidates.size())); // 3. 精排大模型语义评分 ListJob ranked llmService.rerank(topK, userProfile.get(userId)); return ranked.subList(0, Math.min(topN, ranked.size())); }精排的提示词模板大致长这样你是兼职推荐助手请根据用户偏好描述和职位信息为每个职位给出0到100的匹配分。 用户偏好{user_profile} 职位列表 1. {job_1_info} 2. {job_2_info} 请只输出每项的匹配分数格式为编号:分数模型返回后再将分数解析出来排序。这里有个细节大模型输出格式不稳解析时最好用正则提取数字而不是指望它严格按格式输出。我一开始让模型直接返回JSON结果偶尔出现括号、中文描述混进去解析直接报错改成纯数字格式后稳定了很多。5. SpringBoot服务端与系统集成5.1 后端分层与核心API设计SpringBoot端我采用了标准的Controller-Service-Mapper三层结构。核心API集中在五个接口用户注册登录、职位搜索、职位详情、推荐列表、行为上报。职位搜索接口支持多条件组合筛选包括城市、岗位类型、薪资区间、标签等RestController RequestMapping(/api/jobs) public class JobController { Autowired private JobService jobService; GetMapping(/search) public ResultPageResultJobVO search(JobQuery query) { // query包含city、tag、salaryMin、salaryMax、page、size PageResultJobVO page jobService.search(query); return Result.success(page); } GetMapping(/detail/{jobId}) public ResultJobVO detail(PathVariable String jobId) { return Result.success(jobService.getById(jobId)); } }行为上报接口用了异步处理用户点击职位、收藏职位、投递职位三个行为都调用同一个接口后端把行为写入消息表后立即返回通过线程池异步刷新用户画像避免写行为日志拖慢主流程。这个设计虽然简单但体现了对高并发场景的基本意识。5.2 定时任务驱动爬虫与大数据流程SpringBoot是整个系统的调度中枢。我配置了一个定时任务每天凌晨2点启动采集流程Component public class DataSyncTask { // 每天凌晨2点执行 Scheduled(cron 0 0 2 * * ?) public void runCrawler() { // 1. 调用Python爬虫脚本采集增量数据 ProcessBuilder pb new ProcessBuilder(python3, /opt/crawler/main.py); Process process pb.start(); process.waitFor(); // 2. 对采集结果做清洗入库 crawlerDataService.cleanAndImport(); // 3. 将原始数据上传HDFS hdfsService.upload(/data/job/raw/); // 4. 提交MapReduce统计任务 mapReduceService.submitStatJob(); } }用ProcessBuilder调Python脚本有一个坑Python脚本的print输出如果量大会阻塞进程缓冲。我用脚本重定向输出到日志文件解决了。SpringBoot项目里执行外部脚本时一定要设置好工作目录和超时时间不然脚本卡住会一直占着定时任务线程。5.3 前端集成与部署细节前端用Vue Element UI EChartsECharts用来展示MapReduce统计出来的可视化数据。开发阶段前后端分离接口走代理访问SpringBoot。正式部署时直接把Vue项目打包后的dist目录复制到SpringBoot的resources/static下这样就是一个单体应用部署简单也不需要单独配Nginx。这里要注意Vue Router的History模式问题打包放进SpringBoot后如果用户直接刷新某个子路由页面会出现404。解决方法是写一个路由转发规则把非API路径的请求都转发到index.html。还有一个跨域问题迎刃而解的细节打包后资源走的是同源不需要再配CORS但本地联调阶段还是要配允许跨域所以我在项目里保留了两种环境的配置用Spring profile切换开发环境允许跨域生产环境关闭。6. 常见问题与排查技巧实录6.1 爬虫采集零数据或字段缺失爬虫最容易踩的坑就是页面改版导致XPath失效表现是程序不报错但解析出来都是空列表。排查方法是先单独打印一个页面的HTML确认目标节点的class名称是否发生变化。另一个常见问题是页面内容用了JavaScript动态渲染Requests直接请求拿不到数据。遇到这种页面要么换数据接口很多站点前端会调后端JSON API要么用Selenium或Playwright做渲染但后者开销大尽量优先找页面内嵌的JSON数据。6.2 Hadoop进程起不来或频繁失联NameNode启动失败九成是格式化时机不对。第一次格式化前检查dfs.namenode.name.dir配置的目录是否存在且为空如果格式化后修改了core-site.xml的端口或路径必须删除原数据目录再重新格式化否则会出现ClusterID不一致。DataNode进程起不来的另一个原因是磁盘空间不足HDFS传输超过1GB的任务时默认会占大量临时空间建议把临时目录指到磁盘空间充足的路径在hdfs-site.xml里配置dfs.datanode.data.dir时选择大分区。6.3 大模型服务响应超时本地部署模型首次请求会加载权重可能要等十几秒甚至几十秒。实际使用时我做了三件事启动时预热模型、接口超时设置到30秒以上、推荐接口本身增加了缓存。缓存策略是同一用户一天内的推荐结果先查缓存只有用户行为发生变化时才重新计算。实测下来推荐接口平均响应时间从8秒降到200毫秒以内。6.4 MySQL查询慢与推荐冷启动一万条数据本身不多但如果搜索接口没有索引性能也会很难看。我在city、tag、salary_min三个字段上建了联合索引分页查询性能立刻上来。推荐冷启动问题主要靠热门职位兜底新用户没有行为数据时推荐列表先按城市匹配和发布时间倒序排再放大模型生成一段“为你精选热门兼职”的开场文案用户在视觉上不会觉得推荐是空的。6.5 常见问题速查表问题现象可能原因解决方法爬虫跑一会就被限流请求频率过高增加随机延时降低单次任务采集量XPath解析为空页面改版或模块加载单页调试查看当前页面DOM结构NameNode启动失败数据目录异常或配置不一致清空数据目录重新格式化YARN任务卡住资源不足或Queue配置错误检查内存配置降低Map/Reduce数量大模型首请求慢模型加载服务启动时预热增加超时时间推荐的职位不是用户想要的用户画像稀疏结合行为反馈降低非活跃标签权重前端刷新404History路由未处理添加转发规则重定向index.htmlJVM内存溢出爬虫数据批量处理过大分批处理适当调大Xmx参数整条链路跑通之后这个项目就不是几个技术栈的简单拼接而是一个能自圆其说的数据应用系统。我个人实际操作中最大的体会是这类多技术栈项目最难的不是某个单独模块而是数据在各模块之间如何顺滑流动。爬虫产出的数据是否符合Hadoop输入格式MapReduce输出的统计结果能否被数据库消费用户行为能否及时回流到推荐模型——这些才真正花时间。建议你自己做的时候先把一条最小链路跑通一次采集、一次清洗、一次HDFS落地、一次统计分析、一次推荐召回然后再横向扩展数据源和优化细节整个系统复杂度的提升就会非常自然。
返回列表