恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于BOSS直聘的数据分析师岗位薪资预测与层级分类实战
首页
资讯中心
/
基于BOSS直聘的数据分析师岗位薪资预测与层级分类实战
基于BOSS直聘的数据分析师岗位薪资预测与层级分类实战
发布时间:2026/10/2 4:39:38
简介这是一套面向计算机与数据科学专业学生的学术实践项目聚焦招聘平台数据分析岗位的信息挖掘与智能研究可用于课程设计、毕业项目筹备与实战技能提升。项目以BOSS直聘数据分析师职位为对象完整覆盖网络爬虫构建、多维统计解析、交互式可视化与机器学习薪资预测全流程并在高校课程考核中获得98分。压缩包共43个文件约1.36MB包含3个py爬虫与处理脚本、3个ipynb分析及建模笔记、26张png可视化结果图、1个csv城市数据以及zbak备份与md说明文档解压后可直接部署运行。技术架构采用分层设计自适应爬虫引擎采集职位信息、薪资范围与能力要求统计分析挖掘学历、经验与薪酬的关联规律动态图表展示地域分布与技能热度集成学习算法构建薪资预测器并量化特征重要性。目前已有54人学习代码遵循模块化规范并配备详细注释与技术文档适合作为数据采集清洗、特征工程、可视化分析与回归预测的完整方法论范本。1. 从BOSS直聘数据分析师岗位看一份招聘数据能挖出多少东西打开BOSS直聘搜“数据分析师”你会看到什么薪资从8K到45K不等要求从“熟练Excel”到“精通Python机器学习”公司从十几个人的创业团队到几万人的大厂。这些信息散落在几千条招聘帖里靠人眼一条条翻翻到第一百条就麻木了。但如果你把这几千条数据抓下来用结构化字段存好再拿机器学习模型跑一遍你就能回答一些很有意思的问题哪些技能组合能把薪资抬上去北京和杭州的数据分析师岗位要求差在哪什么样的公司愿意给应届生开20K这套爬虫系统加机器学习预测模型的方案解决的就是这个问题。它适合有Python基础、想做一个完整数据项目练手的人也适合正在找工作、想用数据反向指导自己技能树的人。整个链路分三段第一段用爬虫把BOSS直聘上数据分析师岗位的招聘信息批量抓下来第二段做清洗和特征工程第三段用回归模型预测薪资、用分类模型判断岗位所属层级。中间会踩不少坑比如反爬策略、字段缺失、薪资区间怎么转成数值这些后面都会细说。2. 爬虫系统怎么搭从请求构造到数据落库2.1 目标字段拆解与请求链路设计先想清楚要抓什么。BOSS直聘的岗位卡片上核心字段包括岗位名称、薪资区间、公司名称、公司规模、融资阶段、行业、城市、区域、经验要求、学历要求、技能标签、岗位描述全文。这些字段分布在列表页和详情页两个层级。列表页能拿到岗位名称、薪资、公司名、城市、经验学历等概要信息详情页才有完整的岗位描述和技能标签。请求链路的设计思路是先请求列表页拿到岗位ID和概要字段再根据岗位ID构造详情页请求补充描述和标签。列表页的URL通常带有城市编码和页码参数比如https://www.zhipin.com/c101010100/?query数据分析师page1其中c101010100是北京的城市编码。翻页时page参数递增但要注意BOSS直聘的列表页有翻页深度限制一般到第10页左右就会触发验证。import requests import time import random from urllib.parse import urlencode CITY_CODES { 北京: 101010100, 上海: 101020100, 广州: 101280100, 深圳: 101280600, 杭州: 101210100, } def build_list_url(city: str, keyword: str, page: int) - str: 构造BOSS直聘列表页URL city_code CITY_CODES.get(city, 101010100) params { query: keyword, page: page, } return fhttps://www.zhipin.com/c{city_code}/?{urlencode(params)} def fetch_page(url: str, headers: dict, retry: int 3) - str: 带重试的页面请求 for i in range(retry): try: resp requests.get(url, headersheaders, timeout10) if resp.status_code 200: return resp.text elif resp.status_code 403: print(f触发反爬第{i1}次重试) time.sleep(random.uniform(5, 10)) except requests.RequestException as e: print(f请求异常: {e}) time.sleep(random.uniform(3, 6)) return 这段代码里CITY_CODES字典把城市名映射到BOSS直聘内部的城市编码这是构造URL的前提。build_list_url负责拼接列表页地址fetch_page做了三件事设置超时、检测403状态码、失败后随机等待重试。参数retry3表示最多重试三次每次等待时间在5到10秒之间随机浮动避免固定间隔被识别为机器人。请求头是另一个关键点。BOSS直聘会检查User-Agent、Referer、Cookie等字段。User-Agent建议用真实浏览器的Referer设为https://www.zhipin.com/Cookie需要从浏览器登录后手动复制。注意Cookie有有效期一般几小时到一天不等过期后需要重新获取。2.2 列表页与详情页的解析逻辑拿到HTML之后解析工作分两步走。列表页用BeautifulSoup或lxml提取岗位卡片每个卡片对应一个li元素里面包含岗位名称、薪资、公司信息等。详情页则需要提取岗位描述全文和技能标签。from bs4 import BeautifulSoup import re def parse_list_page(html: str) - list: 解析列表页返回岗位概要信息列表 soup BeautifulSoup(html, lxml) jobs [] for card in soup.select(.job-card-wrapper): job {} job[title] card.select_one(.job-name).get_text(stripTrue) job[salary] card.select_one(.salary).get_text(stripTrue) job[company] card.select_one(.company-name).get_text(stripTrue) job[city] card.select_one(.job-area).get_text(stripTrue) # 经验和学历要求通常在标签行 tags card.select(.tag-list li) job[experience] tags[0].get_text(stripTrue) if len(tags) 0 else job[education] tags[1].get_text(stripTrue) if len(tags) 1 else # 岗位详情链接 link card.select_one(a.job-card-left) job[detail_url] https://www.zhipin.com link[href] if link else jobs.append(job) return jobs def parse_detail_page(html: str) - dict: 解析详情页提取岗位描述和技能标签 soup BeautifulSoup(html, lxml) detail {} desc_div soup.select_one(.job-sec-text) detail[description] desc_div.get_text(stripTrue) if desc_div else # 技能标签 skill_tags soup.select(.job-keyword-list li) detail[skills] [tag.get_text(stripTrue) for tag in skill_tags] return detailparse_list_page遍历每个岗位卡片提取标题、薪资、公司名、城市、经验学历和详情页链接。parse_detail_page则从详情页中提取岗位描述全文和技能标签列表。这里有个细节BOSS直聘的页面结构会不定期调整class名称可能变化所以解析逻辑需要定期维护。建议把选择器集中写在一个配置字典里方便统一修改。薪资字段的格式是“15-25K·13薪”或“20-35K”这样的字符串后续需要转成数值。经验要求可能是“3-5年”“5-10年”“经验不限”学历要求可能是“本科”“硕士”“大专”。这些都需要在清洗阶段做标准化处理。2.3 反爬应对与数据落库BOSS直聘的反爬机制主要有三层请求频率检测、Cookie有效性校验、行为特征分析。应对策略也分三层控制请求频率、维护Cookie池、模拟真实浏览行为。请求频率方面建议每次请求间隔3到8秒随机每抓取50条休息30秒。Cookie方面可以准备多个账号的Cookie轮换使用但注意不要用同一IP登录多个账号。行为特征方面可以在请求之间随机插入一些对首页或搜索页的访问模拟正常浏览路径。import sqlite3 import json def init_db(db_path: str boss_jobs.db): 初始化SQLite数据库 conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS jobs ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, salary_raw TEXT, salary_min REAL, salary_max REAL, company TEXT, city TEXT, experience TEXT, education TEXT, skills TEXT, description TEXT, crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() return conn def save_jobs(conn, jobs: list): 批量写入岗位数据 cursor conn.cursor() for job in jobs: cursor.execute( INSERT INTO jobs (title, salary_raw, company, city, experience, education, skills, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?) , ( job.get(title, ), job.get(salary, ), job.get(company, ), job.get(city, ), job.get(experience, ), job.get(education, ), json.dumps(job.get(skills, []), ensure_asciiFalse), job.get(description, ), )) conn.commit()数据库用SQLite就够了单机跑几千条数据完全没问题。表结构里salary_raw存原始字符串salary_min和salary_max留空等清洗阶段再填充。skills字段用JSON字符串存储列表读取时用json.loads还原。crawl_time自动记录抓取时间方便后续做增量更新。注意抓取频率不要太高建议单IP每分钟不超过10次请求。如果遇到验证码不要硬闯停下来换Cookie或等一段时间再试。3. 数据清洗与特征工程把招聘帖变成模型能吃的表3.1 薪资字段的解析与标准化薪资字段是回归模型的目标变量必须转成数值。BOSS直聘的薪资格式有几种常见形态“15-25K”“20-35K·13薪”“8-12K”“面议”。前两种需要拆出最小值和最大值第三种只有一个区间第四种直接丢弃。import re def parse_salary(salary_str: str) - tuple: 解析薪资字符串返回(最小值, 最大值)单位K if not salary_str or 面议 in salary_str: return None, None # 匹配 15-25K 或 15-25K·13薪 match re.search(r(\d)-(\d)K, salary_str) if match: return float(match.group(1)), float(match.group(2)) # 匹配 15K 单值情况 match re.search(r(\d)K, salary_str) if match: val float(match.group(1)) return val, val return None, Noneparse_salary先用正则匹配区间格式如果匹配到就返回两个数值如果只匹配到单值就把最小值和最大值设为相同如果都匹配不到返回None, None。后续在DataFrame里过滤掉这些空值行。注意“·13薪”不影响月薪区间但可以在特征工程里加一个“是否13薪”的布尔特征这可能是薪资水平的信号。3.2 经验、学历与技能标签的编码经验要求需要转成有序数值。常见的取值有“经验不限”“1年以内”“1-3年”“3-5年”“5-10年”“10年以上”。可以映射成0到5的整数0表示不限5表示10年以上。学历要求类似“不限”“大专”“本科”“硕士”“博士”映射成0到4。EXPERIENCE_MAP { 经验不限: 0, 1年以内: 1, 1-3年: 2, 3-5年: 3, 5-10年: 4, 10年以上: 5, } EDUCATION_MAP { 不限: 0, 大专: 1, 本科: 2, 硕士: 3, 博士: 4, } def encode_experience(exp_str: str) - int: for key, val in EXPERIENCE_MAP.items(): if key in exp_str: return val return 0 def encode_education(edu_str: str) - int: for key, val in EDUCATION_MAP.items(): if key in edu_str: return val return 0技能标签的处理稍微复杂。原始数据里每个岗位的技能是一个列表比如[Python, SQL, 机器学习, 数据分析]。需要先做去重和归一化比如“Python”和“python”统一成“Python”“机器学习”和“ML”统一成“机器学习”。然后用MultiLabelBinarizer转成0/1矩阵每个技能一列。from sklearn.preprocessing import MultiLabelBinarizer def normalize_skills(skills_list: list) - list: 技能标签归一化 alias_map { python: Python, sql: SQL, excel: Excel, 机器学习: 机器学习, ml: 机器学习, 深度学习: 深度学习, dl: 深度学习, 数据分析: 数据分析, 数据挖掘: 数据挖掘, tableau: Tableau, power bi: Power BI, spss: SPSS, r语言: R, r: R, } normalized set() for skill in skills_list: key skill.strip().lower() if key in alias_map: normalized.add(alias_map[key]) else: normalized.add(skill.strip()) return list(normalized) mlb MultiLabelBinarizer() skills_matrix mlb.fit_transform(df[skills_normalized]) skills_df pd.DataFrame(skills_matrix, columnsmlb.classes_)normalize_skills把常见别名统一MultiLabelBinarizer把技能列表转成二进制矩阵。最终的特征表里每个技能占一列值为0或1。如果技能种类太多可以只保留出现频率最高的前30个其余归入“其他”。3.3 城市与行业的特征处理城市是分类变量但不同城市之间有薪资水平差异可以用目标编码Target Encoding或者直接做One-Hot。如果城市数量不多比如只抓了5个城市One-Hot就够了。如果抓了全国几十个城市建议用目标编码把城市映射成该城市的平均薪资。def target_encode_city(df, city_colcity, target_colsalary_avg, min_samples10): 城市目标编码用城市平均薪资替代城市名 city_stats df.groupby(city_col)[target_col].agg([mean, count]) city_stats city_stats[city_stats[count] min_samples] city_map city_stats[mean].to_dict() global_mean df[target_col].mean() df[city_encoded] df[city_col].map(city_map).fillna(global_mean) return dftarget_encode_city先按城市分组算平均薪资和样本数样本数少于min_samples的城市不单独编码直接用全局平均薪资填充。这样做的好处是避免了高基数分类变量的维度爆炸同时保留了城市间的薪资差异信息。行业字段类似处理。BOSS直聘的行业分类有几十种可以合并成几个大类互联网、金融、教育、医疗、制造业、其他。合并规则可以用关键词匹配比如包含“互联网”“电商”“社交”的归为互联网包含“银行”“保险”“证券”的归为金融。4. 机器学习预测模型回归与分类两条线4.1 薪资回归模型XGBoost与特征重要性薪资预测是一个回归问题目标变量是salary_avg薪资区间中值。特征包括经验编码、学历编码、城市编码、技能标签矩阵、公司规模、融资阶段、是否13薪。模型选XGBoost因为它在表格数据上表现稳定对缺失值和异常值有一定鲁棒性而且能输出特征重要性。import xgboost as xgb from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error, r2_score # 准备特征矩阵 feature_cols [experience_encoded, education_encoded, city_encoded, is_13th_salary] list(mlb.classes_) X df[feature_cols] y df[salary_avg] # 划分训练集和测试集 X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) # 训练XGBoost回归模型 model xgb.XGBRegressor( n_estimators300, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, random_state42, ) model.fit(X_train, y_train) # 预测与评估 y_pred model.predict(X_test) mae mean_absolute_error(y_test, y_pred) r2 r2_score(y_test, y_pred) print(fMAE: {mae:.2f}K, R2: {r2:.3f})参数说明n_estimators300是树的数量太少欠拟合太多容易过拟合max_depth6控制每棵树的深度6层在几千条数据上比较合适learning_rate0.05是学习率配合300棵树收敛比较平稳subsample0.8和colsample_bytree0.8分别对样本和特征做随机采样增加模型泛化能力。评估指标用MAE和R2MAE表示预测薪资平均偏差多少KR2表示模型解释了目标变量多少方差。特征重要性可以直接从模型里拿importance model.feature_importances_ feat_imp pd.Series(importance, indexfeature_cols).sort_values(ascendingFalse) print(feat_imp.head(15))通常经验编码、城市编码、Python和机器学习技能会排在前列。如果发现某个技能的重要性异常高可以单独看一下这个技能对应的薪资分布验证是否符合直觉。4.2 岗位层级分类用逻辑回归做可解释性判断除了预测薪资还可以做一个分类任务判断岗位是“初级”“中级”还是“高级”。层级标签可以从薪资区间和岗位描述里推断比如薪资中值低于15K且经验要求3年以下的标为初级15K到30K且经验3-5年的标为中级30K以上或经验5年以上的标为高级。from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report def label_level(row): 根据薪资和经验打层级标签 if row[salary_avg] 15 and row[experience_encoded] 2: return 0 # 初级 elif row[salary_avg] 30 and row[experience_encoded] 4: return 1 # 中级 else: return 2 # 高级 df[level] df.apply(label_level, axis1) X_cls df[feature_cols] y_cls df[level] X_train_c, X_test_c, y_train_c, y_test_c train_test_split(X_cls, y_cls, test_size0.2, random_state42) clf LogisticRegression(max_iter1000, multi_classmultinomial) clf.fit(X_train_c, y_train_c) y_pred_c clf.predict(X_test_c) print(classification_report(y_test_c, y_pred_c, target_names[初级, 中级, 高级]))逻辑回归的好处是系数可以直接解释。比如“机器学习”技能的系数为正且绝对值大说明会机器学习的岗位更可能被归为高级。max_iter1000确保优化算法收敛multi_classmultinomial处理多分类。分类报告会输出每个类别的精确率、召回率和F1值如果某个类别F1偏低可能是标签定义有问题需要调整阈值。4.3 模型验证与交叉验证的坑单次train_test_split的结果波动可能很大尤其是数据量只有几千条的时候。更可靠的做法是K折交叉验证。from sklearn.model_selection import cross_val_score cv_scores cross_val_score(model, X, y, cv5, scoringneg_mean_absolute_error) print(fCV MAE: {-cv_scores.mean():.2f}K (/- {cv_scores.std():.2f}))cv5表示5折交叉验证scoringneg_mean_absolute_error返回负MAE取负号后得到正常MAE。标准差反映模型在不同数据子集上的稳定性如果标准差超过2K说明模型对数据划分敏感可能需要增加数据量或简化模型。注意交叉验证时不要对全量数据做目标编码否则会引入数据泄露。正确做法是在每折训练集上计算编码映射再应用到验证集。5. 避坑与排查爬虫和模型里最容易翻车的地方5.1 爬虫被403或验证码拦截现象请求返回403状态码或者页面跳转到验证码页面。原因请求频率过高、Cookie过期、User-Agent被识别。解决降低请求频率到每分钟5次以下更换Cookie检查User-Agent是否包含“python”等关键词。如果验证码出现不要尝试自动识别直接暂停抓取等几小时后再试。5.2 薪资解析出现大量空值现象parse_salary返回None, None的比例超过20%。原因薪资格式比预想的复杂比如“15-25K·14薪”“200-300元/天”“底薪8K提成”。解决先统计所有薪资字符串的格式分布针对每种格式写解析规则。对于“元/天”的格式可以按每月22天折算成月薪。对于“底薪提成”的格式只取底薪部分。5.3 技能标签矩阵过于稀疏现象MultiLabelBinarizer生成的矩阵有几百列大部分列只有个位数样本。原因技能标签没有做归一化同一个技能有多种写法。解决先做词频统计把出现次数少于10次的技能合并成“其他”或者只保留前30个高频技能。归一化时注意大小写和空格比如“Power BI”和“PowerBI”要统一。5.4 模型在测试集上R2为负现象XGBoost回归的R2小于0说明模型预测还不如直接用均值。原因特征里混入了噪声或者目标变量分布极端。解决检查特征里是否有ID类字段被误加入检查薪资是否有异常值比如误解析出1000K。对薪资做截断把超过100K的按100K处理。另外如果数据量少于500条XGBoost容易过拟合可以改用线性回归或决策树。5.5 交叉验证结果波动大现象5折交叉验证的MAE标准差超过3K。原因数据里某些类别的样本太少折与折之间分布差异大。解决改用分层交叉验证按岗位层级或城市分层确保每折的类别分布一致。如果数据量实在太小考虑合并类别或收集更多数据。6. 进阶技巧用Prophet看薪资时间趋势与模型部署前面做的都是横截面分析如果把抓取时间拉长比如每周抓一次连续抓三个月就能做时间序列分析。Prophet是Facebook开源的时序预测工具适合有季节性和趋势变化的数据。用它可以看数据分析师岗位的平均薪资是否在涨、哪些技能的需求在上升。from prophet import Prophet import pandas as pd # 假设df_time有ds和y两列ds是日期y是当天抓取的平均薪资 df_time df.groupby(crawl_date)[salary_avg].mean().reset_index() df_time.columns [ds, y] model_prophet Prophet( yearly_seasonalityFalse, weekly_seasonalityTrue, daily_seasonalityFalse, changepoint_prior_scale0.05, ) model_prophet.fit(df_time) future model_prophet.make_future_dataframe(periods30) forecast model_prophet.predict(future) fig model_prophet.plot(forecast)changepoint_prior_scale0.05控制趋势变化的灵活度值越大越容易捕捉突变但也容易过拟合。weekly_seasonalityTrue捕捉一周内的波动因为招聘帖的发布量在工作日和周末差异明显。预测结果可以帮你在找工作前判断薪资走势如果趋势向上可以适当提高期望薪资。模型部署方面最简单的做法是用Flask写一个API接收岗位特征返回预测薪资。from flask import Flask, request, jsonify import pickle app Flask(__name__) model pickle.load(open(xgb_salary_model.pkl, rb)) app.route(/predict, methods[POST]) def predict(): data request.get_json() features [[ data[experience_encoded], data[education_encoded], data[city_encoded], data[is_13th_salary], ] data[skills_vector]] pred model.predict(features)[0] return jsonify({predicted_salary: round(float(pred), 2)}) if __name__ __main__: app.run(port5000)部署时注意特征顺序必须和训练时一致skills_vector的长度要和mlb.classes_一致。如果某个技能在训练集里没有对应位置填0。这个API可以集成到自己的求职工具里输入岗位要求就能看到预估薪资。我自己做这个项目最大的教训是爬虫部分花的时间远超预期反爬策略三天两头变后来干脆把抓取频率降到很低用时间换稳定性。模型部分反而比较顺因为XGBoost对特征工程的要求没那么苛刻只要薪资解析对了R2一般能到0.7以上。如果你也想做类似的项目建议先把爬虫跑通数据量到1000条以上再开始建模否则模型学不到什么规律。希望帮到你。本文还有配套的精品资源点击获取