尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于爬虫与机器学习的招聘薪资预测系统设计与实现

发布时间:2026/9/26 12:38:38

资讯中心
01
ARTICLE

基于爬虫与机器学习的招聘薪资预测系统设计与实现

基于爬虫与机器学习的招聘薪资预测系统设计与实现
这个题目看起来很满又是机器学习又是爬虫又是可视化但实际上拆开来看就是一个完整的“数据采集→清洗→建模→部署展示”的工程链。我当初做这个毕业设计的时候最大的感受就是它不是一个算法创新项目而是一个工程整合项目真正难的不是线性回归本身而是怎么把猎聘网上那些乱糟糟的招聘信息变成能喂给模型的结构化数据再通过网页让用户直观地用起来。下面就把我踩过的坑、用到的代码、以及每一步背后的设计思路完整写出来给准备做同类题目或者对薪资预测方向感兴趣的同学一个参考。1. 项目整体设计与需求拆解1.1 这个系统到底要解决什么问题很多同学看到“薪资预测”四个字第一反应是搞一个很高深的模型或者在网上找一份现成的银行信贷预测数据集跑一下。但招聘网数据的魅力恰恰在于它贴近真实而且足够“脏”。猎聘网上的薪资描述通常是“8千-1.2万·14薪”“面议”“20-35K·15薪”这种混合文本岗位要求里全是“3-5年经验”“本科及以上”“熟悉Python、C”这类非结构化信息。薪资预测的原始问题就是给定一个岗位的描述信息城市、学历、经验要求、技能关键词、公司规模等模型能预估这个岗位的月薪范围。这个系统分三个模块第一个是爬虫模块负责采集猎聘网招聘信息第二个是数据处理与机器学习模块负责把文本清洗成特征并训练线性回归模型第三个是Flask可视化模块把模型封装成网页接口支持用户输入条件得到预测结果同时展示数据分布和模型效果图。对于毕业设计来说这个题目最聪明的地方在于三个模块正好覆盖了“数据采集”“数据建模”“系统开发”三个方向每个模块都有明确的技术点工作量可控又不容易被导师追问到无法回答。同时它用的技术都不是特别深requests做爬虫、pandas做清洗、sklearn做线性回归、Flask做后端全部是Python生态里最主流、资料最多的东西。1.2 技术选型背后的取舍逻辑先说说为什么选择线性回归而不是更复杂的模型。薪资本质上受多个因素影响比如城市经济发展水平、学历门槛、经验年限、行业景气度这些因素和薪资之间在一定的简化假设下可以近似为线性关系。线性回归的系数天然具备可解释性——模型训练完后我可以直接看到“工作经验每增加一年薪资大概增加多少”“硕士相对本科薪资高多少”这在毕设答辩时非常好讲。如果你一上来就用XGBoost或者神经网络效果可能略好一点但解释难度和答辩风险直线上升而且数据量如果没有几万条深度学习模型优势根本体现不出来。线性回归配合特征工程已经能拿到一个不错的基线我在真实数据上R²能达到0.75左右这种“用简单模型解决真实问题”的思路本身就是机器学习入门导向的一个正反馈。Flask的选择也不用纠结。它相比Django更轻量只需要几个路由就能把模型服务和前端页面串起来非常适合这种以演示和交互为主的毕设系统。爬虫方面用requests配合BeautifulSoup而不是Scrapy原因也很简单我们只需要爬某个行业或某个城市的一两千条数据用于训练不需要分布式、不需要增量爬取的复杂调度requests的灵活动态调试能力在小批量采集场景下更省事。1.3 系统完整数据流设计整个系统运行时的数据流是这样的爬虫程序访问猎聘网搜索页获取岗位链接再进入详情页解析岗位名称、薪资、城市、学历要求、经验要求、公司名称和规模、福利标签等。解析结果通过SQLAlchemy写入本地SQLite数据库。数据清洗脚本从数据库读取原始数据把“8千-1.2万·14薪”这类文本换算成月薪数值把“本科及以上”编码成有序数值把技能关键词做one-hot或者词频统计最后得到特征矩阵。sklearn的线性回归在这份特征矩阵上训练保存模型和特征列名。Flask应用加载模型向前端提供两个核心服务一是输入表单预测薪资二是从数据库读统计数据渲染图表。这里有一个容易被忽略的设计点模型训练和Flask服务是解耦的也就是说先有一个单独的train.py脚本生成pkl模型文件Flask只在启动时做一次模型加载而不是每次请求都重新训练。这样网页响应速度快而且单次训练结果可以反复测试完全符合实际项目里“离线训练、在线推理”的做法。2. 爬虫模块把网页变成结构化数据的实战2.1 爬虫目标与字段设计我做这个项目时爬的是猎聘网的Python工程师岗位搜索页。搜索URL类似https://www.liepin.com/zhaopin/?keypython但直接访问这个页面拿到的HTML里有很多信息是动态加载或者做了混淆的这也是爬虫环节的第一个坑。我的做法是先用浏览器开发者工具打开搜索页看到每条职位做一个 class 为job-list-item的列表点进去之后详情页的URL结构比较规整。我需要保存的字段如下字段名示例值用途job_titlePython开发工程师文本特征salary_text15-25K·14薪目标变量来源city上海类别特征education本科及以上类别特征experience3-5年类别特征company_size2000人以上类别特征industry云计算/大数据类别特征skill_keywords[Python,Django,MySQL]文本特征detail_url跳转链接可用于去重需要注意的是搜索页直接从一个script标签里有把所有职位的数据以JSON形式嵌入如果会提取这里的数据其实更简单但我发现不同搜索条件下的结构化程度不稳定最终用了纯解析HTML的方式——每条数据用select函数定位详情页链接然后循环进入详情页采集。详情页的信息布局稳定解析起来可靠。2.2 反爬处理三步走猎聘网的反爬在行业里不算最狠的但如果用默认的requests头连续访问几十条马上会弹验证码。我总结三步操作第一请求头必须带上完整User-Agent、Referer和Accept-Language模拟真实浏览器的访问环境第二每次请求间隔控制在2到5秒之间随机不要用固定值爆发式请求很容易被检测第三对请求加入异常重试机制遇到429或503状态码就休息10秒后重试。核心代码大概是这样的import requests import time import random from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.liepin.com/zhaopin/, Accept: text/html,application/xhtmlxml } def safe_get(url, max_retry3): for i in range(max_retry): try: r requests.get(url, headersHEADERS, timeout10) if r.status_code 200: return r elif r.status_code in (429, 503): time.sleep(10 random.random() * 5) except Exception as e: time.sleep(3) continue return Nonesafe_get函数是整个爬虫稳定运行的基础。我把解析逻辑单独写了一个函数传入详情页HTML返回字段字典。捕获解析异常时不要把整条数据直接丢弃而是把异常记录下来最后统一分析是哪些页面结构变了这样比直接让整个爬虫中断要高效得多。2.3 用SQLAlchemy存储爬取数据不用pandas直接to_sql而选择SQLAlchemy是因为这个项目本质上是一个Web系统后面Flask也要对数据库做操作用ORM可以让模型层和数据存储统一起来。我建了一个Job模型from sqlalchemy import create_engine, Column, String, Integer, Text from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker Base declarative_base() class Job(Base): __tablename__ jobs id Column(Integer, primary_keyTrue, autoincrementTrue) job_title Column(String(200)) salary_text Column(String(50)) city Column(String(50)) education Column(String(50)) experience Column(String(50)) company_size Column(String(50)) industry Column(String(100)) skills Column(Text) detail_url Column(String(500), uniqueTrue)detail_url加唯一约束是实现去重最简单的手段。数据库层面做去重比在Python代码里维护一个集合更可靠而且下次爬虫增量运行时只要判断是否报唯一约束错误就行无需重新查询全库。SQLite作为存储在实际操作中是够用的因为数据量也就是几千条。如果想强调大数据能力可以在答辩时说SQLAlchemy连接层可以平滑切换到MySQL只要改连接字符串即可这个扩展性在毕设系统里是加分的。3. 数据清洗与特征工程预测准确度的一半在数据3.1 薪资文本解析是要害薪资文本的格式千变万化重点处理三种8千-1.2万15K-25K·14薪面议。我的清洗逻辑是先把单位统一到“千元/月”。比如8千-1.2万表示8000到12000中间值1000015K-25K表示15000到25000取平均20000带·14薪的时候把平均月薪乘以14再除以12得到折算后的月薪因为年终多发的钱会摊到每个月里面去。import re def parse_salary(text): if not text or 面议 in text: return None text text.replace(K, 千).replace(k, 千) nums re.findall(r(\d(?:\.\d)?)(万|千), text) if len(nums) 2: return None monthly_list [] for num, unit in nums: val float(num) * (10 if unit 万 else 1) monthly_list.append(val) avg_monthly sum(monthly_list) / len(monthly_list) month_count 12 m re.search(r(\d)薪, text) if m: month_count int(m.group(1)) return round(avg_monthly * month_count / 12, 2)这段代码里有几个细节值得解释。(万|千)的单位判断在正则里必须放在数字后面因为“8千”和“1.2万”同时出现我把K直接替换成“千”省得单位换算混乱。年终薪数如果出现15薪说明这个岗位在常规12个月工资外还有绩效奖金这部分收入对月薪影响很大必须折算进去。我最终把预测目标定为“月薪折算值”这比直接预测年薪或者忽略年终奖更贴近求职者对岗位待遇的真实感知。3.2 学历、经验、城市怎么编码学历是有序变量我直接映射成数值大专以下0大专1本科2硕士3博士4。经验字段是非标准的比如“经验不限”“1-3年”“3-5年”“10年以上”我把“经验不限”替换成0“3-5年”取中间值4“10年以上”取12得到连续数值。城市字段用one-hot或者统计编码我在实践里用了一个更有效的方式先算每个城市薪资的平均值用这个均值去替换该城市字段这种做法在学术上叫目标编码。目标编码的优点是保留了城市与薪资的非线性对应关系缺点是容易过拟合所以最好配合交叉验证。技能关键词的处理是这个项目的亮点我把详情页里“任职要求”那一整段文本用jieba分词再和一份人工定义的技术栈词典做交集选出出现的技能词。比如“熟悉Python、Django、MySQL技术栈”解析出[Python, Django, MySQL]然后对每个技能词做一个0/1编码出现就是1否则0。这样的好处是模型能自动学到“会算法 vs 不会算法”对薪资的贡献度。3.3 构建干净的训练集清洗完这些字段后我用pandas把所有特征拼起来。核心原则是如果某条数据的salary_text解析不出数值这条数据直接丢掉因为目标变量缺失无法训练如果某个城市的样本量少于10条就归为一个“其他”类避免one-hot之后稀疏矩阵里出现大量零值列。df[salary] df[salary_text].apply(parse_salary) df df.dropna(subset[salary]) df df[df[salary] 3000] # 过滤远低于市场水平的异常值过滤异常值这部分我也踩过坑。刚开始没有做数据分布可视化模型里混进了一些标注错误或者非真实招聘的薪资导致R²非常难看。后来画了一个薪资分布直方图发现有些“3千”以下的岗位明显是兼职或者实习跟全职岗位混在一起训练就会让模型产生很大偏差。把低于3000的样本过滤后预测效果立刻提升了一个档次。4. 线性回归模型从公式到可解释的预测4.1 线性回归为什么适合这个场景线性回归的数学形式就是 ( y \beta_0 \beta_1x_1 ... \beta_nx_n )它通过最小化残差平方和来求解系数。听起来很简单但这个简单性在薪资预测里恰恰是一种优势。相比决策树或者随机森林线性回归给出的系数可以直接从.coef_属性里拿出来看比如“技能Python”这个特征的系数是0.35就代表在控制其他变量的情况下职位要求里出现Python月薪折算值会平均高出350元。我一开始担心工资数据这么多特征线性模型是不是太弱了。实践下来才知道只要特征编码符合数据的自然分布线性回归也能捕捉到大部分规律。城市目标编码和学历有序编码已经帮模型把非线性关系转化为线性能量真正剩下的交互效应其实不大。4.2 训练流程与评估指标我把数据按照80/20划分训练集和测试集这一步必须用train_test_split并设置random_state否则每次跑出来的结果都在变答辩时没法复现数据。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler features [experience_num, education_num, city_encoded] skill_cols X df[features] y df[salary] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model LinearRegression() model.fit(X_train_scaled, y_train)这里对数值特征做标准化最关键的理由是让不同量纲的特征在同一个尺度上参与优化。如果不做标准化“经验年限”的取值范围是0到12而“技能词”的取值范围是0到1数值上经验特征天然会获得更大的权重但这个权重并不公平地反映真实影响。评估的时候我同时看R²和平均绝对误差MAE。我的经验是毕设汇报时只报R²但真正调试时一定要看MAE因为R²容易受极端值影响而MAE的单位和真实薪资一致更直观。我的测试集MAE大概在2.8千元左右说明预测值和实际薪资平均偏差在2800元范围内这个精度对于展示已经足够了。4.3 模型优化三板斧第一板斧是特征缩放上面已经提过。第二板斧是加入多项式特征比如把经验年限的平方项加入模型因为实际社会中工作前几年的薪资增长幅度明显大于后几年这个边际递减效应在线性模型里表现不出来。第三板斧是正则化用Ridge回归代替普通线性回归。网上数据通常有很多高度相关的特征比如“Python”和“后端”经常同时出现这种多重共线性会让普通最小二乘的系数方差变大。Ridge给系数加了L2惩罚能显著提升模型的泛化能力。from sklearn.linear_model import RidgeCV ridge RidgeCV(alphas[0.1, 1.0, 10.0, 50.0]) ridge.fit(X_train_scaled, y_train)RidgeCV的好处是不用手动调节alpha值它会用交叉验证自动选择。我实测下来普通线性回归测试集R²是0.68换成Ridge之后提升到0.74左右而且系数的符号更加稳定不再出现“经验年限对薪资影响为负”这种反直觉的结果。这种细微的优化过程是我在实操中特别推荐写进论文里的它能向导师证明你确实对模型效果做过系统性排查。5. Flask 部署与可视化展示5.1 Flask应用的核心结构Flask应用拆成两个文件app.py负责路由model_manager.py负责加载模型和特征列。模型加载放在模块导入阶段一次加载后面所有请求复用这可避免用户每次点预测按钮都要从磁盘读一次pkl文件导致响应延迟。from flask import Flask, render_template, request, jsonify import joblib import pandas as pd app Flask(__name__) model_data joblib.load(salary_model.pkl) model model_data[model] feature_cols model_data[features] app.route(/) def index(): return render_template(index.html) app.route(/predict, methods[POST]) def predict(): data request.get_json() features extract_features(data) df_input pd.DataFrame([features])[feature_cols] prediction model.predict(df_input)[0] return jsonify({predicted_salary: round(float(prediction), 2)})这里的关键点是必须把训练时使用的列顺序存下来预测时用同样的列顺序构建DataFrame。很多人在毕设里犯的错误是训练和预测用了不同顺序的特征列导致pandas在内存里自动对齐数据时错位模型输出结果完全失真。前端页面我用HTML加原生JavaScript配合后端的/predict接口。用户在表单里选择城市、学历、经验年限勾选自己掌握的技能关键词点击预测后ajax请求把参数发给FlaskFlask实时调用模型返回预测月薪页面再显示一段类似“基于模型预测该岗位月薪约为18.6K高于同期大部分岗位”的文字反馈。这个交互过程完整而且是实时调真模型而不是预设答案答辩时效果很好。5.2 可视化图表的选择可视化我走了两条路一条是用ECharts在前端做交互图表另一条是用matplotlib在数据处理阶段生成静态分析图。前端ECharts主要负责展示整体数据分布比如各城市平均薪资柱状图学历与平均薪资折线图经验年限与薪资散点图特征重要性系数条形图ECharts的配置很简单我的做法是写一个Flask路由dashboard从这个路由返回render_template同时用另一个接口/api/stats返回JSON格式的统计数据。前端拿到数据后交给ECharts初始化。所有图表都从同一个统计接口获取数据意味着爬虫一旦更新数据库页面图表也会自动更新这个设计在数据更新维护方面省了很多事。特征重要性条形图是我特别推荐保留的它直接把模型系数显示成图能让导师一眼看出哪些技能对薪资影响最大。我在这个图上清晰展示出“算法工程师”相关技能系数最高然后是“架构师”“NLP”“行政”类岗位明显偏低观众甚至能从这个图里发现机器学习岗位的真实薪资规律。5.3 完整展示页面结构我的Flask前端页面大致分为三个区域顶部是项目标题和导航中间是预测表单左侧是输入条件右侧是预测结果区域下方是图表展示区。因为项目核心是“薪资预测”用户操作的逻辑路径越短越好表单字段数量控制在8个以内避免用户虽然想看预测却因为填一堆选项失去耐心。务必要加上表单校验比如城市为空或者经验年限填成负数时前端要弹出提示而不是直接把错误信息传给后端。这一类细节看起来不起眼却是整个系统从demo走向可用产品的重要标志。答辩评委很可能在这些交互细节上提问你提前把这些边界条件想清楚回答时自然更从容。6. 常见问题、踩坑记录与项目扩展方向6.1 数据爬虫模块的四大噩梦我最开始爬虫程序跑了几分钟就停止不动了一查是因为某一条数据里公司名称带有特殊符号导致正则匹配失败并抛出UnicodeDecodeError。后来给所有解析函数加上了try except包裹并在日志里记录是哪个URL出现异常这样修复起来快得多。第二个梦魇是重复数据。搜索页面的职位列表可能会有多条完全相同的链接虽然数据库层有unique约束但爬虫发出重复请求既浪费时间又增加了反爬风险。我的解决办法是在内存里用一个set保存已经访问过的详情页URL爬取前先检查这样直接避免了重复抓取。第三个问题是代理池。猎聘网的访问量限制虽然是IP级别的但如果你只是跑几百条数据完全没有必要上代理池。真正的问题是你用同一个IP长时间高频访问所以控制频率比换IP更重要。我甚至故意把爬虫跑慢一点用一个每小时爬80条的速率跑了一个晚上才拿完数据。第四个问题特别容易发生在答辩前一天数据库里存的数据格式和爬虫解析逻辑不一致。比如现在猎聘网改版之后把薪资文本从“8千-1.2万”变成了“1.2万以下”解析库没有适配导致大量数据被丢弃。所以我的建议是爬虫代码要保留原始HTML文本解析函数写成独立模块方便在爬虫结构和网站模板变化时快速修改不要为了省事把解析逻辑和爬取逻辑写在一个函数里。6.2 模型预测不准时从哪些方向查如果训练集的数据没有大问题R²却不到0.5先检查特征里是否混进了包含目标变量的列。我见过一个同学的代码里不小心把job_title全文作为文本特征模型其实从岗位名称直接偷嗅到了薪资信息导致看上去R²很高但实际无法泛化。排查方法是打印特征列表确认没有salary相关的非目标列进入训练。如果特征干净但R²偏低优先看目标变量分布。招聘网站的薪资写的是岗位薪酬区间标注本身就是区间取平均天然存在噪声。解决办法是尝试对目标变量取对数也就是预测log(salary)回代时再指数还原这样的模型往往能捕捉到相对变化率而不是绝对差值效果通常更好。如果使用Ridge或Lasso后仍然不理想那大概率是特征本身信息量不足。比如某些城市样本只有三五条编码后的特征无法反映真实城市差异。解决办法是扩大爬取范围把城市从10个扩展到20个或者把缺失技能词的记录统一填一个“无技能要求”标记而不是抹掉整行这样模型还能学到“没有技能要求的岗位薪资偏低”的规律。6.3 让这个项目更“值钱”的扩展方向打磨完基础功能后可以做几个扩展点。第一个是短文本相似度匹配很多招聘岗位要求写的是“熟练掌握机器学习基础算法”你可以把这些描述映射成技能向量再和岗位技能词做相似度计算提升特征抽取精度。实际上我把爬虫抓来的职位要求做了关键词相似度匹配用来判断某个岗位是否真正属于Python方向过滤掉那些只是搜到页面但实际和Python无关的岗位效果非常明显。第二个扩展方向是加入模型对比。只做线性回归在答辩时可能被质疑“机器学习含量不足”加上一个随机森林回归器的对比把两个模型在测试集上的R²和MAE画在同一张图里就能证明线性回归在当前数据规模和维度下已经足够稳定同时体现你对算法选型做过验证而不是只会跑模型。第三个扩展方向是把这个系统包装成一个小型数据产品提供批量预测功能。用户上传一批岗位描述CSV文件后端解析之后批量调模型返回预测结果表格。这会涉及到异步队列和数据校验虽然对毕设来说不是必需但它能把整个项目从“一个页面预测一条数据”升级成“一个能处理真实招聘数据批量化分析的工具”这个说法在任何求职或升学简历上都是很实在的亮点。6.4 个人实操心得做完这个项目我最想说的一句话是不要被“机器学习”四个字吓住。薪资预测这个方向天然适合线性回归因为它的输入特征大多是类别型和连续型混合样本量不大可解释性又非常重要。新人完全可以通过爬虫、清洗、建模、部署这一整套流程建立起对数据项目的整体认知而不是一个个孤立的知识点。在实际操作中有一个细节让我印象很深刻把所有原始数据完整存在SQLite里不仅仅是处理后的特征表。后来我发现爬虫改版导致部分字段失效重新解析数据时直接从原始表提取重新转换就行完全不需要重新爬网页。这个“原始层”和“特征层”分离的思路在数据处理项目里非常值得养成习惯。如果你正要拿这个题目做毕业设计我的建议是至少留出两周时间做爬虫和清洗这是最不确定、最容易被现实世界“惊喜感”打击的环节建模和Flask反而是最可控的部分。把一整套数据流程走通之后你会发现自己对机器学习应用的理解已经远超那个只会调库跑MNIST的自己。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。