51job 数据采集实战

本文章中所有内容仅供学习交流,相关链接做了脱敏处理,若有侵权,请联系我立即删除!

一、项目背景与目标

学习目标 通过本项目,你将掌握:
  • 如何使用浏览器开发者工具进行网络抓包
  • 如何精准定位数据接口
  • 如何识别和分析加密参数
  • 如何使用 EasySpider 工具集 快速转换请求代码
  • 如何定位 JavaScript 中的加密函数
  • 如何使用 在线加解密工具 验证加密算法
  • 如何将 JS 加密逻辑迁移到 Python 实现

在当今数据驱动的时代,招聘数据的采集和分析对于企业招聘策略制定、人才市场趋势研究等方面具有重要意义。51job 作为国内知名的招聘平台,拥有海量的招聘信息,是研究就业市场的重要数据源。

然而,51job 网站为了保护数据安全,采用了 参数签名 的反爬策略,其中 sign 参数是我们需要逆向分析的关键。本文将完整记录从抓包到逆向的整个过程,帮助你掌握爬虫逆向的核心技能。


二、抓包分析

2.1 为什么选择翻页方式抓包?

在采集招聘栏目数据时,采用翻页的方式可以更精准地定位目标接口:

  • 减少干扰:避免页面刷新时加载的大量无关静态资源
  • 精准定位:只触发与数据加载相关的接口
  • 提高效率:快速找到目标数据接口

2.2 抓包步骤

  1. 打开 Chrome 浏览器,访问 51job 网站
  2. 按 F12 打开开发者工具,切换到 Network 标签
  3. 在招聘页面中输入关键词(如 "python")进行搜索
  4. 点击「下一页」按钮,观察网络请求列表
  5. 在过滤器中选择 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 被封锁