资讯动态

招聘薪资爬虫实战:从requests到pyecharts的全链路解析

发布时间:2026/10/9 20:57:20 来源:尧图企业网站定制
先说个挺真实的场景。上个月我想评估一下不同城市Python开发岗的真实薪资水平招聘App上翻了几十页职位信息倒是多但全挤在手机屏幕里城市、经验要求、薪资范围这些字段根本没法横向对比。我当时脑子里冒出的第一个念头就是这活儿该交给爬虫干。于是就有了这个项目——手搓一个针对招聘平台的薪资数据抓取工具最终聚合出一份可以按城市、经验、薪资区间交叉分析的“行业薪资报告”。这个项目适合谁参考一是刚学完Python基础、想找真实项目练手的爬虫初学者二是做招聘数据观察、人力分析这类工作想快速低成本获取一批公开职位数据的从业者。技术栈很朴素requests发请求管理会话XPath选节点pandas做清洗pyecharts出图。难度控制在中等偏下重点不在代码多高级而在完整走通“请求-解析-清洗-分析-可视化”这条链路。下面我从选型、合规边界到实战细节一条条说清楚。1. 为什么选择手搓爬虫现成工具与真实需求的落差1.1 通用爬虫工具在招聘网站面前的优势与短板很多人一听说要抓招聘数据第一反应是找个现成的可视化爬虫工具——后羿采集器、八爪鱼这类。这些工具对付静态页面确实省事鼠标点几下就能把表格数据导出来。但遇到招聘网站这种有登录态、有动态接口、有反爬校验的站点其配置过程会变得非常痛苦你得在界面里一步步点出“登录后采集”“翻页规则”“字段映射”稍有不慎就抓漏一列排查起来反而比写代码更麻烦。更关键的是通用工具很难处理“薪资范围”这类非标准字段。同一页面上“20-40K·15薪”和“面议”并存工具只能原样导出后续还得自己加工。而用代码写解析规则完全可控遇到异常字段随时可以加逻辑兜底。对于“最终要生成报告”这种需求来说灵活性就是核心竞争力。1.2 我的技术选型requests lxml pandas 的理由选型这件事我建议别贪多。起初我也动过用Scrapy的念头框架成熟、调度器完善但对于单机、小规模的爬取任务Scrapy的组件和中间件体系反而显得重。最终我选了四件套requests处理HTTP请求和会话保持。它足够轻量Cookie管理也比较顺手一个Session对象就能维持登录态。lxml XPath解析HTML和提取字段。相比BeautifulSouplxml的解析速度明显快XPath的表达式系统在处理“要找某一类节点”的场景时很直观。pandas清洗和聚合数据。薪资文本转数值、按城市分组算中位数这些操作用DataFrame做几行代码就搞定。pyecharts / matplotlib可视化输出生成柱状图、饼图、城市薪资对比图。这套组合的好处是每一层都可以独立替换。比如你不想用pyecharts换成Plotly也行不影响前面的采集和清洗逻辑。1.3 最终目标拆解从网页到“行业薪资报告”的关键路径整个项目看起来复杂但拆开其实就五个环节定位数据接口找到列表页背后的XHR请求拿到返回JSON的地址和参数。构造请求带上必要的Headers维持会话控制请求频率。字段提取从JSON或HTML中取出岗位名称、城市、薪资、工作经验等字段。数据清洗把“20-40K·15薪”这种文本拆成可计算的数值。聚合分析按城市/经验维度算中位数或平均值出图表。把这条路径理清楚后面每一步都是在填具体的坑。2. 动手之前的合规边界把项目当学习项目而不是风险项目2.1 不碰个人敏感信息只取公开职位字段在敲第一行代码之前我花了不少时间想清楚什么能抓、什么不能碰。招聘网站上的职位名称、薪资范围、公司所在城市、工作经验要求属于企业公开发布的职位信息用于求职者筛选属于合理的公开数据范畴。但求职者的简历、联系方式、个人主页、用户ID这类信息属于个人数据绝对不碰。这个边界必须在一开始就立住。我给自己定的采集范围就是一个白名单岗位标题、薪资描述、城市、工作经验要求、学历要求。连公司名称我都尽量做脱敏处理只在内部关联分析时用不会存到任何公开仓库里。2.2 控制节奏和总量避免对目标站点造成压力爬虫对目标站点的压力是实实在在的。一个正常的招聘网站每天有几十万次访问你多几十次没问题但如果你用并发20、间隔0.1秒去刷服务器日志里很快就露馅了。我做项目时给自己设了几个硬指标请求间隔不低于0.8秒最好随机到1到2秒之间。不设多线程单线程跑最多加一个简单的限速器。采集总量控制在一个较小样本范围比如只抓前5页、每页15条总共几百条数据。只在工作时间之外的低峰时段跑避免影响正常用户访问。这些限制不会让项目“不够酷”但能让它不惹事。学习爬虫的目的是理解HTTP、理解网络数据交换不是把目标站点的资源耗尽。2.3 学习角度看待反爬理解防护才能真正学会防护网上经常有人问“Java的Controller层如何防爬虫”“前端怎么防止查看源码”这些问题背后其实是同一种诉求——理解爬虫的行为特征才知道从哪拦截。我对这个项目的定位是安全学习视角分析招聘网站为什么设置反爬、它是怎么识别机器行为的这比单纯“抓到数据”有价值得多。所以本文后面讲到的Headers伪装、频率控制、验证码触发条件等我都尽量从“如果我写后端接口我会怎么拦”的角度来讲。理解了攻击者的思路才能设计出更稳的防护策略。3. 请求层的攻防细节Headers伪装、会话保持与接口定位3.1 浏览器是怎么发起请求的爬虫就要怎么发爬虫最容易翻车的地方不在解析而在请求阶段就被识别。招聘网站的服务器会在请求到达Controller层之前就做一次基础体检User-Agent是不是常见的浏览器、Referer是不是本站页面、请求头里有没有关键的Accept-Language。我第一次跑项目时直接用默认的requests头几秒钟就被拒了——返回的response里连登录页都不是直接给了一个安全校验页。后来我把浏览器开发者工具里Network面板看到的请求头完整复制过来问题就解决了。核心代码如下import requests from requests.adapters import HTTPAdapter session requests.Session() session.headers.update({ 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/web/geek/job, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, }) session.mount(https://, HTTPAdapter(max_retries2))有人觉得Referer这种细节无所谓其实它恰恰是后端判断“请求从哪来”的重要依据。招聘平台的搜索页跳转到列表页时真实请求的Referer一定是站内页面不会是空的。后端Controller层拿到一个Referer为空的请求可信度立刻下降一个层级。3.2 绕过列表页直接在XHR接口里拿结构化JSON很多招聘网站的职位列表不是后端渲染的HTML而是前端通过XHR请求获取JSON数据再渲染。这意味着我们不需要去解析难看懂的动态HTML直接找到背后的XHR接口就行。操作方法很简单打开浏览器开发者工具的Network面板切到Fetch/XHR分类在网页上点下一页观察哪个接口跟随页码变化返回数据。通常这类接口的响应体里就藏着岗位名、薪资字段、城市ID等结构化的JSON数据而且字段名往往比页面DOM更规范。我从接口拿到的数据长这样{ code: 0, data: { list: [ { jobName: Python高级开发工程师, salaryDesc: 20-40K·15薪, cityName: 上海, jobExperience: 3-5年, jobDegree: 本科 } ], total: 100 } }直接解析JSON避免了XPath在小程序页面里的复杂度这是本项目的首选路径。不过要提醒一句接口字段名可能会随时调整上次还是salaryDesc下次更新可能变成salaryShow。代码里要考虑字段不存在时的兜底逻辑。3.3 安全防护者视角如果我是平台方我会怎么拦站在后端开发的角度我接到“如何防护爬虫”这个问题时第一时间会想几个点。Controller层最基础的防护是频率限制——用Guava RateLimiter或者Redis记录每个IP单位时间内的请求次数超过阈值就直接返回503或者弹验证码。然后是Headers校验User-Agent是否合法、Referer是否站内、Cookie中是否有真实的登录会话。招聘平台比较狠的一招是“行为特征分析”正常用户会在某个页面停留几十秒甚至几分钟爬虫则是毫秒级跳转。所以即使Headers伪装得天衣无缝只要请求间隔过短后端从访问日志里也能识别出异常。理解了这些你在写爬虫时就会主动控制频率——这恰恰是最高效的“保命”手段。4. 页面解析的正确姿势XPath文本提取与JSON解析的取舍4.1 XPath的text()与contains()实际用法虽然接口JSON是首选但总有些数据只有页面渲染后才有例如某些单独的职位详情页字段是写在HTML里的。这时候XPath就派上用场了。XPath里最常用的两个函数是text()和contains()。text()用于提取节点内的纯文本contains()用于按文本内容模糊匹配节点。比如我要提取岗位标题和薪资from lxml import html doc html.fromstring(page_source) job_title doc.xpath(//div[contains(class, job-title)]//text())[0].strip() salary doc.xpath(//span[contains(class, salary)]/text())[0].strip()注意一个问题XPath返回的是列表哪怕只有一个元素也是列表。直接取下标的写法在元素不存在时会抛IndexError我一般会先判断列表长度或者用try/except包一层。这个坑我踩过很多次尤其在不同页面结构不一致时。4.2 接口JSON解析比XPath更省事但依赖字段稳定性对列表页这种有XHR接口的场景我强烈建议优先解析JSON理由有三点。第一JSON是键值对结构不需要关心页面样式变化第二接口返回速度远快于下载整个HTML第三JSON里的数值字段往往是干净的原始值不需要像HTML那样做文本清洗。但JSON解析也有隐患字段名不稳定。同一个招聘平台版本升级后可能把cityName换成city_name也可能整体结构从list套一层变成list2。所以我在解析JSON时写了一个兼容函数用get方法逐个尝试候选字段def extract_field(item, candidates): for key in candidates: if item.get(key): return item.get(key) return None这样即使平台改了字段名代码也能尽量保持稳定。4.3 招聘页面的“反爬混淆”字体加密与字段错位招聘类网站里有一类比较典型的反爬手段叫“字体加密”——页面里的数字和关键文字显示正常但复制出来是乱码因为页面使用了自定义字体把看到的“20K”映射到另一个字符编码上。直接从HTML里用XPath提取文本拿到的是被映射后的乱码。遇到这种情况我的处理原则是不硬刚。字体加密这种手段的目标就是防止机器直接读取页面文本除非你花大精力去分析字体映射文件否则性价比极低。更聪明的做法是回到XHR接口——字体加密主要作用于HTML渲染层接口JSON里的原始数字一般还是正常的。如果接口也被加密那说明平台确实下了血本这时候我会考虑换一个数据源而不是跟它死磕。5. 反爬拦截的实战应对验证码、访问频率与登录态5.1 频率控制最容易被忽略但最有效的“保命”手段很多人被抓了以后第一反应是“换个IP”但实际上对小型学习项目来说频率控制比换IP重要得多。招聘平台的封禁策略通常是先警告后降级频繁触发会从“验证码偶尔出现”升级到“所有请求强制验证”这个坑我踩得很实在。我的实现方式是写一个简单的限速装饰器保证任意两次请求之间至少间隔1秒并在此基础上加一点随机抖动。随机抖动很重要因为固定1秒的间隔是机器行为特征服务器完全可以通过时间戳分析识别出来。真实用户的操作间隔不会有那么精确的节拍。import time import random def throttle(func): def wrapper(*args, **kwargs): time.sleep(random.uniform(1.0, 2.0)) return func(*args, **kwargs) return wrapper5.2 验证码与安全验证不硬刚摸清触发条件招聘平台的反爬体系里验证码和安全验证是常见的“中级惩罚”。触发条件通常有这么几类请求频率超过阈值、User-Agent或Headers明显异常、缺少登录态却高频访问需要登录的接口。我在项目里遇到过一次验证码当时的处理不是去找识别方案而是反推触发原因。排查之后发现是我在测试时开了两段代码同时跑原本设置的间隔被打破了。修正频率后验证码就再也没有出现过。所以我给读者的建议是遇到验证码先别想着怎么绕过停下手里的循环看看自己是不是违反了“人的操作节奏”。硬刚验证码只会加重平台对你的限制最后连正常访问都进不去。5.3 从后端防护者视角看爬虫Controller层限流与校验我在热搜词里看到“java controller层如何防护防止爬虫”这个话题值得展开。作为后端开发者我在写接口时经常会做以下几层校验频率维度针对IP做单位时间内的请求计数超过阈值直接丢弃或降级。Headers维度校验User-Agent的合法性、Referer是否来自本站页面。登录态维度核心接口强制校验Cookie或Token未登录请求一律403。行为维度记录相邻请求的间隔分布、页面停留时长通过统计特征识别机器行为。理解这些之后你在构建爬虫时自然会反过来约束自己Headers做得像浏览器请求间隔模拟真实用户没有登录态就不去碰需要登录的接口。这种“攻防双方互相理解”的视角才是爬虫学习最值钱的地方。6. 从数据到报告薪资清洗、区间换算与可视化6.1 薪资字段的“脏数据”处理从“20-40K·15薪”到数值抓下来的数据很少是干净的数值。招聘网站里的薪资描述五花八门“20-40K·15薪”“10-15K”“5千-8千”“面议”混在一起。要分析必须先统一格式。我的清洗思路是先把文本里的数字区间提取出来计算中位值作为该岗位的“代表薪资”。比如“20-40K”取中位30K“10-15K”取中位12.5K。对于“5千-8千”这种中文数字需要先做一次单位换算统一转成K为单位。用正则表达式处理这类文本非常方便import re import pandas as pd def parse_salary(text): if not text or 面议 in text: return None match re.search(r(\d)(?:-(\d))?([Kk万])?, text) if not match: return None low int(match.group(1)) high int(match.group(2)) if match.group(2) else low unit match.group(3) or K if unit 万: # 假设单位为月薪转成K low, high low * 10, high * 10 return (low high) / 2这个函数处理不了所有情况比如年薪和月薪混写但作为MVP完全够用。我建议你也在自己的项目里从简单规则开始看到新格式再补充规则而不是一开始就写一个完美万能的正则。6.2 面议数据的处理策略“面议”这个字段很麻烦。如果直接剔除样本量会缩水如果当0处理统计结果又会被严重拉低。我采用的是“双轨制”计算中位数和平均值时剔除面议但在报告中单独统计“面议岗位占比”用来观察哪些城市或岗位档位更倾向于面议。实际分析中就发现部分城市的高级岗位里“面议”占比明显偏高这说明面议不等于低薪反而可能代表高薪定制化。所以千万不能把面议直接记为0那是典型的统计失真的反面教材。6.3 可视化中位数、城市对比与岗位分布数据清洗完之后我用pandas做了一次简单的分组聚合按城市计算薪资中位数和岗位数量df pd.DataFrame(records) df[salary_mid] df[salaryDesc].apply(parse_salary) valid_df df.dropna(subset[salary_mid]) city_stats valid_df.groupby(cityName)[salary_mid].median().sort_values(ascendingFalse).head(15)然后直接用pyecharts画柱状图中位数排前15的城市一张图就看明白了。pyecharts的优势是交互性好鼠标悬停能看到具体数值。如果你偏好静态图片matplotlib也够用。6.4 横坐标太密之类的绘图小坑热搜词里有人问“python画图横坐标太密集”这个问题在画城市对比图时特别常见。城市名最多十几个柱状图横坐标还可以承受但如果你画的是“岗位名对比”几十个标题挤在一起x轴会糊成一团。解决办法有两个一是旋转标签plt.xticks(rotation45)二是采样显示只展示前10或每隔N个显示一个。我更推荐后者因为旋转只能缓解拥挤信息量太大时依然看不清。7. 踩坑实录三个真实问题与完整排查链路7.1 翻页参数“加密”导致的重复数据项目做到一半我发现翻页抓取时数据大量重复后20页返回的岗位和前5页完全一样。起初我以为是解析逻辑出错了后来把原始响应保存下来对比才发现问题出在翻页参数上——列表接口的翻页参数不是简单的页码而是由某个JS脚本动态计算出来的加密签名直接传page2会被服务端忽略默默返回第一页的数据。排查链路是先怀疑解析层再怀疑请求层最后定位到参数构造层。这个问题的教训是在动手大规模抓取之前先用小样本验证“翻页之后数据内容是否真的变了”别等到跑完几千条数据才发现全是一页的重复。7.2 Cookie过期后请求返回登录页还有一个坑是登录态过期。招聘网站的主要列表页默认需要登录Session里的Cookie在几小时后会失效。失效之后接口依然能返回状态码200但响应体变成了登录页的HTML。由于请求对话没有报错代码里如果不对内容做校验你会拿到一堆无用的登录页数据。我的排查方法是在解析之前先检查响应内容里是否有“登录”这种关键字或者直接检查响应URL是否被重定向到了登录地址。一旦检测到就暂停采集提醒自己手动刷新Cookie而不是继续跑浪费时间的循环。7.3 数据量不足时的统计失真与“面议”占比过高最后一个坑来自数据分析层。最初我抓的数据量比较少只有几十条有效薪资记录画出来的城市中位数分布图特别离谱——某个城市只有一条45K的数据就成了“全国最高薪城市”。这显然是统计失真。解决办法是加了一个最小样本量过滤每个城市至少有5条有效薪资记录才参与排序不足5条的归入“其他”。同时把“面议”占比一起展示出来避免读者只看中位数而忽略数据覆盖范围。这个小改动让报告的可信度高了一大截。做完整个项目我最深的体会是爬虫的核心不是代码而是对目标系统工作方式的理解——理解它怎么发请求、怎么存储数据、怎么设防你才能真正平稳地把数据拿回来。建议你把抓取范围控制在一个很小的样本内只做技术验证和个人学习使用跑通链路后把代码里的关键参数抽成配置项方便下次快速复用。最后分享一个小技巧把每次请求的原始响应保存成文件排查问题时效率能翻倍。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