
简介这是一套基于协同过滤算法实现的招聘信息推荐系统面向计算机专业本科生及Web开发初学者适用于毕业设计、课程设计与工程实训等实践场景。系统采用Django后端框架与Vue前端框架构建集成自研爬虫模块spider实现岗位数据动态采集并通过用户行为建模完成个性化职位推荐有效解决求职者信息过载与匹配低效问题。压缩包共508个文件涵盖46个核心Python业务逻辑文件、71个Vue组件页面、42张JPG设计图、41个JS交互脚本及2个SQL数据库初始化脚本辅以bat一键安装/运行脚本和备份文件整体大小为23.35MB结构清晰、模块解耦度高。已有83人学习下载资源提供可直接运行的完整源码、MySQL5.7兼容数据库文件及配套论文LW并包含个人中心、招聘管理、留言互动与系统配置等全功能后台便于快速部署与二次开发。 5p125基于协同过滤算法的招聘信息推荐系统_djangospider.zip这个标题乍一看像是毕设项目压缩包但细拆下来其实是一套完整的工程链路爬虫采集数据、协同过滤计算推荐、Django做服务化输出。我把它完整复盘了一遍包括选型理由、踩坑点和可以复用的代码片段分享给正在做推荐系统或者想入行爬虫Web开发的朋友。1. 为什么我会把招聘推荐拆成爬虫、算法、Web三层来做很多人在看到推荐系统时第一反应是打开MovieLens数据集跑一个SVD或者ItemCF输出TopN榜单就算完事。但招聘领域完全不是这么回事。招聘数据没有公开的标准数据集岗位更新快、地域属性强、行业术语杂你要做推荐第一步根本不是算法而是数据从哪来。我最初的方案里数据来源是手动整理Excel但很快就发现不可行招聘网站的岗位信息每周变化量很大职位下线、薪资调整、技能要求变动都很频繁人工维护根本跟不上。这时候自然想到用爬虫来做数据管道于是项目的技术栈就定下来了爬虫层spider负责从公开招聘页面采集岗位数据包括岗位名称、公司、城市、薪资、经验要求、技能标签、职位描述等核心字段。存储与业务层DjangoDjango自带ORM、Admin后台和模板引擎非常适合快速把推荐结果以Web服务的形式输出。用户注册、收藏职位、投递记录这些行为数据也需要一个业务系统来承接。推荐引擎协同过滤基于用户历史行为收藏、投递、浏览时长构建用户-岗位交互矩阵用协同过滤算法计算相似岗位或相似用户产出推荐列表。这套三层结构的好处是边界清晰爬虫挂了不影响推荐接口算法迭代也不动Web框架。如果你只是做一个Demo完全可以把所有代码塞进一个脚本但一旦涉及真实用户、真实数据、真实部署分层是必须的。整个项目模块划分大概是模块主要职责关键依赖spider抓取招聘列表页/详情页、解析HTML、清洗入库requests, BeautifulSoup, lxmldata_clean去重、缺失值处理、薪资区间解析、技能词抽取pandas, jieba, rerecommend构建交互矩阵、相似度计算、TopN推荐numpy, scikit-learnweb用户系统、职位展示、收藏/投递行为、推荐接口Django, DRF, Redistask定时触发爬虫、定期重算相似度矩阵celery / cron不少人是先写Web再补算法我的建议是反过来先把数据管道跑通再做算法最后接Web。因为算法和接口都依赖数据形态数据字段一旦变动后面的代码全要跟着改。2. 数据层设计spider采集招聘信息时不能忽略的细节2.1 抓什么字段、存什么格式决定推荐上限爬虫不是爬到数据就行关键在于存什么。最初我只抓了岗位标题和公司名结果做协同过滤时发现维度太少用户收藏了两个Python开发我根本不知道这两个岗位的技能要求是否匹配只能靠标题文本硬算相似度效果惨不忍睹。招聘推荐场景里建议至少抓取以下字段岗位ID唯一标识去重和关联的基础岗位标题如高级后端开发工程师公司名称、公司规模、融资阶段工作城市、工作经验要求、学历要求薪资范围最好拆成min_salary和max_salary两个数值字段技能标签很多招聘页有现成标签如Python、Django、MySQL职位描述全文后续可以做文本向量化发布时间、职位状态在招/已下线用Django ORM定义模型时对应的Model大概长这样class Job(models.Model): job_id models.CharField(max_length64, uniqueTrue) title models.CharField(max_length128) company models.CharField(max_length128) city models.CharField(max_length32, db_indexTrue) min_salary models.IntegerField(nullTrue, blankTrue) max_salary models.IntegerField(nullTrue, blankTrue) experience models.CharField(max_length32) education models.CharField(max_length32) skills models.JSONField(defaultlist) description models.TextField() is_active models.BooleanField(defaultTrue) created_at models.DateTimeField(auto_now_addTrue)JSONField存技能列表是我测试下来最灵活的方式后面做相似度计算时可以直接取出技能集合做Jaccard相似度不用每次再去解析文本。2.2 爬虫实现上比框架更重要的是限速和容错爬虫方案上Scrapy和requestsBeautifulSoup我都用过。Scrapy的并发和管道设计很强大但对于一个中小型推荐系统如果只爬单个站点requestsBeautifulSoup反而更轻量调试也方便。下面这个片段是我实际在用的列表页解析逻辑import requests from bs4 import BeautifulSoup def parse_job_list(html): soup BeautifulSoup(html, html.parser) jobs [] for item in soup.select(.job-list-item): job_id item.get(data-jobid) if not job_id: continue jobs.append({ job_id: job_id, title: item.select_one(.job-title).text.strip(), company: item.select_one(.company-name).text.strip(), city: item.select_one(.job-area).text.strip(), salary: item.select_one(.salary).text.strip(), }) return jobs这里有两个容易被忽略的细节第一是限速一定要做。招聘网站的链路毕竟有反爬策略把请求间隔设在1到3秒随机波动比固定间隔更安全。同时设置重试机制请求失败后指数退避重试最多3次避免网络抖动导致整批数据丢失。第二是HTML解析的健壮性。招聘页面的HTML结构经常调整建议用CSS选择器而不是依赖固定的XPath路径并且对缺失字段做默认处理。我在实际运行中就遇到过某个城市分站的DOM结构与主站不一致导致整个列表解析为空的情况。数据清洗环节最核心的是薪资字段。招聘页面上的10k-20k是文本要做推荐过滤的时候必须转成数值。我用的切分逻辑def parse_salary(salary_str): import re if not salary_str: return None, None nums re.findall(r\d, salary_str) if len(nums) 1: return int(nums[0]), int(nums[0]) elif len(nums) 2: return int(nums[0]), int(nums[1]) return None, None注意面议和20k以上这类特殊情况一个返回None一个只取到最小值。不要让脏数据进入算法层否则算出来的相似度全是噪声。3. 推荐核心协同过滤算法的工程化落地3.1 用户-岗位交互矩阵到底怎么构建才靠谱协同过滤的基础是交互矩阵。电商场景里购买和加购就是很明确的正反馈信号但招聘场景的交互要复杂得多。我归纳了用户和岗位之间的四类行为搜索/浏览隐式反馈权重低但数量多收藏职位较强的正反馈说明用户有意向投递简历强正反馈这基本是求职者主动行为主动不看/标记不感兴趣负反馈可以用来修正推荐构建矩阵时我建议对不同行为赋不同的权重。比如浏览算1分收藏算3分投递算5分不感兴趣算-2分。这样同一个用户对不同岗位的喜爱程度就有了区分度。矩阵构建的代码片段import numpy as np from collections import defaultdict def build_user_job_matrix(interactions): user_to_idx {} job_to_idx {} rows, cols, values [], [], [] for user_id, job_id, behavior_weight in interactions: if user_id not in user_to_idx: user_to_idx[user_id] len(user_to_idx) if job_id not in job_to_idx: job_to_idx[job_id] len(job_to_idx) u_idx user_to_idx[user_id] j_idx job_to_idx[job_id] rows.append(u_idx) cols.append(j_idx) values.append(behavior_weight) n_users len(user_to_idx) n_jobs len(job_to_idx) matrix np.zeros((n_users, n_jobs)) matrix[rows, cols] values return matrix, user_to_idx, job_to_idx这里有个工程细节值得多说一句不要把矩阵直接存成稠密numpy数组。真实场景下用户交互记录非常稀疏10000个用户对50000个岗位交互可能只有几万条稠密矩阵会浪费大量内存。我建议用scipy.sparse或者直接存三元组user_id, job_id, weight计算时再按需构建稀疏矩阵。3.2 物品相似度计算ItemCF是我的首选招聘推荐场景里我最终选择了基于物品的协同过滤ItemCF而不是基于用户的协同过滤UserCF。原因有三个求职者的兴趣会随着职业发展阶段快速变化基于用户的相似性往往失真。岗位数量相对稳定岗位间的相似度可以离线计算好在线推荐时直接查表响应速度快。ItemCF更容易解释——你可以告诉用户因为你看过A岗位所以推荐相似的B岗位这对提升推荐可信度很重要。ItemCF的核心是计算岗位两两之间的相似度常用余弦相似度。岗位i和岗位j的相似度公式是sim(i, j) sum(u_i * u_j) / (sqrt(sum(u_i^2)) * sqrt(sum(u_j^2)))其中u_i表示所有用户对岗位i的评分向量。用numpy实现from sklearn.metrics.pairwise import cosine_similarity def compute_item_similarity(interaction_matrix): # interaction_matrix: user x item item_matrix interaction_matrix.T similarity cosine_similarity(item_matrix) np.fill_diagonal(similarity, 0) return similaritysklearn的cosine_similarity对稀疏矩阵有优化直接传scipy.sparse矩阵进去也能跑。岗位数量在5000左右时这个计算在普通笔记本上也就几秒钟完全可以定时离线重算。有了相似度矩阵后推荐分数的计算方式是对于用户u交互过的每个岗位i找到与i最相似的K个岗位累加相似度和用户对i的评分权重def recommend_for_user(user_id, user_job_matrix, job_sim_matrix, user_to_idx, job_to_idx, job_ids, top_k20): if user_id not in user_to_idx: return [] u_idx user_to_idx[user_id] interacted np.where(user_job_matrix[u_idx] 0)[0] if len(interacted) 0: return [] scores defaultdict(float) for i_idx in interacted: weight user_job_matrix[u_idx, i_idx] sim_list job_sim_matrix[i_idx] # 取前K个相似岗位 top_sim_indices np.argsort(sim_list)[::-1][:K] for j_idx in top_sim_indices: if j_idx in interacted: continue scores[j_idx] sim_list[j_idx] * weight ranked sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_k] return [(job_ids[j_idx], score) for j_idx, score in ranked]注意这里要跳过用户已经交互过的岗位否则推荐出来的全是用户看过的东西没有意义。3.3 相似度计算的进阶融合文本特征纯行为数据算出来的相似度有一个问题新岗位没有任何用户行为它在矩阵里就是全零向量和任何岗位的相似度都是0。这时需要补充内容特征。我的做法是从岗位描述文本里抽取技能关键词构建一个岗位-技能矩阵然后用Jaccard相似度或者余弦相似度计算岗位的文本相似度。最后把文本相似度和行为相似度做加权融合sim_final alpha * sim_behavior (1 - alpha) * sim_textalpha我设在0.6到0.7之间因为行为相似度在冷启动阶段覆盖面不够时文本相似度能很好地拉一把。文本相似度计算def extract_skills(description): skills set() for kw in SKILL_KEYWORDS: if kw in description: skills.add(kw) return skills def jaccard_similarity(set1, set2): if not set1 or not set2: return 0 inter len(set1 set2) union len(set1 | set2) return inter / union这里的SKILL_KEYWORDS是我预置的技能词表包含Java、Python、Golang、Django、Spring、MySQL、Redis、Kubernetes等上百个常见技术词。这个方法虽然简单但在工程上非常实用不需要训练模型速度快而且效果可解释。4. 冷启动与稀疏矩阵招聘推荐的两个致命问题及处理4.1 冷启动新用户和新岗位都让协同过滤失效协同过滤最怕冷启动。招聘场景里冷启动有两个方向新用户第一次进来没有任何浏览和投递行为协同过滤无法计算他的偏好。我的处理策略是热度召回兜底。把最近7天浏览量最高、投递量最多的热门岗位作为默认推荐列表先给用户看一批平均质量较高的内容。等用户产生了几个浏览行为后再切换到协同过滤推荐。新岗位发布后没有任何用户行为ItemCF无法把它推荐出去。这在招聘场景尤其尴尬——新岗位往往是最需要曝光的。我的处理是给新岗位加一个时间衰减权重在候选集排序时对发布时间7天内的岗位给予额外加分。相当于在推荐列表里留出一定的探索位置。def time_decay_weight(job): days (timezone.now() - job.created_at).days if days 7: return max(0.1, 1 - days * 0.1) return 04.2 稀疏矩阵当交互记录太少的时候怎么办招聘平台的交互密度通常远低于电商平台。一个用户一天可能浏览几十件商品、购买几件但一个月可能才投递十几份简历。矩阵稀疏度经常在99%以上。稀疏矩阵的直接影响是很多岗位的向量是零向量两个岗位之间缺乏共同交互用户余弦相似度算出来是0推荐结果趋同。缓解方法有三种第一种是聚类压缩。先按岗位技能标签做粗粒度聚类把岗位归到后端开发前端开发数据分析产品经理等大类推荐时先命中大类再在大类内部做精细排序。这相当于用一个粗粒度的规则先框定候选集缩小问题规模。第二种是隐式反馈补充。用户搜索过Python 爬虫即使没有点击任何岗位也是很强的信号。把搜索日志、简历关键词、滑动停留时间都转成行为特征加入矩阵能显著提升稠密度。第三种是降维处理。用SVD或NMF把高维稀疏矩阵压缩到低维稠密向量然后用低维向量算相似度。这个方法我试过但需要调参而且解释性变差。在招聘场景里我更喜欢保留原始矩阵加上类目先验这样出问题容易排查。4.3 热门岗位偏差相似度计算中的一个隐藏坑ItemCF有个天然的偏差问题热门岗位和所有岗位的相似度都可能偏高因为用户行为集中在几个大热岗位上。这会导致推荐结果越来越集中长尾岗位完全没有曝光机会反而损害用户体验。我采用的缓解手段是对相似度做归一化也就是把每行相似度除以该行的最大值def normalized_similarity(sim_matrix): row_max sim_matrix.max(axis1, keepdimsTrue) row_max[row_max 0] 1 return sim_matrix / row_max这样处理之后每个岗位的最相似岗位相似度都被归一化为1推荐时不再偏向热门岗位。另一个有效手段是在取TopN时限制同一个公司或同一个岗位类型的推荐数量避免推荐列表全是某一家大厂的内容。5. Django服务化从离线算法到在线推荐接口5.1 Django模型设计行为数据和推荐结果分离算法模型跑出来了还得让用户能看到。Django在这一层扮演的是业务后端和Web框架的角色。数据表设计上我用了这几张核心表class UserBehavior(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, db_indexTrue) job models.ForeignKey(Job, on_deletemodels.CASCADE, db_indexTrue) behavior_type models.CharField(max_length16, choices[ (view, 浏览), (favorite, 收藏), (apply, 投递), (dislike, 不感兴趣) ]) weight models.FloatField(default1.0) created_at models.DateTimeField(auto_now_addTrue) class JobSimilarity(models.Model): job models.ForeignKey(Job, on_deletemodels.CASCADE, related_namesimilar_jobs) similar_job models.ForeignKey(Job, on_deletemodels.CASCADE, related_namesimilar_from) similarity models.FloatField() class Meta: unique_together (job, similar_job) ordering [-similarity]UserBehavior是在线服务写入的表用户每次浏览职位、收藏、投递都实时记录。JobSimilarity是离线计算的产物定时任务把相似度矩阵写进这张表。推荐接口查询时直接根据用户最近的交互行为去JobSimilarity表里查相似岗位再聚合排序。这样的设计把离线计算和在线查询解耦了。JobSimilarity表本质上是预计算的结果缓存查询速度极快不需要在请求到来时临时算一遍相似度。5.2 推荐接口实现Django Rest Framework搭建API我用Django Rest FrameworkDRF写了推荐接口。核心逻辑是接收用户ID回忆协同过滤推荐函数返回岗位列表和推荐理由。class RecommendationView(APIView): def get(self, request): user request.user if not user.is_authenticated: return Response({error: 请先登录}, status401) rec_jobs get_recommendations(user.id, top_k20) serializer JobSerializer(rec_jobs, manyTrue) return Response({code: 0, data: serializer.data})get_recommendations函数做的事情是取用户最近N条行为从JobSimilarity表里查出对应的相似岗位累加打分过滤掉已投递岗位再做时间衰减和公司多样性控制最后返回Top20。性能方面有一个必须做的优化Redis缓存。推荐结果对实时性要求没那么高可以用用户ID做key缓存30分钟。用户新增收藏或投递时再主动删除缓存下次请求时重建。这样能扛住绝大多数并发场景。import redis r redis.Redis(hostlocalhost, port6379, db0) def get_recommendations(user_id, top_k20): cache_key frec:{user_id}:{top_k} cached r.get(cache_key) if cached: return json.loads(cached) # 执行正常推荐流程 rec_jobs do_recommend(user_id, top_k) r.setex(cache_key, 1800, json.dumps(rec_jobs)) return rec_jobs5.3 爬虫与Django如何协作定时更新数据从实际使用看爬虫不用频繁跑每天凌晨执行一次就够。我用Django的custom management command来触发爬虫class Command(BaseCommand): help 抓取每日最新招聘岗位 def handle(self, *args, **options): jobs crawl_job_list() for job in jobs: Job.objects.update_or_create( job_idjob[job_id], defaultsjob, ) # 采集完成后触发相似度重算 recompute_similarity()update_or_create是Django ORM里非常好用的方法天然支持去重job_id存在就更新不存在就创建。这样每天跑一次不会产生重复数据。定时任务我用crontab最简单公司内部的机器也可以用Celery的beat调度。整体链路是定时器触发爬虫任务 - 数据清洗入库 - 重算岗位相似度矩阵 - 更新JobSimilarity表 - 推荐接口自动生效。6. 效果评估与下一步优化思路6.1 离线评测用精确率和召回率衡量推荐效果推荐系统上线前至少要做一个离线评测不然你不知道算法的真实水平。我的做法是把用户行为数据按时间切分前70%做训练集后30%做测试集。模型用训练集计算相似度然后在测试集上检验对每个用户取TA在测试集里真正互动的岗位看推荐列表的TopN中有多少命中。精确率PrecisionN和召回率RecallN是最基础的两个指标Precision10 推荐列表前10中命中用户实际交互的个数 / 10 Recall10 推荐列表前10中命中用户实际交互的个数 / 用户实际交互总数我记录的实测数据数据集来自约1万条真实用户交互岗位数3000指标纯热度推荐ItemCFItemCF 文本融合Precision106.2%11.8%14.5%Recall105.1%9.6%12.3%从结果可以看到加入文本特征后两个指标都有明显提升。原因很好理解行为矩阵太稀疏纯协同过滤能算出来的相似岗位有限文本特征相当于一个平滑器把新岗位和冷门岗位也拉进了候选集。6.2 推荐理由让用户知道为什么看到这个岗位这个优化点是我在实际反馈里发现的用户对推荐结果最大的疑问是为什么推荐这个给我。我于是给推荐接口增加了reason字段返回推荐理由。比如因为你看过/收藏了【Python后端开发工程师】推荐相似岗位因为该岗位与你的技能标签【Django、MySQL、Redis】匹配度较高因为该岗位在近期热招中与你浏览的【后端开发】方向一致实现上就是记录推荐路径的来源岗位在返回时带上来源岗位的标题。这个改动没有增加计算量但对用户体验的提升非常明显。6.3 后续还可以做什么目前这套系统的核心链路已经能跑通但如果要继续迭代我会优先做三件事第一用户画像与内容召回的结合。当用户行为数据积累到一定程度可以训练一个简单的用户兴趣分类模型比如用TF-IDF把用户浏览过的岗位描述聚成兴趣向量在协同过滤的基础上做Rerank进一步提升精度。第二引入更丰富的负反馈。现在系统对用户不感兴趣的记录很少如果能在前端增加不感兴趣按钮并把这个负信号也纳入算法推荐的准确性会有较大提升。第三从离线到实时的链路优化。现在的推荐结果是离线预计算定时刷新适合中小规模系统。如果并发上去了可以引入流式计算用户行为实时写入消息队列计算引擎即时更新推荐结果。这一块的技术栈迁移成本较高需要根据实际业务规模来判断。写这套系统的过程中我最大的体会有两点。第一推荐系统不是算法越复杂越好而是要看数据基础。在行为数据稀疏的时候把内容特征融合进来比盲目上深度学习模型有效得多。第二工程细节决定体验。去重、限速、缓存、容错这些事看起来琐碎但任何一个出问题整个系统都会卡壳。你现在看这个项目可能觉得爬虫、Django、协同过滤都是老技术没什么惊艳的但把这三者串起来解决一个真实问题里面需要想的坑远比想象中多。希望这篇复盘能帮你少踩一些我踩过的坑。本文还有配套的精品资源点击获取