Python爬虫实战:逆向分析猎聘网API接口高效获取招聘数据 1. 项目概述为什么选择猎聘网作为数据源最近在帮一个做人力资源分析的朋友做点数据支持他需要了解特定岗位在市场上的薪资分布、技能要求和地域差异。市面上招聘平台不少但综合来看猎聘网在定位中高端职位、岗位描述的专业性以及薪资信息的相对透明性上有它独特的优势。对于数据分析师、算法工程师、产品总监这类岗位猎聘上的信息质量通常比一些综合性平台要高样本价值也更大。所以这次的目标就很明确了写一个Python爬虫从猎聘网上抓取结构化的招聘信息为后续的分析工作打好数据基础。这活儿听起来简单不就是发请求、解析网页、存数据嘛。但真上手了你会发现现代招聘网站的反爬机制已经相当成熟不再是十年前那种随便写几行requests加BeautifulSoup就能畅通无阻的时代了。猎聘网作为主流平台在数据保护和反爬虫上做了不少功课比如动态加载、请求参数加密、频繁访问限制等等。所以这个项目不仅仅是“爬取数据”更是一次对动态网页数据抓取、反爬策略应对以及数据清洗规整的综合性实战。无论你是想自己做市场调研还是学习Python爬虫的进阶技巧这个案例都有不少值得深挖的地方。2. 核心思路与技术选型绕过障碍高效获取直接去猎聘网搜索一个岗位比如“Python开发”你会发现页面上的内容并不是一次性加载完毕的。滚动页面时新的招聘信息会不断出现这就是典型的AJAX动态加载。这意味着你直接去请求搜索结果的URL拿到的HTML源码里很可能没有我们想要的招聘列表数据数据是通过后续的JavaScript请求异步获取的。因此我们的核心思路不能停留在解析静态HTML上而需要模拟浏览器行为或者直接找到数据接口。这里主要有两条技术路径使用Selenium等浏览器自动化工具这种方式模拟真实用户操作浏览器可以完整地渲染出JavaScript动态生成的内容。优点是简单直观能看到什么就能抓到什么绕过前端加密逻辑。缺点是速度慢、资源消耗大不适合大规模、高频次的抓取且容易被检测到自动化行为。通过浏览器开发者工具分析网络请求找到真正的数据API接口这是更高效、更专业的方法。我们打开浏览器的“开发者工具”F12切换到“Network”网络标签页然后在猎聘网进行搜索、翻页操作。观察期间发起的XHRFetch请求找到那个返回了JSON格式招聘列表数据的请求。分析这个请求的URL、请求头Headers和请求参数Query String Parameters 或 Payload然后用Python的requests库去模拟这个请求。对于这个项目我强烈推荐第二条路。理由很充分首先效率天差地别。一个requests.get()可能几十毫秒就拿到数据而Selenium启动浏览器、加载页面可能需要好几秒甚至十几秒。其次稳定性更高。直接调用后端API拿到的就是结构化的JSON数据解析起来比从复杂的HTML里抽取要稳定和干净得多。最后资源消耗小更适合在服务器或后台长期运行。所以我们的技术栈就明确了请求库requests用于发送HTTP请求。数据解析目标接口返回的是JSON直接用Python内置的json库解析即可无需HTML解析器。参数构造与反爬需要仔细分析并还原API请求的必要参数特别是那些看起来像加密或经过编码的参数。同时需要设置合理的请求头如User-Agent,Referer等来模拟浏览器。数据存储初步存储为pandas的DataFrame然后可以方便地导出为CSV或Excel文件或者存入数据库。节奏控制必须加入延时time.sleep来避免请求过快导致IP被封。注意任何爬虫行为都必须遵守网站的robots.txt协议并尊重网站的服务压力。本案例仅用于技术学习和个人研究请务必控制抓取频率避免对目标网站造成负担。大规模商业用途的数据抓取可能涉及法律风险请谨慎评估。3. 关键步骤拆解与实战从分析到落地3.1 第一步定位核心数据接口这是整个项目最关键也最具技术含量的一步。打开猎聘网进入搜索页面。以搜索“上海 Python开发”为例。打开开发者工具按F12切换到Network标签页。记得勾选上“Preserve log”保留日志防止页面跳转时请求记录被清空。执行搜索操作在搜索框输入条件点击搜索。筛选请求在Network面板中点击“Fetch/XHR”过滤器这样可以快速过滤出可能的数据接口请求。寻找目标请求在请求列表中寻找名称或响应内容看起来像招聘列表的请求。通常这类请求的URL会包含search、list、position等关键词。点击一个可疑的请求查看其“Preview”或“Response”标签页。如果你看到了结构化的JSON数据里面包含positionId、companyName、salary等字段恭喜你找到了分析请求详情点击找到的请求我们需要重点关注三个部分Headers特别是User-Agent用户代理和Referer来源页。User-Agent用来告诉服务器我们是什么浏览器Referer告诉服务器我们是从哪个页面跳转过来的。这两个是反爬虫最基本的验证必须带上。Payload或Query String Parameters如果是POST请求参数在Payload里通常是form-data或json如果是GET请求参数在URL的问号后面。这里包含了我们的搜索条件比如关键词、城市、页码、每页数量等。特别注意那些长串的、无规律的参数例如xxxxxx_t等它们很可能是时间戳、签名或加密参数需要分析其生成规律。经过分析猎聘网以某一时期为例接口可能变动的招聘列表数据通常通过一个POST请求到类似https://www.liepin.com/zhaopin/的接口获取请求参数是form-data格式包含了key关键词、dqs城市代码、currentPage当前页等。3.2 第二步用Python模拟请求并解析数据找到接口后我们就可以用requests库来模拟这个请求了。首先把从浏览器里复制出来的Headers信息整理出来尤其是User-Agent。import requests import json import time import pandas as pd from typing import Dict, List, Optional def fetch_one_page(keyword: str, city: str, page: int) - Optional[Dict]: 抓取猎聘网单页招聘数据 :param keyword: 搜索关键词如 ‘Python开发’ :param city: 城市名用于映射城市代码如 ‘上海’ :param page: 页码从0开始 :return: 包含招聘信息的字典或None如果请求失败 # 基础URL (示例实际需根据分析结果调整) url https://www.liepin.com/zhaopin/ # 请求头从浏览器复制并精简关键字段 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36, Referer: https://www.liepin.com/, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, # 可能还需要其他header如Cookie需根据实际情况添加 } # 请求参数 (Payload) - 这是核心需要根据实际接口分析 # 城市代码需要映射例如上海可能是‘020’北京是‘010’ city_code_map {上海: 020, 北京: 010, 深圳: 050090} dqs city_code_map.get(city, ) data { key: keyword, dqs: dqs, currentPage: page, # 以下参数名称和值需要根据实际抓包分析确定 pageSize: 40, # 每页数量 siTag: xxx, # 可能的标签参数 ckId: xxx, # 可能的ID参数 # ... 可能还有其他加密或校验参数 } try: # 发送POST请求 response requests.post(url, headersheaders, datadata, timeout10) response.raise_for_status() # 检查请求是否成功 # 解析JSON响应 result_json response.json() # 通常数据在 result_json 的某个键下如 ‘data’ - ‘list’ if result_json.get(code) 0 and data in result_json: return result_json[data] else: print(f第{page}页请求成功但数据格式异常或为空。) return None except requests.exceptions.RequestException as e: print(f请求第{page}页时发生错误: {e}) return None except json.JSONDecodeError as e: print(f解析第{page}页JSON响应时发生错误: {e}) return None实操心得这里的data字典里的参数名和值如siTag,ckId不是固定的它们很可能由前端JavaScript动态生成具有一定的时效性或唯一性。直接复制一次抓包的结果可能只能用于当次请求。要稳定爬取可能需要进一步分析这些参数的生成算法或者采用更复杂的方式如使用execjs执行JS代码生成参数。一个更务实的折中方案是在单次会话中先通过一个初始请求获取这些动态参数然后在后续翻页请求中复用或微调它们。这比破解完整算法要简单。3.3 第三步数据清洗与结构化存储接口返回的JSON数据虽然结构化但字段可能很多很杂我们需要从中提取出对我们分析有用的核心信息。def parse_job_list(data: Dict) - List[Dict]: 从接口返回的数据中解析出招聘信息列表 :param data: fetch_one_page 返回的 data 字段 :return: 清洗后的招聘信息字典列表 job_list [] # 假设招聘列表在 data[list] 中 raw_list data.get(list, []) for job in raw_list: # 提取核心字段注意字段名需根据实际响应调整 job_info { 职位ID: job.get(positionId), 职位名称: job.get(title), 公司名称: job.get(companyName), 薪资范围: job.get(salary), # 通常是“20-30k·14薪”这种格式 工作地点: job.get(cityName), 学历要求: job.get(education), 工作经验: job.get(experienceName), 职位福利: , .join(job.get(welfareList, [])), # 福利可能是列表 公司规模: job.get(companySize), 公司类型: job.get(companyType), 发布日期: job.get(createTime), # 可能是时间戳或字符串 职位详情页链接: fhttps://www.liepin.com/job/{job.get(positionId)}.shtml if job.get(positionId) else , } # 对薪资进行初步拆分便于后续分析 salary_str job_info[薪资范围] if salary_str: # 简单处理例如“20-30k·14薪” job_info[薪资下限(k)], job_info[薪资上限(k)], job_info[薪资月数] parse_salary(salary_str) job_list.append(job_info) return job_list def parse_salary(salary_str: str) - (float, float, int): 一个简单的薪资字符串解析函数示例实际需要更健壮的逻辑 :param salary_str: 如 “20-30k·14薪” :return: (下限, 上限, 月数) # 这是一个简化的示例真实情况可能更复杂如“面议”、“20k以上”、“20-30k” try: import re # 匹配 “数字-数字k” 的模式 match re.search(r(\d)[kK]?-?(\d)?[kK], salary_str) if match: low float(match.group(1)) high float(match.group(2)) if match.group(2) else low else: low high None # 匹配 “·数字薪” 的模式 match_months re.search(r·(\d)薪, salary_str) months int(match_months.group(1)) if match_months else 12 # 默认12薪 return low, high, months except: return None, None, 12有了解析函数我们就可以组织主循环进行多页抓取并将数据存入DataFrame和CSV。def main(): keyword Python开发 city 上海 total_pages 5 # 计划抓取的页数请根据实际情况调整切勿过度抓取 all_jobs [] for page in range(total_pages): print(f正在抓取第 {page1} 页...) data fetch_one_page(keyword, city, page) if data: jobs parse_job_list(data) all_jobs.extend(jobs) print(f第 {page1} 页抓取到 {len(jobs)} 条数据。) else: print(f第 {page1} 页抓取失败停止。) break # 非常重要的延时避免请求过快 time.sleep(2 random.random()) # 随机延时2-3秒 # 转换为DataFrame if all_jobs: df pd.DataFrame(all_jobs) print(f总共抓取到 {len(df)} 条招聘信息。) # 数据清洗示例 # 1. 去重根据职位ID df.drop_duplicates(subset[职位ID], inplaceTrue) # 2. 处理空值 df[薪资下限(k)].fillna(0, inplaceTrue) df[薪资上限(k)].fillna(0, inplaceTrue) # 保存到CSV filename fliepin_{keyword}_{city}_{time.strftime(%Y%m%d)}.csv df.to_csv(filename, indexFalse, encodingutf-8-sig) # utf-8-sig解决Excel打开中文乱码 print(f数据已保存到文件: {filename}) # 简单预览 print(df[[职位名称, 公司名称, 薪资范围, 工作地点]].head()) else: print(未抓取到任何数据。) if __name__ __main__: main()4. 进阶策略与深度优化4.1 应对动态加密参数如前所述siTag、ckId这类参数是最大的挑战。如果直接使用固定值可能第一页成功第二页就失败了。解决方案有几种会话维持Session使用requests.Session()对象。首次访问猎聘网主页获取一个初始的Cookie。然后在这个会话Session内进行搜索和翻页许多校验参数会通过Cookie或会话上下文传递无需我们手动生成。session requests.Session() # 首先访问一次主页获取必要的Cookie session.get(https://www.liepin.com/, headersheaders) # 后续的 fetch_one_page 使用 session.post 而不是 requests.post逆向分析JS如果参数生成逻辑在网页的JS文件里可以使用execjs或PyExecJS库来执行JavaScript代码计算出正确的参数值。这需要一定的前端逆向工程能力。使用无头浏览器获取参数折中方案。用Selenium打开页面执行一次搜索然后从页面或网络请求中提取出这些动态参数再用requests去进行高效的翻页抓取。这样既绕过了参数生成又保持了requests的高效率。4.2 提升抓取健壮性代理IP池如果抓取量很大或频率较高单一IP很容易被封锁。需要集成代理IP服务在请求失败或达到一定次数后自动切换IP。异常重试机制网络请求不稳定需要加入重试逻辑。可以使用tenacity或retrying库或者自己写try-except和循环。随机化请求间隔固定的time.sleep(2)仍然有规律可循。最好使用随机延时模拟人类操作的不确定性。import random delay random.uniform(1.5, 4.0) # 随机延时1.5到4秒 time.sleep(delay)完善请求头除了User-Agent和Referer有时Cookie、Accept-Encoding、Connection等字段也会被检查。尽量从浏览器复制完整的请求头。可以准备多个User-Agent轮流使用。4.3 数据解析的精细化我们之前只解析了列表页的基础信息。列表页的“职位描述”通常是缩略的。要获取完整的职位描述JD需要进一步抓取职位详情页。从列表页获取每个职位的positionId和详情页链接。构造详情页URL通常是固定的格式如https://www.liepin.com/job/{positionId}.shtml。请求详情页解析完整的HTML从中提取详细的职位描述、任职要求、团队介绍等信息。这里可能又需要用到BeautifulSoup或lxml来解析HTML。注意详情页的抓取频率要更低间隔要更大因为对网站造成的负载更重。5. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到下面这些问题。这里把我踩过的坑和解决方法记录下来。问题现象可能原因排查与解决思路返回空数据或{code: 403}1. 请求头不完整或错误。2. 关键动态参数如ckId失效或缺失。3. IP被限制或封禁。1.核对Headers确保User-Agent、Referer、Cookie如果需要与浏览器一致。使用Session对象保持会话。2.检查参数确认data中的参数名和值是否与当前抓包结果一致。尝试从首次请求的响应中获取这些参数。3.降低频率大幅增加请求间隔时间如5-10秒。4.更换IP使用代理IP。只能抓到第一页第二页开始失败翻页参数如currentPage或关联的动态参数未正确更新。1.分析翻页请求在浏览器中手动点击第二页对比Network中两个请求的Payload差异找出变化的参数。2.使用Session确保翻页请求在同一个requests.Session()内进行以维持会话状态。3.参数溯源变化参数可能来自第一页响应的某个字段需要提取并用于第二页的请求。返回的数据是HTML而不是JSON请求被重定向到了登录页或验证页说明爬虫行为被识别。1.检查请求头User-Agent是否像真实浏览器Accept是否包含application/json2.检查Referer翻页时Referer应该是上一页的URL。3.验证Cookie可能需要先访问首页获取有效的会话Cookie。4.查看响应内容将返回的HTML保存下来打开看看是不是验证码页面。如果是说明需要进一步模拟人工行为或使用打码平台这通常意味着爬取难度极大应考虑放弃或寻找其他数据源。JSONDecodeError服务器返回的不是合法的JSON可能是错误页面、验证页面或空响应。1.打印响应状态码和文本前500字符print(response.status_code, response.text[:500])先看看到底返回了什么。2.异常处理在json.loads()外做好try-except并记录下错误的响应内容便于分析。抓取速度很慢1. 单线程顺序请求。2. 延时设置过长。1.考虑并发对于大量详情页抓取可以使用concurrent.futures.ThreadPoolExecutor进行有限的并发如3-5个线程但务必注意总请求速率避免被封。2.优化延时在遵守道德和robots.txt的前提下找到不被封的最低延时阈值。列表页间隔可稍长如3秒详情页间隔需更长如5-8秒。最后的叮嘱爬虫技术是一把双刃剑。在动手之前务必仔细阅读目标网站的robots.txt文件通常在网站根目录如https://www.liepin.com/robots.txt尊重其中关于爬虫行为的约定。始终将抓取频率控制在极低的水平最好在非高峰时段进行。这个项目的核心价值在于学习如何分析动态网站、处理反爬机制和清洗数据而不是无限制地获取数据。把这些技术用在正当的学习和研究目的上才是长久之道。