ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Python爬虫实战:从BOSS直聘采集数据到机器学习预测薪资全流程

Python爬虫实战:从BOSS直聘采集数据到机器学习预测薪资全流程 简介本资源是一套面向人工智能与机器学习初学者及求职分析实践者的完整项目实战包聚焦BOSS直聘平台“数据分析师”岗位数据的采集、洞察与预测全流程。资源涵盖网络爬虫requestsBeautifulSoup、数据清洗Pandas、多维度统计分析、Matplotlib/Seaborn可视化含26张生成图表、以及基于scikit-learn的薪资回归建模线性回归、随机森林等与结果解读切实解决求职者市场定位、雇主招聘策略制定等现实问题。压缩包共35个文件含3个核心Python爬虫脚本、3个Jupyter Notebook分别对应分析可视化、城市维度挖掘、机器学习建模、1个清洗后CSV数据集、1份README说明文档及26张可视化输出图整体仅1.29MB轻量易运行。已有205人学习下载提供从原始网页抓取到模型评估的端到端可复现代码、清晰模块化目录结构及关键步骤注释特别适合巩固数据分析 pipeline、理解特征工程逻辑与模型解释方法。 上个月把这个项目从头到尾跑完从爬虫到清洗、分析、可视化再到机器学习预测一套流程下来比想象中费时间。“数据分析师”这个岗位在BOSS直聘上的信息量很大字段也不算规整薪资格式五花八门详情页结构还经常变动。这篇文章就把我的完整实现过程、关键代码、踩过的坑和最终结果一起整理出来给想拿招聘网站练手爬虫和数据分析的朋友做个参考。如果你是刚接触爬虫和数据科学的入门选手这套项目全链路跑完基本能把Python生态里最常用的几个库都过一遍。1. 项目缘起为什么拿BOSS直聘数据分析师岗位练手1.1 选题的三个理由先说为什么要拿招聘网站当数据源。市面上的公开数据源看着不少但真正适合做全链路项目的并不多。BOSS直聘的“数据分析师”职位有几个天然优势第一岗位信息结构化程度高职位名称、薪资、城市、经验、学历、公司规模这些字段都直接暴露在列表页不需要做太多解析第二这个岗位本身跨行业、跨城市分布广样本量容易收集做统计分析时不容易出现“某个城市样本量太少”的尴尬第三岗位描述里的技能词天然适合做文本挖掘和特征工程后续可以跟机器学习预测打通。第三个理由其实是最重要的。我想验证一个问题招聘JD里出现的技能关键词到底能不能预测薪资区间比如一个岗位写了“SQL”“Python”“Tableau”是否真的会比没写这些关键词的岗位薪资高这个问题用招聘数据是能拿到答案的。1.2 技术选型与整体流程我当时的技术栈是Python 3.10主要用到这么几个库用途库说明HTTP请求requests列表页、详情页请求页面解析BeautifulSoup lxml解析HTML结构数据存储SQLite CSVjobId去重与最终数据导出数据清洗pandas numpy字段统一、数值化分词与关键词提取jieba collections.Counter处理岗位描述可视化pyecharts matplotlib生成图表机器学习scikit-learn lightgbm训练薪资预测模型为什么没用Scrapy以这个项目的数据量级requests足够Scrapy的管道和中间件反而会增加心智负担。为什么没用Selenium我判断列表页和详情页的内容都能从静态HTML里拿到没必要上浏览器渲染。事实证明这个判断是对的翻页和详情页提取都稳定只有偶尔遇到风控才需要用浏览器做一次访问验证。整体流程分为五步请求列表页解析职位卡片拿到基本字段和jobId。进入详情页补全岗位描述、职位标签等长文本字段。SQLite里去重保留抓取进度。pandas清洗数据构造特征。分析和建模。1.3 数据规模与字段设计我计划抓取“数据分析师”在北京、上海、广州、深圳、杭州、成都、武汉、南京、西安、长沙这10个城市的前10页数据去重后拿到862条有效记录。字段我设计了13个分别是job_id唯一标识用于去重job_name职位名称salary_raw原始薪资文本city城市experience经验要求education学历要求company_name公司名称finance_stage融资阶段company_size公司规模tags职位标签description岗位描述详情publish_time发布时间url详情链接这些字段已经足够做后面的分析和建模。2. 爬虫实现请求、解析与存储的全过程2.1 列表页URL结构与分页逻辑BOSS直聘的职位搜索列表页URL格式大概是这样的https://www.zhipin.com/web/geek/job?query数据分析师city100010000page1其中query是搜索关键词city是城市编码page是页码。不同城市对应不同的city编码比如北京是100010000上海是100020000广州是100030000。这些编码其实可以通过BOSS直聘的城市选择页面获取但我刚开始写的时候偷了个懒直接在网页里切换城市从URL参数里把编码抄下来做成字典。列表页翻页的核心逻辑就是循环page参数从1开始翻直到拿不到数据为止。我最开始以为招聘网站最多只展示10页实际跑的时候发现确实如此翻到第11页时返回的职位列表为空就可以停止翻页了。要注意的是翻页时需要保持会话的一致性不能每页都用新的Session去请求否则容易被风控。2.2 请求头的关键字段爬取BOSS直聘的难点不在解析而在请求头。这一步踩了坑。我最开始模拟请求时只带了User-Agent结果访问列表页直接被重定向到一个安全验证页面返回内容里没有职位数据。排查后发现BOSS直聘会校验请求头里的Referer和Cookie。Referer需要指向它的首页域名Cookie则需要在访问前先请求一次首页来初始化会话然后带着这个会话Cookie去访问列表页。我当时的基本请求构造是import requests import time import random session requests.Session() headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.zhipin.com/, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } # 先访问首页让session拿到初始Cookie session.get(https://www.zhipin.com/, headersheaders, timeout10) time.sleep(random.uniform(1, 3))这里有个小细节session.get首页之后要让headers里的Cookie保持更新。requests的Session会自动管理Cookie所以后续请求直接使用同一个session.get即可不用手动拼Cookie。爬取频率我控制得比较保守每页请求间隔random.uniform(2, 4)秒。页面不超过10页就算每个城市都爬一遍总请求量也不大完全不需要高并发。2.3 列表页解析逻辑列表页的HTML结构在不同时间会变我抓的时候职位卡片都集中在job-card-wrapper这个class下每页大概30条职位。每个卡片里有职位名称、薪资、公司名、地区、经验学历等信息。用BeautifulSoup解析的核心代码大概是这样的from bs4 import BeautifulSoup import re def parse_list_html(html): soup BeautifulSoup(html, lxml) items [] for card in soup.select(.job-card-wrapper): job_id None link_tag card.select_one(.job-card-left a) if link_tag and link_tag.get(href): match re.search(rjob_detail/(\d), link_tag[href]) if match: job_id match.group(1) job_name card.select_one(.job-name) salary card.select_one(.salary) company card.select_one(.company-name) location card.select_one(.job-area) info card.select_one(.job-info) text info.get_text(|, stripTrue) if info else parts text.split(|) items.append({ job_id: job_id, job_name: job_name.get_text(stripTrue) if job_name else None, salary_raw: salary.get_text(stripTrue) if salary else None, company_name: company.get_text(stripTrue) if company else None, location: location.get_text(stripTrue) if location else None, experience: parts[0] if len(parts) 0 else None, education: parts[1] if len(parts) 1 else None, }) return itemsjob-info这个class里的文本格式一般是“经验不限 | 本科”中间用竖线分割。直接用get_text(|)分割比一个个找子节点要省事。这个技巧在处理同类列表页时很通用。另外需要注意有些职位卡片可能是广告位或推荐位解析出来的job_id可能为空清洗时要过滤掉。2.4 详情页信息补全列表页拿不到岗位描述和职位标签需要进入详情页。详情页URL格式为https://www.zhipin.com/job_detail/{job_id}.html解析详情页时我主要抓两个字段description和tags。岗位描述在.job-sec-text这个class下职位标签则是一个个div下的标签元素。详情页有独立的请求频率控制我每抓5条详情就休眠3秒左右防止触发限制。这里有个容易忽略的点详情页有时候会返回“该职位已下线”的提示这时候解析不到任何内容需要跳过并记录状态。我一开始没有处理这种case导致数据里混进了不少空的description后来清洗时才发现。2.5 断点续爬与存储爬虫最怕跑一半挂了。我的策略是用SQLite存两个表一个是job_list存列表页抓到的所有jobId和基础信息一个是job_detail存详情页补全的信息。job_id设置为主键这样重复抓取时直接INSERT OR IGNORE天然去重。爬虫中断后再次运行时先读SQLite里已存在的jobId集合跳过这些ID只抓新增的。这样即使一次只抓了300条下次也能接着跑。最后把两个表join后导出成CSVimport sqlite3 import pandas as pd conn sqlite3.connect(jobs.db) df pd.read_sql( SELECT l.*, d.description, d.tags FROM job_list l LEFT JOIN job_detail d ON l.job_id d.job_id , conn) df.to_csv(jobs_raw.csv, indexFalse, encodingutf-8-sig)注意CSV编码。Windows下的Excel打开CSV时需要utf-8-sig否则中文会乱码。这是我早期做数据导出经常翻车的地方。3. 数据清洗与特征工程最耗时间的环节3.1 薪资字段的统一清洗阶段最麻烦的是salary_raw字段它长这样15-30K·13薪20-25K·15薪200-300元/天120-180/天5-8千面议不同格式的薪资没法直接比较必须统一成“月薪中位数”这个数值。我的解析逻辑分几步先把“千”转成“K”把“万”转成“K * 10”。判断是否包含“元/天”日薪按每月21.75个工作日换算成月薪。如果包含“·数字薪”说明有年终奖倍数最终月薪可以换算成年包再折算回月薪月薪均值 * (12 年终奖月数) / 12。对“面议”直接设为空值后续填充成None建模时丢弃或单独分桶。核心函数如下import re def parse_salary(s): if not s or 面议 in s: return None s s.replace(千, K).replace(万, K) daily_match re.search(r(\d)-(\d)元/天, s) if daily_match: low, high int(daily_match.group(1)), int(daily_match.group(2)) month_low low * 21.75 / 1000 month_high high * 21.75 / 1000 return round((month_low month_high) / 2, 2) range_match re.search(r(\d)-(\d)K, s) if not range_match: return None low, high int(range_match.group(1)), int(range_match.group(2)) monthly (low high) / 2 bonus_match re.search(r·(\d)薪, s) if bonus_match: months int(bonus_match.group(1)) yearly monthly * months monthly_avg yearly / 12 else: monthly_avg monthly return round(monthly_avg, 2)这个函数把“15-30K·13薪”解析成(1530)/2 * 13 / 12 24.38K。注意这里我用“K”作为单位所以返回的数字是24.38而不是24380。后面分析、可视化、建模都用这个K单位。3.2 学历、经验、公司规模的数值化字符串类的特征必须转成数值才能进模型。学历按等级映射edu_map { 学历不限: 0, 中专/中技: 1, 大专: 2, 本科: 3, 硕士: 4, 博士: 5, } df[edu_level] df[education].map(edu_map)经验字段比较复杂因为有“经验不限”和“1-3年”“3-5年”等。我直接提取区间上限或下限转成年份经验不限 - 0在校/应届 - 01-3年 - 2取中位数3-5年 - 45-10年 - 7.510年以上 - 12这个映射里“1-3年”取2年是基于经验要求的近似不是精确值但作为排序特征够用。公司规模也是类别变量BOSS直聘的选项大概是“0-20人”“20-99人”“100-499人”“500-999人”“1000-9999人”“10000人以上”。我同样转成区间上限的对数或者直接按顺序编码。这里用顺序编码就够了因为规模越大理论上薪资越高。3.3 岗位描述关键词提取数据清洗的另一个重点是description字段。我统计了数据分析师JD里最常见的技能词包括SQL、Python、Excel、Tableau、PowerBI、机器学习、统计学、AB测试、数据挖掘、ETL、数据仓库、Hive、Spark等。处理步骤是把description统一转成小写。用jieba分词。对每个岗位统计技能词是否出现构建独热特征。这里有个小坑英文词大小写不一致比如“SQL”和“sql”、“Tableau”和“tableau”。直接分词再加英文词匹配会漏掉不少。我的做法是先对文本做小写化然后用正则把技能词匹配出来不依赖分词器。skill_list [sql, python, excel, tableau, power bi, powerbi, 机器学习, 统计, ab测试, 数据挖掘, etl, hive, spark] def extract_skill_flags(text): if not isinstance(text, str): text text text.lower() flags {skill: 0 for skill in skill_list} for skill in skill_list: if skill.lower() in text: flags[skill] 1 return pd.Series(flags)最终每个技能列都是0/1标志。这个特征后面直接喂给模型也可以用来做技能词频分析。4. 数据分析与可视化高薪数据分析师需要什么4.1 城市与薪资的分布清洗完数据之后第一个看的是城市维度。我抓了10个城市薪资均值排行大概是这样的城市平均月薪(K)样本量北京27.4142上海26.1138深圳23.895杭州22.576广州18.988成都15.281南京14.864武汉13.969西安12.558长沙12.151这个结果符合直觉但有个例外是杭州。杭州的数据分析师薪资明显高于广州原因是阿里、网易这些大厂拉高了平均值。做可视化时我用pyecharts生成了柱状图用平均薪资排序颜色从高到低渐变便于直观看到城市分档。4.2 学历、经验对薪资的影响学历箱线图显示硕士学历的薪资中位数比本科高约20%博士样本量太少只有6条参考意义不大。但有意思的是大专学历和本科学历在10-20K区间的重叠非常大说明数据分析师岗位对学历的硬性门槛没有想象中高技能和项目经验才是关键。经验维度是最清晰的薪资基本随经验单调上升0-1年平均12K1-3年平均17K3-5年平均22K5-10年平均28K10年以上样本太少但普遍30K以上3-5年是个明显的分水岭从20K跃升到25K招聘方对“能独立带队带项目”的人愿意付出更高溢价。4.3 技能关键词词频与可视化用Counter统计862个岗位描述里的技能词频排名靠前的是Excel652次SQL598次Python486次Tableau312次机器学习286次PowerBI238次统计学198次AB测试164次Hive120次Spark87次把词频做成词云以后基本能看出“数据分析师”的JD现状Excel和SQL是基础Python是加分项机器学习不是必备但正在成为高薪岗位的标配。4.4 高薪岗位与普通岗位的技能差异这是整个分析里最有价值的发现。我把月薪中位数≥25K的岗位归为“高薪组”低于25K的归为“普通组”然后对比两组技能出现率技能高薪组出现率普通组出现率差异Python78%51%27%机器学习52%27%25%AB测试41%16%25%Spark28%8%20%ETL31%13%18%SQL86%74%12%Excel58%79%-21%从这个表格能得出一个结论高薪数据分析师岗位大量要求Python、机器学习、AB测试和Spark而普通岗位更强调Excel。这个结论直接说明技能关键词是可以进入预测模型的强特征。5. 机器学习预测用LightGBM预测薪资区间5.1 为什么预测薪资区间而不是具体数字一开始我也尝试过直接回归预测具体的月薪数值但效果很差R2只有0.3。原因很直观招聘JD上的薪资本身是一个范围不是精确数字我用中位数去拟合噪声很大。加上城市、学历、经验这些特征无法完全解释同一岗位的薪资波动。后来我把问题转成分类把月薪分成四个区间10K、10-20K、20-30K、30K。分类任务对噪声的容忍度更高也和招聘网站的筛选逻辑一致用户关心的是“这个岗位大概在哪个薪资档位”而不是精确到几百块。样本分布大概是10K184条10-20K378条20-30K237条30K63条类别不平衡不算严重直接用分层抽样划分训练集和测试集即可。5.2 特征工程概览进模型的特征分三类类别编码特征城市数字编码、学历等级、经验年限、公司规模编码、融资阶段编码。技能标志特征SQL、Python、Excel等10个技能是否存在。文本长度特征description字符数、tags数量。一共18个特征。融资阶段我按“未融资→天使轮→A轮→B轮→C轮→D轮以上→已上市”的顺序编码因为融资阶段越靠后公司越成熟薪资往往越高。5.3 模型训练与调参我用LightGBM做分类器因为它处理类别特征和稀疏特征的表现稳定速度也快。参数一开始用默认值后来简单调了几个关键参数import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report features df[feature_cols].copy() target df[salary_bin] X_train, X_test, y_train, y_test train_test_split( features, target, test_size0.2, random_state42, stratifytarget ) model lgb.LGBMClassifier( n_estimators500, learning_rate0.05, num_leaves31, max_depth6, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit( X_train, y_train, eval_set[(X_test, y_test)], eval_metricmulti_logloss, callbacks[lgb.early_stopping(50)] ) y_pred model.predict(X_test) print(classification_report(y_test, y_pred, digits3))最终测试集上的准确率在0.62左右macro F1在0.58。对于一个只用了18个特征、样本量仅862条的项目来说这个结果不算差但也远谈不上惊艳。5.4 特征重要性分析LightGBM自带的feature_importances_可以输出特征重要性我按重要度排序后Top5是经验年限城市学历等级公司规模Python技能标志经验和城市占用绝对主导地位这是符合业务直觉的。Python技能标志能进前五说明技能词不是纯粹的噪声。Excel反而重要性很低因为几乎所有岗位都写Excel区分度太差。这给我们的业务启示是如果想提高自己的数据分析师薪资预期最重要的不是学更多工具而是先积累项目和业务经验其次才是Python、机器学习这样的高区分度技能。5.5 混淆矩阵与误差分析绘制混淆矩阵后发现模型最容易把“10-20K”预测成“20-30K”误差主要集中在相邻档位。原因是薪资区间标签化带来的边界问题很多岗位写“15-25K”其中位数20K正好卡在两个档位的边界上分到哪边都有道理。另一个问题是“30K”档位样本太少模型召回率只有0.3很多高薪岗位被预测成20-30K。解决办法之一是改用回归预测月薪中位数再对结果做区间划分或者使用分位数回归直接预测薪资的10%、50%、90%分位数。考虑到数据量这些方案实操起来会比分类更复杂但是更接近真实需求。6. 合规边界和后续优化6.1 爬虫合规建议这个项目做完后我建议所有想拿招聘网站做数据实验的朋友注意三点第一必须遵守目标网站的robots.txt规则。爬取前先看https://www.zhipin.com/robots.txt虽然很多网站不会把完整规则写全但这是基本礼仪。第二请求频率要克制并发数不要拉太高最好设置随机延时。招聘网站属于生产系统爬虫压力过大会影响正常用户访问本质上是不道德的行为。第三抓下来的数据只能用于个人学习和研究不能做商业用途更不能打包出售。我做这个项目时每页请求间隔2-4秒整个抓取过程持续了两三个小时没有对目标网站造成明显压力。这样既能完成任务也不会让自己的IP被过早限制。6.2 后续可以扩展的方向这个项目目前还有很多可以深挖的切口我已经记在TodoList里了。第一个是增量爬取。招聘信息的时效性很强同一个岗位可能过两周就下线了新增岗位也在不断产生。如果每周跑一次爬虫把新岗位和旧岗位都存下来就能做薪资趋势的时间序列分析比如“分析师整体薪资是否在上涨”“杭州和北京的差距在增大还是缩小”。第二个是JD文本的语义分析。我目前只做了关键词匹配其实可以用Bert或text2vec把岗位描述编码成向量再做聚类或匹配能发现不同城市、不同行业对“数据分析师”定义的差异。比如同一个岗位在金融行业的JD可能更强调风控模型在零售行业更强调GMV拆分和AB实验。第三个是模型上线。训练好的LightGBM可以导出为模型文件写一个简单的接口输入“城市、学历、经验、技能关键词”等字段输出预测薪资区间。这样再做简历评估时就不用每次重跑全流程了。最后分享一个我个人的体会这类招聘数据项目的成败不在于模型多高级而在于数据清洗是否认真。薪资字段的脏数据、经验字段的模糊映射、城市编码的口径差异任何一环出问题都会让前面的爬虫工作白费。我自己在这个项目里光清洗和特征工程就花了60%的时间建模只用了不到20%。下次再做类似项目我会先确认数据质量再动手建模而不是一上来就调参。本文还有配套的精品资源点击获取
返回列表