尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Boss直聘岗位数据抓取实战:requests+代理IP池搭建与反爬应对
1. 项目缘起与整体设计思路1.1 为什么选择Boss直聘作为数据抓取目标招聘数据在职业规划、行业薪酬分析、人才流动趋势研究等场景中有着极高的应用价值。Boss直聘作为国内主流的招聘平台之一其岗位信息覆盖面广、更新频率高、行业跨度大是进行招聘市场分析的优质数据源。我最初做这个项目的动机很朴素——想搞清楚自己所处的技术方向在不同城市、不同规模企业中的薪资分布到底长什么样靠手动翻页统计显然不现实于是就有了这套抓取方案。这个项目的核心目标很明确给定关键词和城市批量获取岗位名称、薪资范围、公司名称、行业、融资阶段、技能标签等结构化字段最终输出为可分析的表格数据。适合有一定Python基础、了解HTTP请求基本概念、想通过一个真实项目把爬虫技术串起来的读者。如果你连requests库都没装过建议先花半小时补一下Python环境和pip安装的基础操作后面跟起来会更顺畅。1.2 技术选型为什么是requests而不是Scrapy或Playwright很多教程一上来就推Scrapy框架或者Playwright浏览器自动化但我实际做下来这个场景用requests配合接口分析是最优解。原因有三第一Boss直聘的岗位列表页数据是通过XHR异步接口返回的也就是说HTML源码里根本没有岗位信息你拿requests去请求页面URL只能拿到一个空壳。但反过来想既然数据走的是接口那我们直接请求接口就行了返回的就是干净的JSON省去了HTML解析的麻烦。第二Scrapy适合大规模分布式抓取但对于“5分钟搞定”这种轻量需求来说框架的配置成本远高于收益。你需要定义Item、写Pipeline、配Middleware光是调试框架本身就要花不少时间。第三Playwright和Selenium虽然能模拟浏览器行为绕过一些检测但资源消耗大、速度慢对于结构化的接口数据来说属于“杀鸡用牛刀”。所以最终方案是requests发请求 接口参数构造 pandas做数据清洗和存储。整个链路轻量、可控、易调试。1.3 代理IP在整个方案中的角色定位代理IP不是可选项而是这个项目的必需品。原因很直接Boss直聘对同一IP的请求频率有严格限制连续请求几十次后就会触发验证码或直接返回403。代理IP的作用就是把你的请求分散到不同IP上让每个IP看起来都像是一个正常的独立用户在浏览。这里要特别强调一点代理IP的质量直接决定项目的成败。免费代理的可用率通常不到10%而且速度极慢、随时失效用免费代理调试会让你怀疑人生。我的建议是直接上付费代理服务按量计费的那种几块钱就能跑通整个项目。后面我会详细讲代理IP的选型、配置和自动切换逻辑。2. 核心细节解析与实操要点2.1 接口分析与请求参数拆解在动手写代码之前必须先搞清楚Boss直聘的接口长什么样。打开浏览器的开发者工具切换到Network面板筛选XHR请求然后在页面上翻几页岗位列表你就能看到类似这样的请求GET https://www.zhipin.com/wapi/zpgeek/search/joblist.json ?scene1 queryPython city101010100 experience payType partTime degree industry scale stage position jobType salary multiBusinessDistrict multiSubway page1 pageSize30几个关键参数需要重点理解query搜索关键词比如“Python”“数据分析师”。注意这里需要URL编码中文关键词直接传会出问题。city城市编码。北京是101010100上海是101020100广州是101280100深圳是101280600。这个编码体系需要提前查好不能随便填。page页码从1开始递增。pageSize每页返回条数默认30实测最大可以设到30再大也不会返回更多。请求头方面有几个字段必须带上否则接口会返回异常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/web/geek/job?queryPythoncity101010100, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9, Cookie: 你的Cookie字符串, }注意Cookie是有时效性的通常几小时到一天就会过期。如果接口突然返回“当前操作存在异常”之类的提示第一件事就是去浏览器重新复制一份Cookie。2.2 代理IP的选型与配置逻辑代理IP的配置方式取决于你选的服务商。市面上主流的付费代理服务通常提供两种接入模式模式一API提取式。服务商给你一个API链接你请求这个链接它返回一个或多个IP:PORT你拿这些IP去发请求。这种模式的优点是IP池大、可控性强缺点是每次都要先请求API获取IP多了一步操作。模式二隧道代理式。服务商给你一个固定的域名和端口你所有请求都发到这个域名端口它自动帮你轮换IP。优点是配置简单不用手动管理IP池缺点是灵活性稍差不能指定特定地区的IP。我两种都用过实测下来隧道代理更适合新手因为不用写IP池管理逻辑代码量少一半。但如果你需要按城市定向抓取比如只想要北京的IP那就得用API提取式手动筛选目标城市的IP。代理配置的核心代码逻辑是这样的import requests proxies { http: http://用户名:密码代理域名:端口, https: http://用户名:密码代理域名:端口, } response requests.get(url, headersheaders, proxiesproxies, timeout10)如果你用的是API提取式就需要写一个IP池管理模块定期获取新IP、检测可用性、剔除失效IP。这部分后面在实操环节会详细展开。2.3 反爬机制的识别与应对策略Boss直聘的反爬体系不算最狠的那一档但也绝对不是随便就能过的。我踩过的坑主要有这几类第一类频率限制。同一IP在短时间内请求超过一定次数实测大约30-50次接口开始返回{code: 37, message: 当前操作存在异常}。解决办法就是控制请求间隔配合代理IP轮换。我的经验是每个请求间隔1.5-3秒随机每个IP请求15-20次后切换。第二类Cookie校验。接口会校验Cookie中的__zp_stoken__字段这个字段是JavaScript动态生成的直接复制浏览器的Cookie可以用但过期后需要重新获取。如果你发现接口返回{code: 1, message: 请先登录}那就是Cookie失效了。第三类验证码拦截。当系统判定你的行为异常时会返回一个验证码页面。遇到这种情况当前IP基本就废了需要立即切换。这也是为什么代理IP池里要保留足够的备用IP。实操心得不要试图用多线程猛冲。我试过开20个线程同时跑结果不到2分钟所有IP全被封。后来改成单线程代理轮换随机延时反而稳定跑了几万条数据。慢就是快这个道理在爬虫领域特别适用。3. 实操过程与核心环节实现3.1 环境准备与依赖安装先把基础环境搭好。Python版本建议3.8以上我用的3.10兼容性最好。需要安装的库不多pip install requests pandas openpyxlrequests发HTTP请求pandas数据清洗和导出openpyxlpandas导出Excel时的引擎依赖如果你打算用API提取式代理可能还需要redis来做IP池的去重和状态管理但这个不是必须的用Python内置的列表和字典也能凑合。3.2 代理IP池的搭建与自动切换先讲API提取式的IP池搭建。假设你的代理服务商提供了一个提取链接返回格式是每行一个IP:PORT那么IP池的核心逻辑如下import requests import time import random class ProxyPool: def __init__(self, api_url, pool_size20): self.api_url api_url self.pool_size pool_size self.proxies [] self.last_refresh 0 def fetch_proxies(self): 从API提取新IP try: resp requests.get(self.api_url, timeout10) if resp.status_code 200: ip_list resp.text.strip().split(\n) self.proxies [ip.strip() for ip in ip_list if ip.strip()] self.last_refresh time.time() print(f成功获取 {len(self.proxies)} 个代理IP) except Exception as e: print(f获取代理失败: {e}) def get_proxy(self): 随机获取一个代理 if not self.proxies or time.time() - self.last_refresh 300: self.fetch_proxies() if self.proxies: ip random.choice(self.proxies) return {http: fhttp://{ip}, https: fhttp://{ip}} return None def remove_proxy(self, proxy_dict): 移除失效的代理 ip proxy_dict[http].replace(http://, ) if ip in self.proxies: self.proxies.remove(ip)这段代码的逻辑很直白维护一个IP列表每5分钟刷新一次每次随机取一个用失效了就删掉。实际跑的时候你需要在请求失败时调用remove_proxy然后换一个IP重试。如果你用的是隧道代理那就更简单了直接把隧道地址填进proxies就行不需要管理IP池proxies { http: http://隧道用户名:密码隧道域名:端口, https: http://隧道用户名:密码隧道域名:端口, }3.3 核心抓取逻辑的完整实现下面是把所有环节串起来的完整代码。我把它拆成了几个函数每个函数负责一个独立的功能方便调试和复用。import requests import pandas as pd import time import random import urllib.parse # 配置区 KEYWORD Python CITY_CODE 101010100 # 北京 MAX_PAGES 10 COOKIE 你的Cookie字符串 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/web/geek/job, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9, Cookie: COOKIE, } # 代理池初始化 proxy_pool ProxyPool(api_url你的代理提取API链接) # 抓取单页数据 def fetch_page(page_num): 抓取指定页码的岗位数据 params { scene: 1, query: KEYWORD, city: CITY_CODE, page: page_num, pageSize: 30, } url https://www.zhipin.com/wapi/zpgeek/search/joblist.json for retry in range(3): proxy proxy_pool.get_proxy() try: resp requests.get( url, headersHEADERS, paramsparams, proxiesproxy, timeout15 ) data resp.json() if data.get(code) 0: return data.get(zpData, {}).get(jobList, []) elif data.get(code) 37: print(f第{page_num}页触发频率限制切换代理重试) if proxy: proxy_pool.remove_proxy(proxy) time.sleep(random.uniform(3, 6)) else: print(f第{page_num}页返回异常: {data.get(message)}) time.sleep(2) except Exception as e: print(f第{page_num}页请求失败: {e}) if proxy: proxy_pool.remove_proxy(proxy) time.sleep(2) return [] # 解析岗位数据 def parse_jobs(job_list): 从原始JSON中提取结构化字段 records [] for job in job_list: record { 岗位名称: job.get(jobName, ), 薪资: job.get(salaryDesc, ), 城市: job.get(cityName, ), 区域: job.get(areaDistrict, ), 商圈: job.get(businessDistrict, ), 经验要求: job.get(jobExperience, ), 学历要求: job.get(jobDegree, ), 公司名称: job.get(brandName, ), 行业: job.get(brandIndustry, ), 融资阶段: job.get(brandStageName, ), 公司规模: job.get(brandScaleName, ), 技能标签: ,.join(job.get(skills, [])), 福利标签: ,.join(job.get(welfareList, [])), HR姓名: job.get(bossName, ), HR职位: job.get(bossTitle, ), } records.append(record) return records # 主流程 def main(): all_records [] for page in range(1, MAX_PAGES 1): print(f正在抓取第 {page} 页...) job_list fetch_page(page) if not job_list: print(f第 {page} 页无数据跳过) continue records parse_jobs(job_list) all_records.extend(records) print(f第 {page} 页获取 {len(records)} 条数据) time.sleep(random.uniform(1.5, 3.0)) if all_records: df pd.DataFrame(all_records) df.to_excel(boss_jobs.xlsx, indexFalse) print(f共抓取 {len(df)} 条数据已保存到 boss_jobs.xlsx) else: print(未抓取到任何数据请检查Cookie和代理配置) if __name__ __main__: main()这段代码有几个设计细节值得说明重试机制每个页面最多重试3次每次重试都会换一个代理IP。如果3次都失败就跳过这一页继续下一页避免卡死。频率控制每页之间随机延时1.5到3秒触发限制时延时3到6秒。这个延时范围是我反复测试后确定的再快就容易触发风控。数据解析接口返回的JSON字段名和最终表格的列名做了映射方便后续分析。技能标签和福利标签是数组用逗号拼接成字符串。3.4 数据存储与导出数据存储这块我选了Excel作为输出格式因为大部分做招聘分析的人最终都要在Excel里做透视表和图表。pandas的to_excel一行搞定但有几个细节要注意# 处理薪资字段拆分为最低和最高 def parse_salary(salary_str): 将15-25K·14薪解析为(15, 25, 14) import re match re.match(r(\d)-(\d)K(?:·(\d)薪)?, salary_str) if match: low int(match.group(1)) high int(match.group(2)) months int(match.group(3)) if match.group(3) else 12 return low, high, months return None, None, None df[[薪资下限, 薪资上限, 薪资月数]] df[薪资].apply( lambda x: pd.Series(parse_salary(x)) ) df[年薪下限] df[薪资下限] * df[薪资月数] df[年薪上限] df[薪资上限] * df[薪资月数]这样处理后你就可以直接用df.groupby(城市)[年薪上限].mean()来算各城市的平均薪资上限了。实测下来北京Python岗位的年薪上限中位数大概在40-50万之间当然这个数据会随市场波动仅供参考。4. 常见问题与排查技巧实录4.1 接口返回异常码速查表在调试过程中你会遇到各种各样的返回码。我把踩过的坑整理成了一张速查表返回码提示信息原因解决办法0成功正常返回无需处理1请先登录Cookie失效重新从浏览器复制Cookie37当前操作存在异常IP被限频切换代理IP增加延时403ForbiddenIP被封禁更换代理IP检查请求头404Not Found接口地址变更重新抓包获取最新接口500服务器错误服务端问题等待几分钟后重试特别注意返回码37是最常见的几乎每个跑这个项目的人都会遇到。不要慌切换代理增加延时就能解决。如果你用的是隧道代理直接等几秒再请求就行隧道会自动换IP。4.2 Cookie获取与维护的实操技巧Cookie的获取方式很简单登录Boss直聘后打开开发者工具在Network面板随便找一个请求复制Request Headers里的Cookie字段。但有几个坑要注意坑一Cookie不完整。有些教程只让你复制__zp_stoken__但实测下来完整的Cookie字符串才能稳定通过校验。建议全选复制不要挑挑拣拣。坑二Cookie过期。Boss直聘的Cookie有效期大概在4-8小时具体取决于你的账号活跃度。如果你要跑长时间任务建议写一个Cookie自动刷新机制或者每隔几小时手动更新一次。坑三多账号Cookie轮换。如果你有多个账号可以准备多个Cookie在请求时随机选一个。这样能进一步降低单账号被风控的概率。但注意不要用新注册的账号新号的风控阈值更低。4.3 代理IP失效的快速排查方法代理IP失效是家常便饭关键是要能快速定位问题。我的排查流程是这样的第一步先用requests直接请求一个IP查询接口确认代理是否生效def check_proxy(proxy): 检测代理是否可用 try: resp requests.get( http://httpbin.org/ip, proxiesproxy, timeout8 ) if resp.status_code 200: return resp.json().get(origin, ) except: pass return None第二步如果代理生效但Boss直聘接口返回403说明这个IP被目标网站封了直接剔除。第三步如果代理请求超时说明IP本身不可用也剔除。这套检测逻辑可以集成到IP池的get_proxy方法里每次取IP时先检测一下可用的才返回。虽然会多花一点时间但能大幅降低请求失败率。4.4 抓取效率与稳定性的平衡经验最后分享几个我在反复调试中总结的经验关于并发单线程代理轮换的稳定性远高于多线程。如果你非要上并发建议用asyncioaiohttp做异步而不是开多线程。异步的资源消耗更低而且更容易控制请求频率。关于延时不要设固定延时用随机延时。固定延时容易被识别出规律随机延时更像真人操作。我的配置是random.uniform(1.5, 3.0)你可以根据实际情况调整。关于数据量如果你只需要几百条数据做分析跑10页就够了。如果要跑几千条建议分时段跑比如早上跑一批、下午跑一批避免短时间内请求量过大。关于法律边界抓取公开的招聘信息用于个人分析和绕过技术保护措施批量获取敏感数据是两码事。我的建议是只抓取列表页的公开字段不要尝试获取HR的联系方式等隐私信息控制请求频率不影响目标网站的正常服务。这个边界心里要有数。4.5 数据清洗中的常见问题抓下来的原始数据往往有不少脏数据比如薪资字段格式不统一有的写“15-25K”有的写“15-25K·14薪”还有的写“面议”经验要求有的写“3-5年”有的写“经验不限”。这些都需要在分析前做清洗。我的处理策略是能解析的解析成数值不能解析的保留原始字符串并标记为“待确认”。比如薪资字段用正则提取数字部分提取不到的就归入“面议”类别单独统计。这样既不会丢失信息又能保证数值分析的准确性。另外技能标签字段是数组拼接的分析时需要先拆分再统计词频。比如df[技能标签].str.split(,).explode()就能把一行拆成多行然后value_counts()就能看到哪些技能出现频率最高。我跑出来的结果里Python岗位出现最多的技能标签是“后端开发”“Django”“MySQL”“Redis”这个结果和行业认知基本吻合。
RELATED

