Python爬虫实战:逆向分析猎聘网API接口高效获取招聘数据
2026/8/31 15:28:07 网站建设 项目流程

1. 项目概述:为什么选择猎聘网作为数据源

最近在帮一个做人力资源分析的朋友做点数据支持,他需要了解特定岗位在市场上的薪资分布、技能要求和地域差异。市面上招聘平台不少,但综合来看,猎聘网在定位中高端职位、岗位描述的专业性以及薪资信息的相对透明性上,有它独特的优势。对于数据分析师、算法工程师、产品总监这类岗位,猎聘上的信息质量通常比一些综合性平台要高,样本价值也更大。所以,这次的目标就很明确了:写一个Python爬虫,从猎聘网上抓取结构化的招聘信息,为后续的分析工作打好数据基础。

这活儿听起来简单,不就是发请求、解析网页、存数据嘛。但真上手了你会发现,现代招聘网站的反爬机制已经相当成熟,不再是十年前那种随便写几行requestsBeautifulSoup就能畅通无阻的时代了。猎聘网作为主流平台,在数据保护和反爬虫上做了不少功课,比如动态加载、请求参数加密、频繁访问限制等等。所以,这个项目不仅仅是“爬取数据”,更是一次对动态网页数据抓取、反爬策略应对以及数据清洗规整的综合性实战。无论你是想自己做市场调研,还是学习Python爬虫的进阶技巧,这个案例都有不少值得深挖的地方。

2. 核心思路与技术选型:绕过障碍,高效获取

直接去猎聘网搜索一个岗位,比如“Python开发”,你会发现页面上的内容并不是一次性加载完毕的。滚动页面时,新的招聘信息会不断出现,这就是典型的AJAX动态加载。这意味着,你直接去请求搜索结果的URL,拿到的HTML源码里很可能没有我们想要的招聘列表数据,数据是通过后续的JavaScript请求异步获取的。

因此,我们的核心思路不能停留在解析静态HTML上,而需要模拟浏览器行为,或者直接找到数据接口。这里主要有两条技术路径:

  1. 使用Selenium等浏览器自动化工具:这种方式模拟真实用户操作浏览器,可以完整地渲染出JavaScript动态生成的内容。优点是简单直观,能看到什么就能抓到什么,绕过前端加密逻辑。缺点是速度慢、资源消耗大,不适合大规模、高频次的抓取,且容易被检测到自动化行为。
  2. 通过浏览器开发者工具分析网络请求,找到真正的数据API接口:这是更高效、更专业的方法。我们打开浏览器的“开发者工具”(F12),切换到“Network”(网络)标签页,然后在猎聘网进行搜索、翻页操作。观察期间发起的XHR(Fetch)请求,找到那个返回了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等)来模拟浏览器。
  • 数据存储:初步存储为pandasDataFrame,然后可以方便地导出为CSVExcel文件,或者存入数据库。
  • 节奏控制:必须加入延时(time.sleep)来避免请求过快导致IP被封。

注意:任何爬虫行为都必须遵守网站的robots.txt协议,并尊重网站的服务压力。本案例仅用于技术学习和个人研究,请务必控制抓取频率,避免对目标网站造成负担。大规模商业用途的数据抓取可能涉及法律风险,请谨慎评估。

3. 关键步骤拆解与实战:从分析到落地

3.1 第一步:定位核心数据接口

这是整个项目最关键也最具技术含量的一步。打开猎聘网,进入搜索页面。以搜索“上海 Python开发”为例。

  1. 打开开发者工具:按F12,切换到Network标签页。记得勾选上“Preserve log”(保留日志),防止页面跳转时请求记录被清空。
  2. 执行搜索操作:在搜索框输入条件,点击搜索。
  3. 筛选请求:在Network面板中,点击“Fetch/XHR”过滤器,这样可以快速过滤出可能的数据接口请求。
  4. 寻找目标请求:在请求列表中,寻找名称或响应内容看起来像招聘列表的请求。通常,这类请求的URL会包含searchlistposition等关键词。点击一个可疑的请求,查看其“Preview”“Response”标签页。如果你看到了结构化的JSON数据,里面包含positionIdcompanyNamesalary等字段,恭喜你,找到了!
  5. 分析请求详情:点击找到的请求,我们需要重点关注三个部分:
    • Headers:特别是User-Agent(用户代理)和Referer(来源页)。User-Agent用来告诉服务器我们是什么浏览器,Referer告诉服务器我们是从哪个页面跳转过来的。这两个是反爬虫最基本的验证,必须带上。
    • PayloadQuery String Parameters:如果是POST请求,参数在Payload里(通常是form-datajson);如果是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;q=0.9,en;q=0.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, headers=headers, data=data, timeout=10) 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'), # 可能是时间戳或字符串 '职位详情页链接': f"https://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

