资讯详情 基于Python的职位推荐系统实战:从简历解析到召回排序
📅 2026/10/11 18:32:42
简介这份资源面向具备一定Python基础、希望深入理解推荐系统落地流程的开发者与在校学生围绕职位推荐场景提供了一套可运行的完整项目源码。压缩包共79个文件约942KB其中47个py文件承载核心算法与业务逻辑涵盖协同过滤、冷启动处理、数据过滤与评估等模块17个png与5个html用于结果可视化展示4个csv保存推荐输出数据另有4个md说明文档、1个docx流程说明和1个ini配置文件便于快速理解项目结构与运行方式。资源按多个子目录组织包含itemCF_IUF、userCF_IIF等不同推荐策略的实现并配有评估脚本与图表输出可帮助读者对比算法效果、掌握从数据读取到推荐生成再到结果展示的完整链路。目前已有1130人学习下载适合作为课程设计、毕业设计或推荐算法入门实践的参考素材。1. 基于 Python 的职位推荐系统从简历堆里捞出那个对的人招聘后台里躺着 8000 份简历HR 一天能翻 200 份就算勤快剩下 7800 份里有没有合适的人大概率有但没人翻得到。这就是职位推荐系统要解决的事把「人找岗位」和「岗位找人」从体力活变成一次向量匹配。基于 Python 做这套东西核心链路其实就四步——简历解析、岗位建模、召回排序、结果评估。它适合两类人一类是中小团队里被要求「给招聘系统加个推荐」的后端或数据工程师另一类是拿它当毕业设计或练手项目、想真正跑通一条推荐链路的学生。这篇文章不讲空泛的算法史只讲怎么用 Python 把这条链路搭起来、参数怎么调、哪里会翻车。2. 简历与岗位怎么变成可计算的向量文本清洗到特征工程推荐系统的第一道坎不是模型是数据。简历是 PDF、Word、甚至图片岗位描述是 HR 随手写的三段话两者格式完全不统一。如果这一步做不干净后面模型再花哨也是垃圾进垃圾出。这一章把「非结构化文本 → 结构化特征 → 数值向量」这条链路拆开讲。2.1 简历解析从 PDF 到结构化字段的清洗流程常见做法是用pdfplumber抽文本再用正则和关键词词典切出姓名、学历、技能、工作年限这几个关键字段。不要指望一个正则搞定所有简历实际项目里都是「通用规则 技能词典 人工兜底」三层。import pdfplumber import re # 技能词典实际项目里应该从配置文件或数据库加载这里做示例 SKILL_DICT [python, java, sql, redis, docker, k8s, spring, vue] def parse_resume(pdf_path): text with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: # extract_text 对双栏简历会串行必要时加 layoutTrue text page.extract_text() or # 学历匹配常见写法顺序从高到低避免本科在读被误判为本科 edu 未知 for level in [博士, 硕士, 本科, 大专]: if level in text: edu level break # 工作年限匹配X年经验工作X年两种写法 years 0 m re.search(r(\d)\s*年(?:工作)?经验, text) if m: years int(m.group(1)) # 技能命中统一转小写再匹配避免大小写漏召回 lower text.lower() skills [s for s in SKILL_DICT if s in lower] return {education: edu, years: years, skills: skills, raw: text}这段代码的逻辑是先抽全文再逐字段用规则提取。extract_text()对排版规整的单栏 PDF 够用遇到双栏或表格简历会串行这时要加layoutTrue参数但代价是速度下降明显。学历匹配必须从高到低遍历否则「硕士」里含「士」不影响但「本科在读」会先命中「本科」——如果你的业务需要区分在读和已毕业得单独加一条规则。技能匹配统一转小写是关键简历里「Python」「PYTHON」「python」混着写是常态不统一大小写召回率会掉一截。提示解析结果一定要落库并保留raw原文。规则改版后可以离线重跑不用重新上传简历。2.2 岗位侧建模JD 结构化与技能权重岗位描述比简历更随意同一家公司不同 HR 写出来的 JD 差异极大。我的做法是把 JD 拆成「硬性要求」和「加分项」两段硬性要求里的技能权重高加分项权重低。具体实现是给每个技能配一个权重命中硬性要求段得 1.0命中加分项段得 0.5。def parse_jd(jd_text): # 按常见分隔词切段实际项目里应该用更鲁棒的分段逻辑 hard_part, plus_part jd_text, for sep in [加分项, 优先考虑, 有以下经验者优先]: if sep in jd_text: hard_part, plus_part jd_text.split(sep, 1) break lower_hard hard_part.lower() lower_plus plus_part.lower() skill_weight {} for s in SKILL_DICT: if s in lower_hard: skill_weight[s] 1.0 elif s in lower_plus: skill_weight[s] 0.5 return {skill_weight: skill_weight, raw: jd_text}切段逻辑用split(sep, 1)只切一次避免 JD 里出现多个「优先」导致段落被切碎。权重 1.0 和 0.5 是经验值如果你的岗位对加分项也很看重可以调到 0.7。这个权重会直接影响后面的匹配分数调参时要和业务方对齐——技术觉得 0.5 合理业务可能觉得「会就是会不该打折」。2.3 特征向量化TF-IDF 与技能 one-hot 的取舍文本向量化有两条路TF-IDF 和词嵌入。TF-IDF 快、可解释、不需要预训练模型适合简历量在十万级以内的场景词嵌入比如用sentence-transformers语义召回更好但推理慢、要 GPU、调参空间大。我一般先用 TF-IDF 跑通基线效果不够再上嵌入。from sklearn.feature_extraction.text import TfidfVectorizer import numpy as np # 技能 one-hot把技能词典转成固定维度向量 def skill_onehot(skills): vec np.zeros(len(SKILL_DICT)) for s in skills: if s in SKILL_DICT: vec[SKILL_DICT.index(s)] 1.0 return vec # TF-IDF对原始文本做向量化max_features 控制维度 tfidf TfidfVectorizer(max_features5000, ngram_range(1, 2), min_df2) # 假设 corpus 是所有简历和 JD 的 raw 文本列表 # tfidf_matrix tfidf.fit_transform(corpus)max_features5000是维度和效果的平衡点太小丢信息太大稀疏且慢。ngram_range(1, 2)让「机器学习」和「机器 学习」都能被捕捉但维度会涨配合min_df2过滤只出现一次的词。技能 one-hot 和 TF-IDF 拼接时要注意归一化否则 one-hot 的 0/1 会被 TF-IDF 的小数淹没常见做法是给 one-hot 乘一个权重系数再拼接。3. 召回与排序用 Python 把匹配分数算出来特征有了接下来是算分。推荐系统标准架构是「召回 → 排序」两级召回从全量岗位里快速筛出几百个候选排序再精细打分。小规模场景可以合并成一步但思路要清楚否则数据量一涨就崩。3.1 基于余弦相似度的召回实现余弦相似度是最直接的召回方式把简历向量和所有岗位向量做点积取 Top-N。用sklearn的cosine_similarity一行搞定但要注意矩阵规模和内存。from sklearn.metrics.pairwise import cosine_similarity def recall_topn(resume_vec, jd_matrix, topn50): # resume_vec: (1, dim) 的稀疏或稠密向量 # jd_matrix: (n_jd, dim) 的矩阵 scores cosine_similarity(resume_vec, jd_matrix)[0] # argsort 默认升序取负号后取前 topn top_idx np.argsort(-scores)[:topn] return [(int(i), float(scores[i])) for i in top_idx]cosine_similarity返回的是 (1, n_jd) 的矩阵取[0]拿到一维数组。argsort(-scores)是取 Top-N 的惯用写法比sorted快。如果岗位数超过十万这个全量计算会吃内存要改用NearestNeighbors或 faiss 做近似最近邻。topn 设 50 是经验值太小可能漏掉合适岗位太大排序阶段压力大。3.2 排序阶段加权分数与业务规则融合召回只用了向量相似度排序阶段要把技能权重、工作年限、学历这些硬条件加进去。我的做法是「向量分 × 0.6 技能匹配分 × 0.3 年限匹配分 × 0.1」权重按业务调。def rank_score(resume, jd, vec_score): # 技能匹配简历技能与 JD 技能权重的交集和 skill_score 0.0 for s, w in jd[skill_weight].items(): if s in resume[skills]: skill_score w # 归一化到 0-1避免技能多的 JD 天然占优 max_possible sum(jd[skill_weight].values()) or 1.0 skill_score skill_score / max_possible # 年限匹配简历年限 JD 要求得满分差一年扣 0.2 year_score 1.0 if resume[years] jd.get(require_years, 0) else max(0, 1 - 0.2 * (jd[require_years] - resume[years])) return 0.6 * vec_score 0.3 * skill_score 0.1 * year_score技能分归一化这步容易被忽略。如果不除以max_possible一个列了 10 个技能的 JD 会比只列 3 个的 JD 拿到更高分但这不是因为候选人更匹配而是 JD 写得多。年限分的扣减系数 0.2 是经验值差 5 年就归零实际业务里可能更宽松。这三个权重没有标准答案要拿真实数据做 A/B 测试。3.3 冷启动新用户和新岗位没有行为数据怎么办推荐系统最怕冷启动。新用户没投过简历新岗位没人投过协同过滤直接失效。我的处理是冷启动阶段纯靠内容特征就是前面讲的技能和文本向量等积累了行为数据再引入协同信号。具体做法是给每个用户维护一个「行为权重」投递过的岗位技能加权到用户画像里。def update_user_profile(user_profile, applied_jd): # 用户投递过的岗位其技能以 0.3 的权重累加到用户画像 for s, w in applied_jd[skill_weight].items(): user_profile[skill_weight][s] user_profile[skill_weight].get(s, 0) 0.3 * w return user_profile0.3 这个衰减系数控制行为数据的影响速度太大则几次投递就带偏画像太小则冷启动期过长。实际项目里这个系数要配合投递量动态调整投递少时权重低投递多时逐步提高。4. 避坑与排查职位推荐系统上线后最容易翻车的 5 个点这一章是我踩过的坑按「现象 → 原因 → 解决」写。每一条都真实发生过不是理论推演。4.1 推荐结果全是同一类岗位现象用户反馈推荐列表里 80% 是同一个方向的岗位比如全是后端前端和算法岗一个没有。原因TF-IDF 向量被高频技能词主导比如「python」在大量 JD 里出现权重被 IDF 压低后反而是「redis」这种低频词主导了相似度导致所有含 redis 的岗位被推到一起。解决对技能 one-hot 部分做 L2 归一化并在拼接时给技能向量更高权重我一般给 0.7TF-IDF 给 0.3。另外在召回后加一层多样性打散同一公司或同一岗位类别最多取 3 个。4.2 简历解析把「本科在读」识别成「本科」现象实习生岗位推荐里混进了大量在校生但岗位要求是已毕业。原因学历匹配从高到低遍历时「本科在读」先命中了「本科」没有区分在读状态。解决在学历字段后加一个is_graduated布尔字段用「在读」「预计毕业」「应届」等关键词判断。匹配时先看is_graduated再看学历等级。4.3 相似度分数普遍偏高区分度差现象所有岗位的余弦相似度都在 0.8 以上排序几乎没区分度。原因TF-IDF 向量维度高且稀疏余弦相似度对稀疏向量天然偏高尤其是短文本。解决改用 BM25 或对 TF-IDF 做 sublinear_tf 处理或者直接上句嵌入模型。另一个办法是在排序阶段引入更多硬条件让分数分布拉开。4.4 新岗位上线后长时间不被推荐现象HR 新发的岗位一周内几乎没出现在任何推荐列表里。原因召回阶段用的是全量岗位矩阵新岗位没有历史行为数据向量也没被更新到矩阵里。解决岗位发布时同步更新向量矩阵并给新岗位一个「曝光加权」在前 3 天把召回分数乘 1.2保证有机会被看到。这个加权要设过期时间否则老岗位永远压新岗位。4.5 推荐接口响应超过 2 秒现象用户点开推荐页要等 2 秒以上体验差。原因每次请求都实时计算全量余弦相似度岗位数上万时矩阵运算耗时。解决把召回结果做缓存用户画像不变时直接读缓存岗位矩阵用 faiss 建索引做近似最近邻把 O(n) 降到 O(log n)。缓存失效策略用「用户行为更新时失效」不要用固定 TTL。5. 把推荐效果量化离线评估与在线验证的具体做法推荐系统最怕「感觉还行」。没有量化指标调参就是玄学。这一章讲怎么用 Python 把效果算出来以及上线后怎么验证。5.1 离线评估PrecisionK 和 RecallK 的计算离线评估需要一份标注数据对每个用户人工标出哪些岗位是真正相关的。实际项目里这份数据很难拿退而求其次用「用户实际投递的岗位」作为正样本。def precision_recall_at_k(recommended, relevant, k): # recommended: 推荐列表的岗位 id 列表按分数降序 # relevant: 真实相关的岗位 id 集合 topk recommended[:k] hit len(set(topk) set(relevant)) precision hit / k recall hit / len(relevant) if relevant else 0 return precision, recall # 示例推荐了 10 个用户实际投了 3 个其中 2 个在推荐列表里 p, r precision_recall_at_k([1,2,3,4,5,6,7,8,9,10], {2, 5, 99}, 10) # p 0.2, r 0.667k一般取 5、10、20 三档看趋势。PrecisionK 高说明推荐准RecallK 高说明覆盖全。两者往往此消彼长要按业务目标取舍——招聘场景更看重 Precision因为 HR 没耐心翻长列表。5.2 在线验证A/B 测试的分流与指标离线指标好不代表线上好。上线前一定要做 A/B 测试把用户随机分成对照组旧逻辑和实验组新逻辑看点击率、投递转化率、HR 反馈率。指标含义观察周期推荐点击率推荐列表被点击的比例3 天投递转化率点击后实际投递的比例7 天HR 反馈率HR 对推荐候选人的回复比例14 天推荐多样性推荐列表中不同岗位类别的占比3 天分流用用户 ID 哈希取模保证同一用户始终在同一组。观察周期要覆盖一个完整的招聘周期否则数据有偏。我一般先跑 7 天看点击和投递HR 反馈率要等 14 天才有统计意义。5.3 一个容易被忽略的验证技巧反事实抽查A/B 测试看的是整体指标但有时候指标涨了体验却变差了。我的习惯是每周随机抽 20 个用户人工看他们的推荐列表问三个问题推的岗位是不是这个方向有没有明显不相关的有没有重复这个动作花不了多少时间但能抓到指标看不出的问题比如推荐结果全是同一家公司、或者把已关闭的岗位推出来了。这套系统我前后调了三个月最大的教训是别一上来就上深度学习。TF-IDF 加规则能解决 80% 的问题剩下 20% 再考虑嵌入模型。先把数据清洗和评估链路搭稳模型换起来才没有后顾之忧。希望帮到你。本文还有配套的精品资源点击获取