相关推荐

伺服电机通信协议选型指南:Modbus、CANopen与EtherCAT对比

伺服电机通信协议选型指南:Modbus、CANopen与EtherCAT对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/21 2:46:57
视频会议系统操作手册:从结构设计到doc格式落地全攻略

视频会议系统操作手册:从结构设计到doc格式落地全攻略

简介:《视频会议系统操作手册》是一份面向企业、教育机构、政府机关等组织的视频会议管理员及日常使用者的实用文档,旨在帮助用户系统掌握视频会议前、中、后的操作要点,减少因配置不当或操作失误导致的网络丢包、音画不同步等问题。资源包内…

📅 2026/9/21 2:41:56
Raycast 2.0 深度体验:AI 启动器重构与高效工作流配置指南

Raycast 2.0 深度体验:AI 启动器重构与高效工作流配置指南

1. 从启动器到指令中心:Raycast 2.0 到底改了什么用了三年 Raycast,从最早那个只能搜应用、算汇率的小工具,到如今把 AI、剪贴板历史、窗口管理、脚本命令全塞进一个输入框里,我对它的感情挺复杂。一方面它确实把我 Mac 上原本要装…

📅 2026/9/21 2:41:56
MORE NEWS

更多资讯

📰

rust-analyzer 排障 FAQ 实战指南:sysroot 损坏与 Cargo 构建锁竞争

