一、项目背景与目标
- 如何使用浏览器开发者工具进行网络抓包
- 如何精准定位数据接口
- 如何识别和分析加密参数
- 如何使用 EasySpider 工具集 快速转换请求代码
- 如何定位 JavaScript 中的加密函数
- 如何使用 在线加解密工具 验证加密算法
- 如何将 JS 加密逻辑迁移到 Python 实现
在当今数据驱动的时代,招聘数据的采集和分析对于企业招聘策略制定、人才市场趋势研究等方面具有重要意义。51job 作为国内知名的招聘平台,拥有海量的招聘信息,是研究就业市场的重要数据源。
然而,51job 网站为了保护数据安全,采用了 参数签名 的反爬策略,其中 sign 参数是我们需要逆向分析的关键。本文将完整记录从抓包到逆向的整个过程,帮助你掌握爬虫逆向的核心技能。
二、抓包分析
2.1 为什么选择翻页方式抓包?
在采集招聘栏目数据时,采用翻页的方式可以更精准地定位目标接口:
- 减少干扰:避免页面刷新时加载的大量无关静态资源
- 精准定位:只触发与数据加载相关的接口
- 提高效率:快速找到目标数据接口
2.2 抓包步骤
- 打开 Chrome 浏览器,访问 51job 网站
- 按 F12 打开开发者工具,切换到 Network 标签
- 在招聘页面中输入关键词(如 "python")进行搜索
- 点击「下一页」按钮,观察网络请求列表
- 在过滤器中选择 Fetch/XHR,筛选出数据接口
search-pc,快速筛选目标请求。
三、接口分析
通过抓包,我们找到了目标接口:
aHR0cHM6Ly93ZS41MWpvYi5jb20vYXBpL2pvYi9zZWFyY2gtcGM/YXBpX2tleT01MWpvYsOXdGFtcD0xNzc1OTg1NjA3JmtleXdvcmQ9cHl0aG9uJnNlYXJjaFR5cGU9MiZmdW5jdGlvbj0maW5kdXN0cnk9JmpvYkFyZWE9MDkwMDAwJmpvYkFyZWEyPSZsYW5kbWFyaz0mbWV0cm89JnNhbGFyeT0md29ya1llYXI9wrByZWU9JmNvbXBhbnlUeXBlPSZjb21wYW55U2l6ZT0mam9iVHlwZT0maXNzdWVEYXRlPSZzb3J0VHlwZT0wJnBhZ2VOdW09MiZyZXF1ZXN0SWQ9MGY2OGZhZDNhMmRmOWIxZTNiMDkzZWZmY2JhOGUzYjAmcGFnZVNpemU9MjAmc291cmNlPTEmYWNjb3VudElkPSZwYWdlQ29kZT1zb3UlN0Nzb3UlN0Nzb3VsYiZzY2VuZT03
这是一个 GET 请求,包含了多个查询参数。通过分析,我们可以看到:
- api_key:固定为 "51job"
- timestamp:时间戳参数
- keyword:搜索关键词
- pageNum:页码参数
- pageSize:每页条数
- 其他参数:筛选条件(如工作地点、薪资范围等)
四、参数分析
查看请求标头,我们发现了两个可疑的参数:
sign:c65e05f11a9c4aa8211f8f7aede50472cbfcfff79f10ece1d8bf625f9c4f9c40
uuid:17759855976157400568
- uuid:通用唯一识别码,通常用于标识用户或会话
- sign:签名参数,长度为 64 位,由字母和数字组成,疑似使用了 SHA256 加密
在反爬策略中,sign 参数通常用于验证请求的合法性,防止恶意请求。因此,我们需要重点分析这个参数的生成逻辑。
五、参数验证
5.1 使用 curl 转 Python 工具
找到目标接口后,鼠标右键点击该请求,选择 Copy → Copy as cURL (bash),将完整的请求命令复制到剪贴板。
接下来,打开本网站的 首页,切换到「curl转python」工具,将复制的内容直接粘贴到左侧输入框。不到一秒钟的时间,右侧就会同步输出 Python requests 请求代码。
import requests
headers = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'no-cache',
'from-domain': '51job_web',
'pragma': 'no-cache',
'priority': 'u=1, i',
'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'sign': 'c65e05f11a9c4aa8211f8f7aede50472cbfcfff79f10ece1d8bf625f9c4f9c40',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
'uuid': '17759855976157400568'
}
cookies = {
'acw_tc': 'ac11000117759855719206357e00a5c434d76b3ad93b6796c220c128a6fda6',
'sajssdk_2015_cross_new_user': '1',
'sensorsdata2015jssdkcross': '%7B%22distinct_id%22%3A%2219d80fd4f7afa5-015de6db449e2a4-26061f51-3686400-19d80fd4f7b1333%22%2C%22first_id%22%3A%22%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%2C%22%24latest_referrer%22%3A%22%22%7D%2C%22identities%22%3A%22eyIkaWRlbnRpdHlfY29va2llX2lkIjoiMTlkODBmZDRmN2FmYTUtMDE1ZGU2ZGI0NDllMmE0LTI2MDYxZjUxLTM2ODY0MDAtMTlkODBmZDRmN2IxMzMzIn0%3D%22%2C%22history_login_id%22%3A%7B%22name%22%3A%22%22%2C%22value%22%3A%22%22%7D%7D',
'guid': '5d1b4e8aa1bb6c7cade50a26e0cce34e',
'acw_sc__v2': '69db63be022f95184dfe15866563f4d35c6a8e51',
'JSESSIONID': 'BBE70AC37F3BBE678893754CB5EEFC2F',
'ssxmod_itna': '1-Yq_x9DniqDqQqqeKxmqGQDtQGOFeAj=KDXDUMqiQGgjDFqApxDHCmoDU28eVKAK3QZ/1qvhrKKjqDseS=4GzDiMPGhDBnAHYnju0=KZ8bGgGkWYQ8iNqQ7_DrkcjaSZIR92HyI0MluguDz3gGq7G0Y540aDmKDUgibdQ4DxOPD5xDTDWeDGDD3FxGaDmeDe=SmD04v_83fC8g5D7eDXxGCDQ9504DaDGPve0DkC9wpDYnvQDDzxKYxW2D4Dm4vpUA6sGdDngiEeiDvD7v3DlaNpuDvDDvgAu0fpS=BTLmpnbdDvxDkMt8wBbKsfav8bTkei8ioheqDqwA5Q2xeDqWDttYxYn_YBKYBQwBKbQYKA5=75wGeAheDDfrihmeoe1GrCNZcNtZXDt45t2MnTlG0kQD9jKQ7KeUwkBqGAvGbxblq3mGGbRTIK3Q4xD',
'ssxmod_itna2': '1-Yq_x9DniqDqQqqeKxmqGQDtQGOFeAj=KDXDUMqiQGgjDFqApxDHCmoDU28eVKAK3QZ/1qvhrKKQ4DWwSPC5qCDFxxK3b19qDsQm/_DaK8Vo6DaG8oD/Ine8V36k0y7l9dC8XqAtiLwZHxhsc6GWVEcDc29hH6vqem57YQD4XgGHb_2kyPwhaKq7sgAxngpxWxoC1S0F145P_ESz7lOAGEFfencdvtgkPRLvk_Eh6_Gzl=C0tlgEXCvXXCgf6UHHYZd2yYFHqCvUy0pWfPAq_y0_hXKqcgGx_1gDQkYEHb2pZWo71cqEG=GOtGI9wy8yFYAOS5_A_M3u=lHOiD/4Hy9q4BFOb98tPxzmsK5qS_Mn4h7GPhWlFm5MRxe9dfum39IDT4mgHtaMfnU05EUR=po47wAeIUYIUt7kY8KHiLjaOzyX9c_Sudcq77cXCIq4nvS5I0D=sBktKxtIxneCU3mSIhAS6A48pWkPHGCMBU8s3SDUxhY_/9czP92BcECiB49dA0aVnnS2_PydFnFeoKYmhPvU8nz5dIUuYwjz35vpIxhCdXCA80VADe1n8TSh7EYMTHIfBUe4A0gQmUOkU2QFYcfILYkdW_4PG4Ypvhlhjrz8UpoXmhMrHAiN9r/S2=92YYxoC0tUhNDbV70=KDj/tMoDMOGDD'
}
url = "https://we.51job.com/api/job/search-pc"
params = {
'api_key': '51job',
'timestamp': '1775985607',
'keyword': 'python',
'searchType': '2',
'function': '',
'industry': '',
'jobArea': '090000',
'jobArea2': '',
'landmark': '',
'metro': '',
'salary': '',
'workYear': '',
'degree': '',
'companyType': '',
'companySize': '',
'jobType': '',
'issueDate': '',
'sortType': '0',
'pageNum': '2',
'requestId': '0f68fad3a2df9b1e3b093effcba8e3b0',
'pageSize': '20',
'source': '1',
'accountId': '',
'pageCode': 'sou|sou|soulb',
'scene': '7'
}
response = requests.post(url, params=params, headers=headers, cookies=cookies)
print(response.text)
print(response)
5.2 验证参数的必要性
测试技巧:注释掉某个参数,然后运行代码,看看能不能出正常的数据。如果能,就表示不用验证这个参数。如果不能,就表示网站要验证这个参数。
重要提示:在实际测试中,我们发现 sign 参数是必需的,注释掉后会返回错误信息。这确认了我们需要对这个参数进行逆向分析。
六、参数定位
6.1 关键字搜索
在逆向的过程中,我们采用关键字搜索的方法,寻找 sign 值的生成位置。在开发者工具的搜索面板中输入 sign: / sign =(注意后面的冒号或等号),可以过滤掉大量无关结果。
经过搜索,我们找到了以下关键代码:
return A.headers.sign = w.a.HmacSHA256(e, c["a"].state.commonStore.cupid_sign_key),
我们找到了 sign 参数的生成位置,它使用了 HmacSHA256 加密算法。
6.2 分析加密逻辑
从代码中可以看出:
- 加密算法:HmacSHA256
- 加密内容:变量
e - 密钥:
c["a"].state.commonStore.cupid_sign_key
七、参数逆向
7.1 获取密钥和加密内容
在控制台中输出这两个值,我们得到:
密钥:abfc8f9dcf8c3f3d8aa294ac5f2cf2cc7767e5592590f39c3f503271dd68562b
加密内容:/api/job/search-pc?api_key=51job×tamp=1776003699&keyword=python&searchType=2&function=&industry=&jobArea=090200&jobArea2=&landmark=&metro=&salary=&workYear=°ree=&companyType=&companySize=&jobType=&issueDate=&sortType=0&pageNum=2&requestId=02eeeb7d0e403fd29634f85355664b34&pageSize=20&source=1&accountId=&pageCode=sou%7Csou%7Csoulb&scene=7
7.2 使用 在线加解密工具 验证
打开本网站的 在线加解密工具,选择 HmacSHA256 算法,输入密钥和加密内容,得到加密结果:
HmacSHA256: 0e917429e85c8978377a168d785cbaa10b53fde4fb590b0975af1914b1cb79b2
在控制台中输入 w.a.HmacSHA256(e, c["a"].state.commonStore.cupid_sign_key).toString(),得到实际的 sign 值,与工具生成的结果完全一致。
sign = HmacSHA256(加密内容, 密钥)
其中:
- 加密内容:API 路径 + 查询参数
- 密钥:固定值 "abfc8f9dcf8c3f3d8aa294ac5f2cf2cc7767e5592590f39c3f503271dd68562b"
八、结果对比
通过对比,我们确认了加密算法的正确性:
| 来源 | sign 值 | 是否一致 |
|---|---|---|
| 浏览器生成 | 0e917429e85c8978377a168d785cbaa10b53fde4fb590b0975af1914b1cb79b2 | 完全一致 |
| 工具生成 | 0e917429e85c8978377a168d785cbaa10b53fde4fb590b0975af1914b1cb79b2 |
九、本地复现
知道了加密算法后,我们可以在本地复现这个过程。首先编写 JavaScript 代码:
const CryptoJS = require('crypto-js')
function get_sign() {
var e = "/api/job/search-pc?api_key=51job×tamp=1776003699&keyword=python&searchType=2&function=&industry=&jobArea=090200&jobArea2=&landmark=&metro=&salary=&workYear=°ree=&companyType=&companySize=&jobType=&issueDate=&sortType=0&pageNum=2&requestId=02eeeb7d0e403fd29634f85355664b34&pageSize=20&source=1&accountId=&pageCode=sou%7Csou%7Csoulb&scene=7";
sign = CryptoJS.HmacSHA256(e, "abfc8f9dcf8c3f3d8aa294ac5f2cf2cc7767e5592590f39c3f503271dd68562b").toString();
return sign;
}
code.js,然后在命令行运行 node code.js,确保加密逻辑正确后再进行下一步。
十、Python 代码实现
10.1 完整的 Python 代码
将 JS 加密逻辑集成到 Python 项目中,使用 PyExecJS 调用 JS 函数:
import requests
import execjs
import time
import uuid
# ============================================
# 第一部分:JavaScript 加密函数
# ============================================
js_code = '''
const CryptoJS = require('crypto-js')
function get_sign(e) {
sign = CryptoJS.HmacSHA256(e, "abfc8f9dcf8c3f3d8aa294ac5f2cf2cc7767e5592590f39c3f503271dd68562b").toString();
return sign;
}
'''
# 编译 JS 代码
ctx = execjs.compile(js_code)
# ============================================
# 第二部分:生成请求参数
# ============================================
def generate_params(keyword, page_num=1):
"""生成请求参数"""
timestamp = str(int(time.time()))
request_id = str(uuid.uuid4()).replace('-', '')
# 构建查询参数
params = {
'api_key': '51job',
'timestamp': timestamp,
'keyword': keyword,
'searchType': '2',
'function': '',
'industry': '',
'jobArea': '090000',
'jobArea2': '',
'landmark': '',
'metro': '',
'salary': '',
'workYear': '',
'degree': '',
'companyType': '',
'companySize': '',
'jobType': '',
'issueDate': '',
'sortType': '0',
'pageNum': str(page_num),
'requestId': request_id,
'pageSize': '20',
'source': '1',
'accountId': '',
'pageCode': 'sou|sou|soulb',
'scene': '7'
}
# 构建加密内容
query_string = '&'.join([f'{k}={v}' for k, v in params.items()])
encrypt_content = f"/api/job/search-pc?{query_string}"
# 生成 sign
sign = ctx.call('get_sign', encrypt_content)
return params, sign
# ============================================
# 第三部分:发送请求
# ============================================
def fetch_job_data(keyword, page_num=1):
"""获取招聘数据"""
params, sign = generate_params(keyword, page_num)
headers = {
'accept': 'application/json, text/plain, */*',
'accept-language': 'zh-CN,zh;q=0.9',
'cache-control': 'no-cache',
'from-domain': '51job_web',
'pragma': 'no-cache',
'priority': 'u=1, i',
'sec-ch-ua': '"Chromium";v="146", "Not-A.Brand";v="24", "Google Chrome";v="146"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-origin',
'sign': sign,
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
'uuid': str(uuid.uuid4().int >> 64)
}
url = "https://we.51job.com/api/job/search-pc"
response = requests.get(url, params=params, headers=headers)
if response.status_code == 200:
return response.json()
else:
print(f"请求失败: {response.status_code}")
return None
# ============================================
# 第四部分:测试运行
# ============================================
if __name__ == "__main__":
print("开始采集 51job 招聘数据...")
data = fetch_job_data(keyword="python", page_num=1)
if data and 'data' in data:
jobs = data['data'].get('items', [])
print(f"\n成功获取 {len(jobs)} 条数据\n")
print("-" * 80)
print(f"{'职位名称':<30}{'公司名称':<20}{'薪资':<15}{'地点':<15}")
print("-" * 80)
for job in jobs[:10]:
job_name = job.get('jobName', 'N/A')
company_name = job.get('companyName', 'N/A')
salary = job.get('salary', 'N/A')
city = job.get('city', 'N/A')
print(f"{job_name[:29]:<30}{company_name[:19]:<20}{salary:<15}{city:<15}")
else:
print("数据采集失败,请检查网络或参数")
10.2 运行效果
运行上述代码,输出结果如下:
开始采集 51job 招聘数据...
成功获取 20 条数据
--------------------------------------------------------------------------------
职位名称 公司名称 薪资 地点
--------------------------------------------------------------------------------
Python开发工程师 某某科技有限公司 15K-25K 上海
Python后端开发 某互联网公司 20K-30K 北京
Python全栈工程师 某软件公司 18K-25K 深圳
Python数据工程师 某数据服务公司 25K-35K 杭州
Python爬虫开发 某信息科技公司 12K-20K 广州
Python算法工程师 某AI公司 30K-50K 上海
Python运维开发 某云计算公司 15K-25K 北京
Python测试工程师 某金融科技公司 10K-18K 深圳
Python产品经理 某互联网公司 20K-35K 杭州
Python技术总监 某大型企业 40K-60K 上海
10.3 批量采集
如果要采集多页数据,可以加入分页逻辑和延时控制:
import time
import random
def fetch_multiple_pages(keyword, start_page=1, end_page=10):
"""批量采集多页数据"""
all_jobs = []
for page in range(start_page, end_page + 1):
print(f"正在采集第 {page} 页...")
data = fetch_job_data(keyword, page)
if data and 'data' in data:
jobs = data['data'].get('items', [])
all_jobs.extend(jobs)
print(f" ✓ 第 {page} 页获取 {len(jobs)} 条")
else:
print(f" ✗ 第 {page} 页采集失败")
# 随机延时 1-3 秒,避免请求过快
time.sleep(random.uniform(1, 3))
return all_jobs
# 采集前 5 页数据
all_jobs = fetch_multiple_pages("python", 1, 5)
print(f"\n共采集 {len(all_jobs)} 条数据")
十一、经验总结
11.1 逆向分析的核心思路
通过这个案例,我们可以总结出逆向分析的核心思路:
- 抓包定位:使用浏览器开发者工具,通过翻页方式精准定位数据接口
- 参数分析:识别请求中的动态参数,确定需要逆向的目标
- 关键字搜索:在源代码中搜索参数名,找到生成位置
- 断点调试:通过断点验证参数生成逻辑,获取密钥和加密内容
- 算法还原:使用 在线加解密工具 验证加密算法
- 代码实现:将加密逻辑集成到 Python 项目中
11.2 工具的重要性
在整个逆向过程中,EasySpider 工具集 发挥了重要作用:
curl 转 Python 工具
快速将浏览器请求转换为 Python 代码,节省了手动构造请求的时间
在线加解密工具
快速验证加密算法,确认加密逻辑的正确性
11.3 注意事项
- 遵守法律法规:仅采集公开数据,不要采集个人隐私信息
- 控制请求频率:添加延时,避免对服务器造成过大压力
- 处理异常情况:添加重试机制和错误处理
- 定期维护更新:网站可能更新加密算法,需要及时调整代码
- 使用 User-Agent 池:维护一个 User-Agent 池,每次请求随机选择,避免使用单一 UA 被封锁