ARTICLE DETAIL

资讯详情

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

Python爬虫实战:Boss直聘职位采集与薪资分析

Python爬虫实战:Boss直聘职位采集与薪资分析 先交代一下背景。最近我花了两周时间基于 Python 把 boss直聘 的职位数据完整跑通了一条采集链路从 API 分析、请求伪装、数据清洗到最后的薪资分析和岗位画像都做了。这篇文章不是代码仓库的搬运是我把整个过程里踩过的坑、做过的取舍、还有对数据合规边界的思考一次性写清楚。如果你正准备做招聘数据相关的爬虫项目或者想拿真实数据练手 Python 数据分析这篇文章应该能帮你少走不少弯路。开头先给个明确的预期管理这个项目只采集公开可见的职位信息岗位名称、薪资范围、任职要求、技能标签这类不碰任何用户个人隐私数据。文中所有方案均基于个人学习研究用途实际使用请务必遵守目标网站的服务条款和法律法规。接下来我会按项目推进的时间线来写从需求拆解到技术选型再到实战中的反爬应对最后是数据怎么变现成洞察。1. 我为什么盯上招聘平台的数据——项目动机与合规前提1.1 一个真实的需求薪资倒挂与岗位趋势调研做这个项目的动机其实挺实际。年初和朋友聊到跳槽发现在同一个城市、同样三年经验的岗位不同行业、不同规模公司的薪资能差出一倍还多。市面上所谓的薪资报告大多是抽样调查样本量小、更新慢而且往往偏向大厂根本反映不了真实市场。我就想能不能自己动手把招聘网站上正在招的岗位数据拉下来做分析用一手数据回答几个问题当前市场上 Python 相关岗位的真实薪资分布是什么哪些技能关键词出现频率最高不同城市、不同经验梯度下的薪资差异到底有多大这个需求听起来直接但真正动手才发现招聘平台的数据恰好是爬虫领域里看着简单、做起来一堆事的典型场景。职位列表页是服务端渲染还是前端异步加载搜索接口的参数怎么构造翻页有没有反爬限制数据量大了之后怎么存、怎么清洗这些问题一个一个排着队等你解决。1.2 先说清楚合规边界什么能爬、什么不能爬在写第一行代码之前我花了整整一天研究法律边界和平台规则。这一节必须放在最前面因为我在各个技术社区见过太多人上来就问怎么绕过验证码怎么批量注册账号这种思路从一开始就跑偏了。我的合规原则就三条只采集求职者肉眼可见的公开信息。职位名称、薪资范围、技能要求、公司名称、工作地点这些是任何用户打开网页不需要登录就能看到的属于公开信息。不碰个人隐私数据。简历联系方式、求职者姓名电话这类字段从设计上就不进入采集范围。技术上能做到不代表可以做这是我给自己定的死线。控制请求频率不影响目标网站正常运行。访问频率控制在人类浏览的正常范围内单次任务限制在数千条数据量级不搞并发轰炸。提示不同国家和地区对数据采集的合规要求不同爬虫项目涉及的法律问题需要结合具体场景评估。本文分享的是技术学习路径不构成法律建议。个人学习研究应当控制数据规模商业用途必须获得平台授权。这些边界想清楚之后后面的技术选型就顺了不需要破解登录态、不需要逆向加密参数、不碰验证码识别只需要老老实实分析 API、构造合法请求、解析公开数据。2. 技术选型对比requests、scrapy还是playwright2.1 三种方案的核心差异先说我评估过的三个方案这可能是大家最纠结的环节。requests 解析库是经典组合轻量、灵活、学习成本低适合接口相对规整、不需要执行 JavaScript 的场景。如果目标页面是纯 HTML 后端渲染这个组合一把梭就够了。scrapy的优势在于框架化自带爬虫调度、并发控制、管道Pipeline处理和中间件机制适合大规模分布式采集。缺点是学习曲线陡峭而且对于 boss直聘 这种接口需要带 cookie 和特定请求头的场景中间件的配置也够折腾一阵。playwright是这几年很火的浏览器自动化方案直接驱动 Chromium 执行页面能处理所有前端异步渲染的场景。最大的好处是不用逆向分析接口像人一样操作浏览器就行。但代价是资源占用高、速度慢而且并发控制需要自己实现不适合纯接口型的数据采集。我最终的选择是requests 解析理由有两个一是 boss直聘 的职位数据走的是独立的接口返回 JSON不需要渲染页面二是数据量级在几千到几万这个区间requests 的串行请求配合合理延时完全够用没必要上一个重型框架。2.2 为什么不是playwright一个务实的选择我看不少新手一上来就选 playwright被可视化爬虫不用分析接口这些点吸引。但实测下来playwright 在这个场景有三个明显短板启动浏览器实例的 CPU 和内存开销很大跑几百个页面就要小心内存泄漏的问题。速度比接口请求慢一个数量级。接口请求单次 200ms 内能完成浏览器渲染要 1-3 秒还要等待网络空闲、处理弹窗。更容易被网站风控系统识别。正常用户行为模型和自动化浏览器行为之间有差异反而增加了被封的风险。当然这个选择有个前提我是先通过浏览器开发者工具确认了职位数据来自独立的 JSON 接口才放心用 requests。如果哪天网站改成纯前端加密渲染requests 方案就失效了到时候再切 playwright 也不迟。技术选型不要迷信最流行要用最匹配的方案。2.3 环境搭建与依赖清单项目环境很简单Python 3.10 几个常用库就搞定了。给一个可以直接参考的依赖清单requests2.31.0 # HTTP 请求 pandas2.0.3 # 数据清洗与分析 BeautifulSoup44.12.2 # HTML 解析备用 lxml4.9.3 # 解析加速 openpyxl3.1.2 # Excel 导出这个组合没有花哨的东西全部是爬虫数据分析的标配。如果你本地还没装用 pip 一条命令就能装上。这里提醒一句建议用虚拟环境别直接装在系统 Python 里后面改版本的时候你就知道好处了。3. 从岗位搜索接口到结构化数据的完整抓取流程3.1 第一步打开开发者工具摸清楚接口规律整个项目的核心突破口就是用浏览器开发者工具F12的 Network 面板观察网络请求。具体操作步骤打开 boss直聘 网站进入职位搜索页面。按 F12 打开开发者工具切到 Network 面板。输入关键词比如Python点击搜索。在请求列表里找 XHR 类型异步请求的条目逐个查看返回内容。我当时很快就锁定了一个名为job/getJobList的接口返回的是标准 JSON里面直接包含了岗位名称、薪资、城市、经验要求、学历要求、技能标签这些字段。这一步是整个爬虫设计的定海神针——确定了数据源后面所有工作都是基于这个 JSON 做的。有一个细节值得注意浏览器开发者工具里的请求是可以右键Copy as cURL的然后能直接转成 Python requests 代码。这是一个网上很多教程不讲的提速技巧我日常做接口分析都用这个方法拿到第一手请求参数。3.2 请求头的伪装少了Referer一定被拦接口找出来之后测试构造请求的第一步就吃了闭门羹——直接返回错误页面。排查了半天发现问题出在请求头Headers上。当时抓到的关键 Headers 信息是这样的已脱敏Accept: application/json, text/plain, */* Accept-Language: zh-CN,zh;q0.9,en;q0.8 Content-Type: application/json Origin: https://www.zhipin.com Referer: https://www.zhipin.com/web/geek/job?queryPython User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Cookie: 需要登录后保持会话的Cookie这里最容易被忽略的就是Referer字段和Origin字段。很多初学者的请求头只伪造一个 User-Agent这完全不够。网站的风控系统会校验请求的来源页面是不是合法的站内页面如果 Referer 缺失或者指向外部站点直接拒绝服务。这个机制的本质就是模拟一个人在浏览器里点击搜索的行为而不是一个脚本直接从别处发起的请求。解决方式很简单在 requests 里构造 headers 字典把上面这些字段全部填上尤其是 Referer 和 Origin。UA 建议用真实浏览器的 UA 字符串别用 requests 默认的一眼就会被认出来。3.3 请求参数解析从 offset 分页到 query 构造接口的请求参数我也仔细梳理了一遍核心参数大概是这几个参数名示例值说明queryPython搜索关键词city100010000城市编码experience1,2,3经验要求筛选degree2,3学历要求筛选salary2,3,4薪资范围筛选page1页码pageSize30每页数量关键的翻页逻辑是通过page参数控制的pageSize固定为 30。这里有个容易被忽略的细节第一页的接口路径可能和其他页不一样有些网站第一页走独立的 SEO 接口第二页起才走标准 JSON 接口。我实测的时候就发现page1和page2的响应结构有细微差别需要分别处理。构造请求的参考代码import requests def build_params(query: str, page: int, page_size: int 30) - dict: return { query: query, city: 100010000, page: page, pageSize: page_size, } def fetch_job_page(session: requests.Session, params: dict) - dict: url https://www.zhipin.com/wapi/zpgeek/search/joblist.json headers { Accept: application/json, text/plain, */*, Referer: https://www.zhipin.com/web/geek/job?queryPython, Origin: https://www.zhipin.com, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, } resp session.get(url, paramsparams, headersheaders, timeout10) resp.raise_for_status() return resp.json()这里有个容易被忽略的坑city编码每个城市都不一样。我一开始直接用城市名传参结果返回空数据后来才发现要用城市编码。这个编码哪里找可以在网站前端源码里搜也可以先手动搜索一次从请求里直接抓。把热门城市的编码做成一个字典后续想跨城市分析会很方便。3.4 数据解析与清洗薪资字段的月薪/年薪陷阱接口返回的 JSON 结构不算复杂核心数据都在zpData.jobList这个数组里每个元素对应一条职位数据。字段不算少实际有效的核心字段大概有岗位名称、薪资范围比如 20-40K·15薪、城市、工作经验要求、学历要求、公司名称、公司规模、融资阶段、技能标签列表、职位描述。薪资字段是我在数据清洗环节最头疼的部分。原始数据长这样20-40K·15薪15-25K8千-1.2万25-35K·14薪面议要拿这些数据做分析必须进行标准化转换。我的处理步骤是先判断单位K 代表千元万代表万元。提取最低值和最高值。计算中位数作为该岗位的代表薪资。解析15薪这类信息换算成年度总收入。具体的清洗函数我贴出来这个函数在后续分析中非常核心import re import pandas as pd def parse_salary(salary_str: str) - dict: 将 20-40K·15薪 这类薪资字符串转换为结构化数据。 if not salary_str or salary_str 面议: return {salary_low: None, salary_high: None, salary_avg: None, annual_months: 12} salary_str salary_str.replace( , ) annual_match re.search(r(\d)薪, salary_str) months int(annual_match.group(1)) if annual_match else 12 salary_part salary_str.split(·)[0] if 万 in salary_part: numbers [float(x) for x in re.findall(r[\d.], salary_part)] low numbers[0] * 10000 / 1000 # 转化为 K high numbers[1] * 10000 / 1000 if len(numbers) 1 else low else: numbers [float(x) for x in re.findall(r[\d.], salary_part)] low numbers[0] high numbers[1] if len(numbers) 1 else low avg (low high) / 2 return { salary_low: low, salary_high: high, salary_avg: avg, annual_months: months, }这个函数虽然简单但包含了薪资分析里最核心的转换逻辑。做薪资分析时务必注意不同规模公司发布的薪资口径差异很大创业公司标注的 15 薪往往有较大波动大厂反而相对固定。数据可视化时建议使用中位数而非平均值后者容易被头部大厂的高薪拉偏。3.5 数据存储落库还是落 Excel数据量在几千到几万这个级别的时候存储方案的选择其实不用太纠结。我最终选了 Excel 作为主力存储辅以 CSV 做中间备份。理由很实在Excel 可以直接用 pandas 读取分析也能用 Excel 本身做透视图表探索对个人项目来说交互效率最高。import pandas as pd # 假设 data 是经过清洗后的字典列表 df pd.DataFrame(data) df df[[job_name, company_name, salary_avg, salary_low, salary_high, annual_months, city, experience, degree, skills, job_desc]] df.to_excel(jobs_python.xlsx, indexFalse, engineopenpyxl)如果你计划长期爬取做趋势监控建议加一个 SQLite 存储轻量又稳定后面做增量更新也方便。但单次调研 Excel 完全够用。4. 反爬机制的识别思路与合规应对4.1 第一道坎请求频率限制跑通单页请求之后我试了直接循环抓取 50 页数据结果第 10 页左右就开始出现响应异常。返回内容变成了一个验证页面或者空 JSON这在爬虫术语里叫请求频率限制。这个机制的原理并不神秘网站会统计单个 IP 在单位时间内的请求次数超过阈值就触发限制。触发后的表现也不止一种——有的直接拒绝有的返回垃圾数据有的会弹验证码。我的应对策略就两条限速和随机延时。限速是指把请求间隔控制在 1-2 秒左右随机延时就是在这个基础上加一个随机数让请求节奏更像人工操作。我实测的经验是串行请求 随机延时 1.0~2.5 秒可以稳定跑完上千条数据不触发限制。如果想用多线程加速要千万小心并发数一旦上去封 IP 的概率直线上升得不偿失。import time import random def safe_sleep(): time.sleep(random.uniform(1.0, 2.5))这个函数的细节就在random.uniform上固定间隔比随机间隔更容易被识别。原因很简单真人操作不可能每次都精准停留在某个固定时间间隔上。4.2 第二道坎Cookie 过期与会话保持boss直聘 的接口请求需要携带 Cookie但 Cookie 会过期。我遇到的具体场景是第一次构造请求时从浏览器复制了一个长 Cookie直接跑能通。但跑了十几分钟后突然某个请求返回 401排查发现是 Cookie 失效了。这个问题的解决办法比较笨但很稳手动从浏览器重新复制 Cookie更新到代码里。因为你只是个人学习研究不是要 7x24 小时运行的生产爬虫Cookie 有效期通常有几天过期了手动换一次就够用。另一种做法是使用requests.Session()对象来保持会话但 Session 对象也依赖初始 Cookie并不能根治问题。这里想强调一个经验爬虫项目最容易翻车的不是代码本身而是运行过程中的各种意外——Cookie 过期、页面改版、请求频率触发。所以一定要给请求函数加上异常捕获和重试机制单次请求失败不要直接中断整个任务。def fetch_with_retry(session, url, params, headers, max_retries3): for attempt in range(max_retries): try: resp session.get(url, paramsparams, headersheaders, timeout10) if resp.status_code 200: return resp.json() else: time.sleep(random.uniform(2, 5)) except requests.RequestException as e: time.sleep(random.uniform(2, 5)) return None4.3 关于验证码和登录墙我的边界原则有一个现象很有意思在技术交流群和论坛里爬虫求助帖最多的问题就是遇到验证码怎么办怎么绕过登录。但我必须明确表个态个人学习项目遇到验证码和登录墙正确的做法是停下来而不是想办法突破。验证码和登录机制本质上是网站所有者的访问控制措施绕过这些机制本身就违背了访问意愿。如果你的爬虫需求非突破这些机制不可大概率说明这个需求本身已经超出了正当学习研究的范畴。做技术的人应该有自己的判断底线不要什么需求都接什么技术都上。我在这个项目里就是老老实实带上自己的登录 Cookie登录后能看到的职位数据比未登录更多不搞任何自动登录、验证码识别的操作。这个边界守住之后整个过程反而特别顺利——只要你的请求行为像一个正常用户遇到的反爬阻力其实很低。4.4 应对网页改版的架构设计爬虫有一个天然弱点一旦目标网站调整了接口路径或参数结构爬虫立刻失效。这是所有爬虫项目都无法回避的现实也是爬虫与网站之间永无休止的攻防博弈。我在这套代码里做了一点针对性的架构设计把所有固定配置接口 URL、字段映射、请求参数从业务逻辑里拆出来单独放在一个配置文件里。网站一旦改版只需要修改配置不需要重写代码逻辑。这个习惯帮我省过不少事上一次网站调整了字段名我只花了五分钟更新映射就重新跑通了。行业里还有一种思路是用 JSONPath/XPath 做字段映射这样即使字段顺序变化也能正确解析。不过看具体项目复杂度简单的字典映射就够用了。5. 爬下来之后怎么变成岗位洞察5.1 数据探索先清洗再统计最后可视化采集到数据只是第一步真正体现技术含量的是后续的数据分析和洞察。我用 pandas 做了一套标准的分析流程这里分享几个最实用的分析维度。第一步先看数据总量和缺失情况print(f总岗位数: {len(df)}) print(f薪资缺失: {df[salary_avg].isna().sum()}) print(f技能标签缺失: {df[skills].isna().sum()})第二步看核心薪资分布。以北京 Python 岗位为例我跑出来的结果大致是薪资中位数约 28K平均薪资约 32K上四分位数约 40K。这个结果和招聘网站晒出的平均薪资有明显差异原因就是中位数受极端值影响更小。第三步按技能标签分组统计平均薪资。把职位描述和技能标签里的关键词做词频统计再计算每个技能对应的平均薪资能得到一张技能溢价表同样三年经验要求分布式架构的岗位比不带这个技能的岗位薪资高出 15%-25%这个洞察直接能指导技术学习路线的优先级。5.2 一个可复用的分析模板城市与岗位交叉视角跨城市对比是最直观的分析维度。我抓了北京、上海、广州、深圳、杭州五个城市的 Python 岗位数据做了个对比城市岗位数量薪资中位数(K)平均经验要求(年)最高频技能北京1520283.2Python, C上海1280273.1Python, Java深圳980253.0Python, 分布式杭州860263.0Python, 算法广州720222.8Python, Web这个表格信息量极大。杭州虽然是二线城市但薪资中位数和深圳几乎持平侧面反映了当地互联网和电商行业的活跃度。同时杭州岗位对算法能力的要求明显偏高这和当地 AI 初创企业扎堆的分布一致。这就是典型的数据驱动决策——不是凭感觉判断哪个城市机会多而是用真实岗位数据说话。5.3 从数据到决策一个具体的求职策略建议基于上述分析我给自己也顺带能给别人得出了几条可执行的结论如果你有三年 Python 经验且熟悉 Web 开发上海和北京的综合回报最优。如果你擅长算法和分布式系统杭州的竞争密度相对低但薪资不逊于一线城市。广州市场对 Python 需求量偏少且薪资相对偏低如果手里有广州和深圳两个 offer薪资谈判空间明显不同。15薪出现在大厂和独角兽岗位的密度远高于创业公司但创业公司的薪资范围上限往往更高。做选择时不能只看月薪要算年度总包。这些结论全部建立在一手数据上比任何职场 App 上的经验分享都有说服力这也是我耗费两周时间做这个项目的核心价值所在。6. 实测中最容易翻车的几个细节6.1 请求头漏字段导致的灵异现象这里要单独说一个我排查了两个小时的怪问题接口单独请求成功但批量运行时频繁失败。后来定位到原因是批量运行时的请求头里少带了一个Content-Type: application/json字段。因为部分接口在 GET 请求时对 Content-Type 不敏感但部分接口会强制校验。这类问题在爬虫里非常隐蔽因为不是每次都失败而是间歇性失败很容易误导你怀疑是网络问题。建议把所有请求头做成一个不可变的配置常量不要每次请求临时构造。这样能保证所有请求发送的头部信息完全一致方便定位问题。6.2 数据编码问题中文乱码的根源另一个高频坑是编码问题。HTTP 响应默认可能是 ISO-8859-1 编码中文直接变乱码。requests 库虽然会自动识别大部分情况但碰上接口不返回 charset 字段的时候就很容易出错。我的做法是强制指定编码为 UTF-8resp.encoding utf-8这个操作在解析 JSON 前执行。6.3 翻页参数不是简单的 page1最后再提醒一个谨慎的翻页设计。我刚开始写翻页逻辑时确实简单认为page参数从 1 递增就行但实际测试发现网站的前几页和后面页请求路径可能有区别尤其是搜索接口存在推荐置顶的内容时第一页的数据可能掺杂了非标准搜索结果。稳妥的做法是先人工翻页观察请求参数的变化规律确认清楚之后再把翻页逻辑固定下来。新手最容易犯的错误就是写死一个 page 变量循环跑一旦网站有个别页返回数据格式不一致整个抓取任务直接崩溃而且你很难定位是哪一页出了问题。我的建议是每次解析后打印页码和返回条数做日志能快速定位中断位置。7. 写在最后的几点经验体会这个项目做下来我最深刻的感受是爬虫的技术难度从来不在写代码这一步而在于对整个系统的理解——协议、安全策略、数据特征、合规边界缺一不可。如果你也想上手类似项目我给三个实操层面的建议第一先从单页爬通开始不要一上来就写全量抓取。先把一个页面的数据抓下来、清洗完、存储好再去拓展循环。单页通了整体就完成了 70%。第二数据分析比数据采集更重要。很多初学者把大量精力花在绕过反爬上但最终拿到的数据只是一堆 JSON 而已不能形成结论和分析项目价值大打折扣。反过来用合理的频率采集一小部分高质量数据分析出的结论可能比全量数据更有洞见。第三守住合规底线才能走远。技术圈子很小口碑很重要。做爬虫项目之前先想清楚边界哪些数据能碰、哪些不能碰不要为了炫技去做突破底线的事。遵守规则不代表技术受限反而是在规则内探索才更有挑战性。我后续的扩展计划是把这个项目做成一个长期的岗位监控工具定期抓取数据追踪岗位数量变化和薪资波动趋势再把分析结果自动生成图文报告。技术上考虑引入 SQLite 做增量存储用 GitHub Actions 定时触发抓取任务。这些都是在现有代码基础上的自然延伸等跑一段时间有了新的数据结论再回来跟大家分享。如果你在复现过程中遇到了什么有意思的问题或者在数据分析环节发现了不一样的结论欢迎来交流。这一行就是这样多碰问题、多分享、才能一起往前跑。
返回列表