rust-analyzer 排障 FAQ 实战指南:sysroot 损坏与 Cargo 构建锁竞争 【免费下载链接】rust-analyzer A Rust compiler front-end for IDEs 项目地址: https://gitcode.com/gh_mirrors/ru/rust-analyzer 本篇指南聚焦 rust-analyzer 官方 Troubleshooting FAQ…

📰

RxJS 4 `partition` 操作符深度解析:按谓词将一条 Observable 流一分为二

后端 【免费下载链接】RxJS The Reactive Extensions for JavaScript 项目地址: https://gitcode.com/gh_mirrors/rxj/RxJS 点击查看 免费下载 本文基于 RxJS v4(The Reactive Extensions for JavaScript)官方 API 文档与仓库源码&#xff0…

📰

sentence-transformers CrossEncoder 模型卡模板全解析:为 Reranker 自动生成专业 README 的完整机制

sentence-transformers CrossEncoder 模型卡模板全解析:为 Reranker 自动生成专业 README 的完整机制 【免费下载链接】sentence-transformers State-of-the-Art Embeddings, Retrieval, and Reranking 项目地址: https://gitcode.com/gh_mirrors/se/sentence-tra…

📰

BrowserSkill错误码速查手册:cdp_failed、timeout、cancelled常见错误一次看懂

BrowserSkill错误码速查手册:cdp_failed、timeout、cancelled常见错误一次看懂 【免费下载链接】BrowserSkill Let AI agents use your real, logged-in browser without interrupting your work. CLI extension for browser automation across any shell-capable …

📰

为什么你的页面加载这么慢?高性能图片懒加载库lazysizes完全入门指南

为什么你的页面加载这么慢?高性能图片懒加载库lazysizes完全入门指南 【免费下载链接】lazysizes High performance and SEO friendly lazy loader for images (responsive and normal), iframes and more, that detects any visibility changes triggered through …

📰

STM32智能婴儿床毕设实战:从方案设计到答辩完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