有了解析函数,我们就可以组织主循环,进行多页抓取,并将数据存入DataFrameCSV

def main(): keyword = "Python开发" city = "上海" total_pages = 5 # 计划抓取的页数,请根据实际情况调整,切勿过度抓取 all_jobs = [] for page in range(total_pages): print(f"正在抓取第 {page+1} 页...") data = fetch_one_page(keyword, city, page) if data: jobs = parse_job_list(data) all_jobs.extend(jobs) print(f"第 {page+1} 页抓取到 {len(jobs)} 条数据。") else: print(f"第 {page+1} 页抓取失败,停止。") 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'], inplace=True) # 2. 处理空值 df['薪资下限(k)'].fillna(0, inplace=True) df['薪资上限(k)'].fillna(0, inplace=True) # 保存到CSV filename = f"liepin_{keyword}_{city}_{time.strftime('%Y%m%d')}.csv" df.to_csv(filename, index=False, encoding='utf-8-sig') # utf-8-sig解决Excel打开中文乱码 print(f"数据已保存到文件: {filename}") # 简单预览 print(df[['职位名称', '公司名称', '薪资范围', '工作地点']].head()) else: print("未抓取到任何数据。") if __name__ == '__main__': main()

4. 进阶策略与深度优化

4.1 应对动态加密参数

如前所述,siTagckId这类参数是最大的挑战。如果直接使用固定值,可能第一页成功,第二页就失败了。解决方案有几种:

  • 会话维持(Session):使用requests.Session()对象。首次访问猎聘网主页,获取一个初始的Cookie。然后在这个会话(Session)内进行搜索和翻页,许多校验参数会通过Cookie或会话上下文传递,无需我们手动生成。
    session = requests.Session() # 首先访问一次主页,获取必要的Cookie session.get('https://www.liepin.com/', headers=headers) # 后续的 fetch_one_page 使用 session.post 而不是 requests.post
  • 逆向分析JS:如果参数生成逻辑在网页的JS文件里,可以使用execjsPyExecJS库来执行JavaScript代码,计算出正确的参数值。这需要一定的前端逆向工程能力。
  • 使用无头浏览器获取参数:折中方案。用Selenium打开页面,执行一次搜索,然后从页面或网络请求中提取出这些动态参数,再用requests去进行高效的翻页抓取。这样既绕过了参数生成,又保持了requests的高效率。

4.2 提升抓取健壮性

  • 代理IP池:如果抓取量很大或频率较高,单一IP很容易被封锁。需要集成代理IP服务,在请求失败或达到一定次数后自动切换IP。
  • 异常重试机制:网络请求不稳定,需要加入重试逻辑。可以使用tenacityretrying库,或者自己写try-except和循环。
  • 随机化请求间隔:固定的time.sleep(2)仍然有规律可循。最好使用随机延时,模拟人类操作的不确定性。
    import random delay = random.uniform(1.5, 4.0) # 随机延时1.5到4秒 time.sleep(delay)
  • 完善请求头:除了User-AgentReferer,有时CookieAccept-EncodingConnection等字段也会被检查。尽量从浏览器复制完整的请求头。可以准备多个User-Agent轮流使用。

4.3 数据解析的精细化

我们之前只解析了列表页的基础信息。列表页的“职位描述”通常是缩略的。要获取完整的职位描述(JD),需要进一步抓取职位详情页

  1. 从列表页获取每个职位的positionId和详情页链接。
  2. 构造详情页URL(通常是固定的格式,如https://www.liepin.com/job/{positionId}.shtml)。
  3. 请求详情页,解析完整的HTML,从中提取详细的职位描述、任职要求、团队介绍等信息。这里可能又需要用到BeautifulSouplxml来解析HTML。
  4. 注意:详情页的抓取频率要更低,间隔要更大,因为对网站造成的负载更重。

5. 常见问题与排查技巧实录

在实际操作中,你几乎一定会遇到下面这些问题。这里把我踩过的坑和解决方法记录下来。

问题现象可能原因排查与解决思路
返回空数据或{'code': 403}1. 请求头不完整或错误。
2. 关键动态参数(如ckId)失效或缺失。
3. IP被限制或封禁。
1.核对Headers:确保User-AgentRefererCookie(如果需要)与浏览器一致。使用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/json
2.检查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),尊重其中关于爬虫行为的约定。始终将抓取频率控制在极低的水平,最好在非高峰时段进行。这个项目的核心价值在于学习如何分析动态网站、处理反爬机制和清洗数据,而不是无限制地获取数据。把这些技术用在正当的学习和研究目的上,才是长久之道。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询