
简介一份基于Python与Django框架、面向招聘信息管理的协同过滤推荐系统毕业论文适合计算机相关专业学生、毕业设计开发者及对推荐系统感兴趣的技术人员参考。文档完整呈现了从绪论、相关技术介绍到需求分析、系统设计、实现与测试的论文框架并围绕个人中心、用户管理、招聘信息管理、留言板管理、系统管理等核心模块展开。系统采用Python作为后端语言基于Django框架构建Web应用使用MySQL存储业务数据并采用B/S架构实现浏览器端访问。论文详细阐述了协同过滤算法的原理及其在招聘信息推荐中的落地步骤包括用户行为数据收集、相似度计算、推荐结果生成等环节同时包含数据库表设计、系统流程图、界面设计说明和测试用例能够帮助读者完整理解项目设计思路与开发流程。资源为1个docx文档压缩包大小约3.83MB当前已有331人学习下载适合作为毕业设计选题参考或系统开发前的技术预研。1. 招聘信息推荐系统的建模起点是行为数据不是算法“招聘信息推荐”与“协同过滤”组合起来看着顺理成章可系统真正落地时最先暴露的问题多半不是模型不够新而是数据层没法支撑计算。同一个职位用户搜索后停留三秒和直接点击“投递”用户意图强度完全不同把这两种行为都记为一次点击最后看到的推荐列表势必滑向热门职位。围绕这个标题可以做的事情是清晰的用 Django 把招聘数据、行为数据组织成协同过滤需要的打分矩阵然后选择合适的相似度算法产出推荐结果最后用日志和离线指标验证推荐是否真的起效。这篇内容适合正在写招聘推荐相关论文、或打算在 Django 项目里加推荐模块的开发者。2. 搭建 Django 数据层行为评分矩阵的采集与存储推荐系统的数据层和普通业务系统有一个明显区别它要保留“事实行为”而不只是保留最终结果。Django 的 MTV 模式里Model 是数据访问的唯一入口所以不管算法层怎么设计最早要做的都是把实体规划清楚。常见做法是分别用python manage.py startapp jobs和python manage.py startapp behaviors建业务 App避免把行为表堆在默认的models.py里。下面这个Job和Behavior模型覆盖了招聘推荐系统的最小数据集。# behaviors/models.py from django.conf import settings from django.db import models class Job(models.Model): title models.CharField(max_length255) company models.CharField(max_length128) salary_min models.IntegerField(default0) salary_max models.IntegerField(default0) city models.CharField(max_length32, db_indexTrue) tags models.ManyToManyField(JobTag, related_namejobs) pub_date models.DateTimeField(db_indexTrue) source_url models.URLField(uniqueTrue) class JobTag(models.Model): name models.CharField(max_length64, uniqueTrue) class Behavior(models.Model): BEHAVIOR_TYPE ( (view, 浏览), (collect, 收藏), (apply, 投递), ) user models.ForeignKey( settings.AUTH_USER_MODEL, on_deletemodels.CASCADE ) job models.ForeignKey(Job, on_deletemodels.CASCADE) behavior models.CharField(max_length10, choicesBEHAVIOR_TYPE) created_at models.DateTimeField(auto_now_addTrue, db_indexTrue)choices把行为枚举固定在三种类型上防止脏值进入评分矩阵db_indexTrue让按用户或按时间过滤行为时走索引source_url的uniqueTrue从数据库层防止重复入库。这里最容易被忽略的一点是评分不落表。在表里设计一个专门的评分字段看似直观但评分应该作为可调整的策略去计算而不是存成一个静态数字。2.1 把 Django ORM 行为记录转换为评分矩阵行为记录转换为评分矩阵换个评分规则时只要改一个字典就够了。常见做法是把投递当作强信号、收藏作为中等信号、浏览作为弱信号权重分别设为 5、3、1。# recommend/services.py from collections import defaultdict BEHAVIOR_WEIGHT {view: 1, collect: 3, apply: 5} def build_rating_matrix(): matrix defaultdict(dict) rows ( Behavior.objects .values(user_id, job_id, behavior) .iterator(chunk_size5000) ) for row in rows: user_id row[user_id] job_id row[job_id] weight BEHAVIOR_WEIGHT[row[behavior]] matrix[user_id][job_id] matrix[user_id].get(job_id, 0) weight return {u: dict(items) for u, items in matrix.items()}这段代码用values()只取需要的字段避免 ORM 把整行实体对象加载进内存iterator(chunk_size5000)在行为表超过几十万行时能明显降低内存峰值。累加逻辑让同一用户对同一职位的多次行为可以叠加例如先浏览再投递该职位的评分就是 156。把评分规则与矩阵构建分开后续做 A/B 测试时只需替换BEHAVIOR_WEIGHT。2.2 招聘数据的爬虫采集与重复处理招聘数据的来源一般有两类一类是运营人工录入另一类是用爬虫增量抓取招聘站点。增量抓取最怕重复入库Django 的update_or_create正好处理这个场景。# jobs/managers.py import requests from time import sleep def fetch_jobs_into_db(source_url, parse_func): resp requests.get(source_url, headers{User-Agent: Mozilla/5.0}, timeout10) resp.raise_for_status() # 非 200 状态直接抛异常便于告警定位 for item in parse_func(resp.text): job, created Job.objects.update_or_create( source_urlitem[source_url], defaults{ title: item[title], company: item[company], city: item[city], salary_min: int(item[salary_min]), salary_max: int(item[salary_max]), }, ) if created: names [t.strip() for t in item[tags].split(,) if t.strip()] job.tags.set([JobTag.objects.get_or_create(namen)[0] for n in names]) sleep(1) # 限流避免对目标站点造成压力update_or_create的第一个参数是匹配条件defaults里的字段只在行不存在时触发写入已存在时只会更新。这样重复抓取不会产生重复职位但职位被改过的薪资、城市也会同步更新。sleep(1)限流既是抓取礼仪也避免瞬时请求过多导致目标站反爬策略升级。后续接入更严谨的反爬时可以把这段换成scrapy但入库收敛逻辑不变。招聘信息的文本字段在进入模型前还要做一次清洗。salary_min接到“15-25K”这类字符串时需要先拆成两个数值再入库city建议统一成“北京”“上海”这种规范名。清洗规则单独写一个模块不要让抽取逻辑和入库逻辑混在一起否则换数据源时整段爬虫代码都要重写。3. 协同过滤的落地实现从相似度计算到推荐列表生成协同过滤是一个很大的家族标题里没有限定用哪个变体但招聘推荐场景下选错变体比调错参数更致命。以下是两种主要变体的对比对比角度UserCFItemCF计算对象用户和用户之间的相似度职位与职位之间的相似度招聘场景稳定性用户行为稀疏很难找到可靠相似用户职位共现链相对稳定冷门职位也能兜底可解释性“和你相似的人也在投”“看过这个职位的人也在看”更新成本用户行为变化频繁矩阵需要频繁刷新职位相似度可以按小时离线重算招聘网站用户数量通常远大于职位数量一个新注册用户往往只有一两次浏览行为UserCF 算出“他像谁”要靠运气。一般建议两个都实现然后用一个配置项切换。下面给出两种最简洁且可以直接跑通的 Python 写法。3.1 用 Python 实现 UserCF余弦相似度加评分加权from collections import defaultdict from math import sqrt def user_cf_recommend(rating_matrix, target_user, top_n10, min_common5): target_items rating_matrix.get(target_user, {}) if not target_items: return [] norm { user: sqrt(sum(score * score for score in items.values())) for user, items in rating_matrix.items() } cand defaultdict(float) for other_user, items in rating_matrix.items(): if other_user target_user: continue common set(target_items.keys()) set(items.keys()) if len(common) min_common: continue # 共现太少时相似度噪声大直接丢弃 dot sum(target_items[job] * items[job] for job in common) if dot 0: continue sim dot / (norm[target_user] * norm[other_user]) for job_id, score in items.items(): if job_id in target_items: continue cand[job_id] sim * score return sorted(cand.items(), keylambda x: -x[1])[:top_n]min_common5是参数敏感度较高的地方这个值太小时两个用户只看过同一个热门职位就会被认为高度相似太大时冷门用户直接被排除。按经验行为量级在几千到几万的站点5 到 10 是合理区间。norm预先算好每个用户的向量长度避免内层循环重复计算。候选中最终得分是相似度乘以对方评分目的是让相似用户的强反馈比弱反馈获得更高权重。3.2 ItemCF 实现职位相似度与加权求和ItemCF 的核心不是遍历用户而是借助“共同被浏览过的职位对”算出职位之间的相似度然后按目标用户的评分加权。from collections import defaultdict def build_job_similarity(rating_matrix, top_k20): # 把用户-职位矩阵转置为职位-用户矩阵 job_users defaultdict(set) for user_id, items in rating_matrix.items(): for job_id in items: job_users[job_id].add(user_id) sim defaultdict(dict) jobs list(job_users.keys()) for i, job_a in enumerate(jobs): for job_b in jobs[i 1:]: users_a job_users[job_a] inter len(users_a job_users[job_b]) if inter 2: continue score inter / (len(users_a) * len(job_users[job_b])) ** 0.5 if score 0.05: # 相似度阈值过滤低频共现 sim[job_a][job_b] score sim[job_b][job_a] score return {job: sorted(items.items(), keylambda x: -x[1])[:top_k] for job, items in sim.items()} def item_cf_recommend(rating_matrix, job_sim, target_user, top_n10): target_items rating_matrix.get(target_user, {}) cand defaultdict(float) for job_id, history_score in target_items.items(): for related_job, sim_score in job_sim.get(job_id, []): if related_job in target_items: continue cand[related_job] sim_score * history_score return sorted(cand.items(), keylambda x: -x[1])[:top_n]相似度公式取的是余弦相似度的等价形式分母用两个职位的被行为用户数乘积开方等价于把每个职位当作一个向量。inter 2和score 0.05是两个不同的动作前者是共现绝对次数下限后者是相似度下限。在招聘数据里大部分行为集中在头部热门职位长尾分布下共现次数低但相似度极高的情况非常常见两个阈值配合使用比只调一个稳定得多。build_job_similarity放到离线任务里跑结果存进数据库或 Redisitem_cf_recommend只做查表和加权线上响应不会超过几百毫秒。职位数在几千级别的项目中这种写法足够跑过几十万条行为记录。3.3 两种算法在 Django 项目里的接入方式离线算相似度在线出结果是当前 Django 推荐模块最常见的分布方式。用 Celery 定时任务每小时重建一次job_sim存到缓存视图层从缓存读取相似度表再调用item_cf_recommend。完全实时的矩阵构建会让每一次请求都触发全表扫描Django 的开发服务器下尤其明显论文截图或答辩演示时表现会很差。4. 围绕 Django 的推荐业务闭环从请求到结果输出数据层和算法层就位后剩下的是把推荐结果从“计算出的 job_id 列表”变成“页面上的职位卡片”。这一步对应的正是 MTV 模式里 View 和 Template 的职责。4.1 在 Django 视图中调用推荐服务并渲染结果# recommend/views.py from django.shortcuts import render from django.core.cache import cache from jobs.models import Job, Behavior def recommend_view(request): user request.user if not user.is_authenticated: jobs Job.objects.order_by(-pub_date)[:10] return render(request, recommend.html, {jobs: jobs}) cache_key frec:{user.id}:v1 rec cache.get(cache_key) if rec is None: matrix load_rating_matrix_from_cache() job_sim cache.get(job_sim:v1) rec item_cf_recommend(matrix, job_sim, user.id, top_n20) cache.set(cache_key, rec, 60 * 30) applied_ids set( Behavior.objects.filter(useruser, behaviorapply) .values_list(job_id, flatTrue) ) job_ids [job_id for job_id, _ in rec if job_id not in applied_ids][:10] jobs list( Job.objects.filter(id__injob_ids).prefetch_related(tags) ) jobs.sort(keylambda j: job_ids.index(j.id)) # 恢复算法排序 return render(request, recommend.html, {jobs: jobs})这段代码集中体现了推荐功能和普通 CRUD 的差异。首先未登录用户直接走时间序兜底登录用户优先从缓存取 30 分钟内的推荐结果。取得 job_ids 后filter(id__injob_ids)拿回来的是无序 QuerySet必须用sort按原列表顺序恢复否则前端看起来排序是乱的。prefetch_related(tags)避免渲染职位标签时逐条查询数据库这是 Django 推荐页最常用的查询优化手段之一。4.2 用 Django ORM 的过滤与删除操作清洗推荐结果用户投递过的职位绝不能反复出现在推荐列表里。上面的applied_ids过滤是一种方式更彻底的做法是直接用 ORM 的exclude结合子查询Behavior.objects.filter(behaviorapply).values_list(job_id, flatTrue)生成一个子查询视图层再写Job.objects.exclude(id__inapplied_subquery)。两种写法的区别在于内存占用数据量大时优先用带子查询的版本。行为日志的清理在 Django 中也很顺手通常保留最近 90 天from datetime import timedelta from django.utils import timezone deleted, detail ( Behavior.objects .filter(created_at__lttimezone.now() - timedelta(days90)) .delete() )delete()返回的是(总数, 明细字典)从detail里能看到各类模型删除了多少行方便确认级联删除是否误伤了其他表。这里要注意立刻删掉 90 天前的行为相当于直接放弃长周期用户的学习信号正确做法是先确认推荐候选中还包含这些职位再执行删除避免出现“删完才知道需要它”的尴尬。提示清理到期行为日志前先把推荐候选写入独立表格或缓存日志删除后还能通过候选表回溯排查问题。4.3 相似度矩阵与推荐结果的缓存分层Redis 在整个推荐链路里承担两层缓存职责。第一层是“物料”也就是job_sim6 小时刷新一次第二层是“结果”也就是每个用户的rec:{uid}:v130 分钟过期。物料层失效后所有用户首次请求都会触发全量计算形成缓存雪崩因此离线任务更新完job_sim时要主动生成一份新版本 key旧 key 自然过期后流量自动切到新版本。缓存 key内容过期时间更新方式job_sim:v2职位两两相似度6 小时Celery 定时重建rec:{user_id}:v1用户推荐列表30 分钟首次请求生成hot_jobs:v1热门职位兜底10 分钟手动或任务刷新缓存键里带版本号是很实用的小习惯。改权重、改阈值之后只要把 key 从v1升到v2就能在不清空 Redis 的情况下让新逻辑立即生效想对比新旧两版结果时还能在日志里按版本号拆分数据。5. 招聘信息推荐里冷启动与数据稀疏的常见优化路径招聘信息推荐系统最容易挨骂的地方不是算法精度而是用户没行为。新用户注册进来推荐页直接空白论文演示时这几乎等于判负。这也说明冷启动需要当成一个独立模块来设计而不是在算法函数里打个补丁。5.1 冷启动用户与冷启动作业的兜底策略冷启动通常分两种情况新用户没有行为老用户行为太少。前者适合直接给“最新职位 热门职位”的混合列表后者可以在算法结果太少时追加流行度补全。def recommend_with_fallback(user_id, rating_matrix, job_sim, top_n10): rec item_cf_recommend(rating_matrix, job_sim, user_id, top_ntop_n * 2) if user_id not in rating_matrix or len(rec) top_n: existed {job_id for job_id, _ in rec} hot list( Job.objects.filter(status1) .exclude(id__inexisted) .order_by(-view_count, -pub_date)[: top_n - len(rec)] ) rec [(job.id, 0.0) for job in hot] return rec[:top_n]order_by(-view_count, -pub_date)同时利用热度与时效比单一排序更贴合招聘场景一个职位浏览量再高如果发布已经几个月职位可能早已招满。兜底补全只占总量的两成以下时不会稀释算法命中率超过五成就说明行为矩阵太薄优先任务是扩大采集。新用户场景里还能叠加一层画像匹配把注册表单里的期望城市、期望岗位类型转成标签用标签和职位标签的重合度打分。这不算协同过滤但能解决协同过滤冷启动阶段的空白问题。实现上可以把它合并进rec列表生成逻辑作为相似度分数之外的一个可配置加分项。5.2 稀疏矩阵下的相似度阈值与降维处理招聘行为矩阵天然稀疏。一个用户看过 5 个职位几千个职位里绝大多数没有行为直接用原始矩阵算相似度会得到一堆共现次数为 1 的噪声对。def jaccard_sim(job_a, job_b, rating_matrix): users_a set(rating_matrix[job_a].keys()) users_b set(rating_matrix[job_b].keys()) inter len(users_a users_b) return inter / max(len(users_a | users_b), 1)Jaccard 系数比余弦对稀疏矩阵更稳因为它只看交集大小不放大向量长度差异。实际站点里一般要求共现次数至少 2 到 3 次同时 Jaccard 相似度不低于 0.03两个条件用and连接。这个组合会把大多数只被同一个人点过的职位对过滤掉保留下来的关系才具备足够的泛化能力。再进一步的做法是对行为做“职位主题降维”把职位向量映射到技能标签空间上。招聘信息里通常有 Java 开发、数据分析、产品经理这类标签把职位相似度改为“标签共现 用户共现”的加权和能显著改善冷门职位的推荐质量。这一步不改变 ItemCF 的骨架只是把相似度计算公式换掉因此可以放在算法链路稳定之后再迭代。6. 用日志与离线指标验证招聘推荐效果的三个具体技巧推荐系统不做验证等于在黑暗中调参。论文里至少需要一个可重复的验证手段工程上也需要靠日志确认推荐是否真的被人打开了。6.1 用 StreamingHttpResponse 导出推荐日志每一个推荐请求都应该落一条日志字段包括用户、推荐职位、得分、排序位置以及是否被点击。用 Django 的StreamingHttpResponse可以边查询边写文件几十万行日志也不会撑爆内存。from django.http import StreamingHttpResponse import csv def export_rec_log(request): def generate(): yield [user_id, job_id, score, position, clicked] rows (RecommendationLog.objects .order_by(id) .values_list(user_id, job_id, score, position, clicked)) for row in rows.iterator(chunk_size10000): yield [str(v) for v in row] resp StreamingHttpResponse( generate(), content_typetext/csv, ) resp[Content-Disposition] attachment; filenamerec_log.csv return respcontent_type决定浏览器如何解析响应体Content-Disposition里的attachment加上filename会触发下载而不是直接在页面里打开。iterator(chunk_size10000)是这里的核心ORM 逐块从数据库取数不会一次性把整个表载入内存。6.2 命中率与覆盖率两个离线指标的快速计算两个指标够用命中率衡量推荐列表里有几条被点击覆盖率衡量推荐结果有没有局限于少数职位。def hit_rate(log_rows, top_n10): clicked_in_top sum( row[clicked] and row[position] top_n for row in log_rows ) return clicked_in_top / max(len(log_rows), 1) def coverage(log_rows, total_jobs): returned_jobs {row[job_id] for row in log_rows} return len(returned_jobs) / max(total_jobs, 1)命中率在 5% 到 15% 之间常见不要拿电商的点击率标准来套。覆盖率掉到 5% 以下说明候选几乎全是热门职位冷门职位永远没有曝光机会需要回头检查相似度阈值。6.3 参数调整的优先顺序调整参数时先动最小共现样本数min_common再看相似度阈值最后才动评分权重。min_common直接决定噪声级别它的影响面最大评分权重即使在 1 到 5 之间来回变也只是在加权的分数上平移不会改变候选排序的相对结构。每调完一组参数导出一次日志、算一次命中率和覆盖率两个指标对比后才能确认改动方向。只追求命中率系统会把推荐收敛到热门只追求覆盖率推荐会变成拼凑。把这套日志与验证流程留在 Django 项目里比任何一个算法公式都更能回答“这个推荐系统到底行不行”。本文还有配套的精品资源点击获取