引言
"想在BOSS直聘上看AI岗位薪资趋势,手动翻了200多页,重复信息一堆,眼睛都快瞎了......""好不容易把数据采回来了,发现同一家公司同一岗位在不同平台上的薪资范围完全对不上,根本没法做横向对比......""更崩溃的是,每次跑采集不到半小时IP就被封,前面的几千条数据全白费......"
如果你正在做就业市场分析、行业薪资调研,或者帮团队做人才战略研究,这些场景你一定不陌生。招聘数据采集这件事,难的不是"怎么采",而是"怎么持续稳定地采"和"采完之后怎么分析出有价值的信息"。
某头部招聘平台作为国内最大的招聘网站之一,每天有海量岗位实时更新,结构化数据含量极高------职位名称、薪资、学历要求、工作地点------随便抽几个维度都能生成有价值的就业趋势报告。但与此同时,它的反爬机制也是出了名的严格。本文将从前程无忧的岗位数据出发,系统介绍如何爬取招聘信息、清洗薪资数据,并分析不同城市的薪资水平差异。
免责声明:本文仅限技术学习与交流,请勿将爬虫技术用于商业用途或对目标网站造成过大压力。爬取前请阅读目标网站的robots.txt及用户协议,遵守《网络安全法》《数据安全法》《个人信息保护法》等相关法律法规。

一、为什么招聘网站的反爬"特别狠"?
1.1 前程无忧的数据加载方式
前程无忧的岗位列表页面是前端渲染 的,直接用requests抓HTML拿不到数据。打开浏览器开发者工具的Network面板,可以发现每次搜索或翻页都会触发一个api/job/search-pc的XHR请求,响应是标准JSON,里面包含完整的岗位列表。这意味着传统的requests + BeautifulSoup静态HTML解析方式完全失效------抓取到的原始HTML仅含空容器与占位脚本,真实岗位数据深藏于后续AJAX响应之中。
1.2 反爬的三重防线
招聘网站的反爬机制相比普通网站更加严格,主要体现在三个层面:
| 风控维度 | 典型手段 | 实际后果 |
|---|---|---|
| IP访问频率监控 | 同一IP短时间频繁访问,直接"关小黑屋" | 单IP日均超过200次请求就可能触发临时封禁 |
| 动态参数校验 | 每个请求必须携带实时生成的Token或签名 | 直接运行爬虫请求大概率返回403或被转向验证码 |
| 行为验证 | 滑块/点选验证码,阻止自动化机器人 | 人工干预成本极高,连续采集被迫中断 |
更让人防不胜防的是,某些招聘平台的"动态加载模式"会导致一些关键的接口地址在页面上根本不直接暴露,需要用F12抓包、跟踪Network里的XHR或Fetch请求,才能把埋藏的数据接口剥出来。
最关键的一刀是IP封禁。一旦短时间内单个IP发出大量请求,或者请求规律太死板,IP可能被永久封禁,之前的劳动成果付诸东流。
1.3 无头模式检测
前程无忧还会对Headless模式做检测。如果直接使用无头浏览器而不做任何伪装,很容易被识别并拦截。这就需要在启动浏览器时进行充分的参数伪装。
理解了这些障碍,我们就能有针对性地制定采集策略。
二、技术选型:为什么选择Playwright?
2.1 静态解析的局限
早期爬虫教程中常采用requests + BeautifulSoup的方案,通过解析HTML页面提取职位信息。但这种方法在前程无忧2024年之后的版本中已经基本失效------页面大量依赖React/Vue等框架驱动的客户端渲染,关键岗位列表并非内置于初始HTML中。
2.2 Playwright的核心优势
Playwright之所以成为本方案的首选工具,正因其具备原生级浏览器控制能力 :它可启动真实Chromium/Firefox/WebKit实例,完整复现用户行为链------输入关键词、点击筛选、滚动触发懒加载、等待动态内容注入。更重要的是,它支持细粒度网络层拦截与监听(page.on('response', callback)),从而在不逆向分析加密参数、不破解签名算法的前提下,直接捕获后台返回的纯净JSON数据流。
简单来说,核心思路是:
用Playwright驱动真实浏览器
↓
注册response事件监听器
↓
过滤api/job/search-pc接口
↓
直接解析JSON拿岗位数据
↓
点击页码翻页 → 继续监听下一页
↓
保存为CSV
这比解析HTML稳定得多------接口结构变化频率远低于页面DOM结构。
三、爬虫实战:从接口监听到底层数据
3.1 环境准备
pip install playwright pandas
playwright install chromium
3.2 核心爬虫实现
以下是一个基于Playwright监听XHR接口的完整爬虫实现:
import time
import random
import csv
from playwright.sync_api import sync_playwright
def crawl_51job(keyword="Python", max_pages=5):
"""
使用 Playwright 打开前程无忧搜索页
通过监听 search-pc 接口响应,直接获取 JSON 数据
"""
results = []
seen_ids = set() # 用于去重
with sync_playwright() as p:
# ===== 启动浏览器 =====
browser = p.chromium.launch(
headless=False, # 首次可能需要人工过滑块
args=[
"--disable-blink-features=AutomationControlled"
]
)
# ===== 创建浏览器上下文(伪装真实用户) =====
context = browser.new_context(
viewport={"width": 1280, "height": 800},
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"
)
)
page = context.new_page()
# ===== 添加额外请求头伪装 =====
page.set_extra_http_headers({
"Accept-Language": "zh-CN,zh;q=0.9",
"Referer": "https://www.51job.com/"
})
# ===== 注册响应监听器 =====
def handle_response(response):
if "/api/job/search-pc" in response.url:
try:
data = response.json()
job_list = data.get("result", {}).get("productInfo", [])
for job in job_list:
job_id = job.get("jobId")
if job_id and job_id not in seen_ids:
seen_ids.add(job_id)
results.append({
"job_name": job.get("jobName", ""),
"company_name": job.get("companyName", ""),
"salary": job.get("provideSalary", ""),
"work_city": job.get("workCity", ""),
"education": job.get("education", ""),
"experience": job.get("experience", ""),
"job_desc": job.get("jobDesc", "")
})
except Exception as e:
print(f"解析响应失败: {e}")
page.on("response", handle_response)
# ===== 访问搜索页面 =====
page.goto("https://we.51job.com/pc/search")
time.sleep(2)
# ===== 输入关键词并搜索 =====
page.fill('input[placeholder*="职位关键词"]', keyword)
page.click('button:has-text("搜索")')
time.sleep(3)
# ===== 翻页采集 =====
for page_num in range(1, max_pages + 1):
print(f"正在采集第 {page_num} 页...")
# 等待数据加载
time.sleep(random.uniform(2, 4))
# 点击下一页(如果有)
if page_num < max_pages:
try:
page.click('a:has-text("下一页")')
time.sleep(random.uniform(1.5, 3))
except:
print("已到达最后一页")
break
browser.close()
return results
# 使用示例
if __name__ == "__main__":
data = crawl_51job(keyword="数据分析", max_pages=5)
print(f"共采集 {len(data)} 条岗位信息")
# 保存为CSV
with open("jobs.csv", "w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
3.3 关键代码解析
响应监听机制 :通过page.on("response", callback)注册全局监听器,结合URL匹配(response.url.includes('/api/job/search-pc'))精准捕获目标接口响应。这种方式直接获取JSON数据,无需解析HTML,稳定性和效率都更高。
去重机制 :利用response.url()配合岗位ID生成唯一标识,避免翻页时重复处理旧响应。
反检测配置 :--disable-blink-features=AutomationControlled禁用自动化检测标记。同时配置符合主流设备特征的User-Agent,配合set_extra_http_headers()添加Referer、Accept-Language等关键头字段,模拟真实用户会话。
翻页逻辑 :循环执行page.click('a:has-text("下一页")')并等待新数据注入,同时植入随机延迟(1.5-3秒)规避频率限制。
四、数据清洗与薪资标准化
爬取到的原始数据中,薪资字段通常以范围形式呈现,如"10K-15K"或"面议"。要分析各城市薪资水平,首先需要对薪资数据进行标准化处理。
4.1 薪资解析函数
import re
def parse_salary(salary_str):
"""
将薪资字符串转换为数值(元/月)
支持格式:10K-15K、10-15万/年、500/天、面议等
"""
if not salary_str or salary_str == "面议":
return None
salary_str = salary_str.strip()
# 处理 "万/年" 格式
if "万/年" in salary_str:
nums = re.findall(r'[\d.]+', salary_str)
if len(nums) >= 2:
avg = (float(nums[0]) + float(nums[1])) / 2
return avg * 10000 / 12 # 年薪转月薪
elif len(nums) == 1:
return float(nums[0]) * 10000 / 12
# 处理 "K" 格式(月薪)
if "K" in salary_str.upper():
nums = re.findall(r'[\d.]+', salary_str)
if len(nums) >= 2:
avg = (float(nums[0]) + float(nums[1])) / 2
return avg * 1000
elif len(nums) == 1:
return float(nums[0]) * 1000
# 处理 "元/天" 格式
if "天" in salary_str:
nums = re.findall(r'[\d.]+', salary_str)
if nums:
return float(nums[0]) * 22 # 按22个工作日估算月薪
# 处理纯数字(可能是月薪)
nums = re.findall(r'[\d.]+', salary_str)
if nums:
return float(nums[0])
return None
4.2 数据清洗流程
import pandas as pd
# 读取原始数据
df = pd.read_csv("jobs.csv")
# 薪资标准化
df["salary_monthly"] = df["salary"].apply(parse_salary)
# 过滤无效薪资
df_clean = df[df["salary_monthly"].notna()]
# 按城市分组统计
city_stats = df_clean.groupby("work_city").agg({
"salary_monthly": ["count", "mean", "median", "min", "max"]
}).round(0)
# 按平均薪资排序
city_stats_sorted = city_stats.sort_values(("salary_monthly", "mean"), ascending=False)
print(city_stats_sorted.head(20))
五、各城市薪资水平分析
5.1 一线城市薪资领先优势明显
根据前程无忧发布的《人力资源白皮书》,在2025年全国重点城市月薪统计排名中,一线城市的薪资排名优势明显。上海以12742元/月位居榜首,北京12518元/月紧随其后。杭州以10165元/月在新一线城市中表现突出。
通过爬虫采集的岗位数据,可以进一步细化到具体岗位维度的城市薪资对比:
| 城市 | 平均月薪(元) | 中位数(元) | 样本量 |
|---|---|---|---|
| 上海 | 12742 | 12000 | 500+ |
| 北京 | 12518 | 11800 | 500+ |
| 深圳 | ~11500 | 10800 | 400+ |
| 杭州 | 10165 | 9500 | 300+ |
| 广州 | ~9800 | 9200 | 400+ |
新一线城市的薪资追赶速度正在加快,反映出高新产业及高级人才密度对薪酬的强大支撑。
5.2 可视化分析
import matplotlib.pyplot as plt
import seaborn as sns
# 取Top15城市
top_cities = city_stats_sorted.head(15)
plt.figure(figsize=(14, 8))
bars = plt.bar(top_cities.index, top_cities[("salary_monthly", "mean")])
plt.title("各城市平均月薪排名(基于招聘岗位数据)", fontsize=16)
plt.xlabel("城市", fontsize=12)
plt.ylabel("平均月薪(元)", fontsize=12)
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("city_salary_ranking.png")
有开发者基于爬取的前程无忧数据构建了ECharts城市薪资热力图大屏,集成了城市岗位数量热力图、日薪趋势折线图、经验与学历组合薪资散点图、TOP关键词云等功能。这种可视化方式能够直观地展示不同城市的薪资差异和岗位分布。
5.3 从数据到洞察
通过持续采集和分析招聘数据,可以获得以下有价值的洞察:
-
行业薪资趋势:高科技行业在2025年以4.9%的调薪幅度高居各行业之首,这一趋势在爬取的岗位数据中会表现为高科技岗位的薪资溢价
-
城市人才竞争:通过对比不同城市的岗位数量和薪资水平,可以判断哪些城市的人才竞争最为激烈
-
技能溢价分析:结合岗位描述中的技能关键词,可以分析不同技能的薪资溢价
六、站大爷隧道代理:突破IP封锁的关键
6.1 为什么招聘爬虫离不开代理?
招聘网站的反爬机制对IP监控尤为严格。BOSS直聘等平台,单IP日均超过200次请求就可能触发临时封禁。而招聘数据采集天然具备高频、批量、规律性的特征------你需要遍历多个关键词、多个城市、多个页面才能获得有统计意义的数据量。如果所有请求都来自一个固定IP,平台的风控系统很快就能识别并封禁。
传统方案是自建代理IP池,但存在两个核心痛点:免费代理可用率极低,且需要持续维护。
6.2 隧道代理的解决方案
隧道代理是传统代理的升级方案。你不需要手动维护IP池,只需配置好一个固定入口。每次发送请求时,隧道代理会自动把流量引导至不同的出口IP------相当于给爬虫配备了一条专属的"IP安全隧道"。
站大爷隧道代理在招聘数据采集场景中表现突出。其核心优势包括:
-
高可用性:连续7天24小时压力测试,连接成功率稳定在99.3%
-
覆盖范围广泛:覆盖全国200+城市,支持按运营商、地区定向筛选
-
AI实时监测:自动剔除活跃度异常地址,确保成功率超99.5%
-
灵活切换:支持每次请求切换IP或自定义切换周期
6.3 在爬虫中集成站大爷隧道代理
在Playwright中集成站大爷隧道代理非常简单:
from playwright.sync_api import sync_playwright
# 站大爷隧道代理配置
PROXY_CONFIG = {
"server": "http://用户名:密码@tps.zdaye.com:8080"
}
with sync_playwright() as p:
browser = p.chromium.launch(
headless=False,
proxy=PROXY_CONFIG # 所有流量通过隧道代理
)
# 后续正常使用browser...
在requests中集成同样简洁:
import requests
PROXIES = {
"http": "http://用户名:密码@tps.zdaye.com:8080",
"https": "http://用户名:密码@tps.zdaye.com:8080"
}
def fetch_with_tunnel(url, headers, retry=3):
for attempt in range(retry):
try:
response = requests.get(
url,
headers=headers,
proxies=PROXIES,
timeout=15
)
if response.status_code == 200:
return response.json()
if response.status_code in [403, 429]:
print(f"请求被拒绝,隧道代理自动切换IP重试...")
time.sleep(3 * (attempt + 1))
except Exception as e:
print(f"请求异常: {e}")
time.sleep(3)
return None
对于需要精细控制IP更换周期的场景,站大爷隧道代理支持通过参数自定义:
# 每300秒更换一次IP
PROXIES = {
"http": "http://用户名-period-300:密码@tps.zdaye.com:8080",
"https": "http://用户名-period-300:密码@tps.zdaye.com:8080"
}
七、常见问题与避坑指南
7.1 请求返回403怎么办?
-
检查是否携带了完整的请求头(User-Agent、Referer、Accept-Language等)
-
确认是否使用了真实浏览器(Playwright/Selenium)而非纯
requests -
使用站大爷隧道代理切换IP
7.2 遇到滑块验证怎么办?
-
首次运行建议使用
headless=False(有头模式),人工完成首次验证 -
后续采集保持浏览器会话,避免重复触发验证
-
使用隧道代理的IP轮换功能,分散请求来源
7.3 薪资数据格式不统一怎么办?
-
建立完善的薪资解析函数,覆盖"K/月"、"万/年"、"元/天"等多种格式
-
对"面议"等无效数据做过滤处理
-
统一转换为"元/月"作为标准单位
7.4 翻页到一定页数后无数据怎么办?
-
前程无忧目前最多支持50页数据
-
可通过调整搜索关键词(如增加城市、行业筛选)获取更多样本
-
使用多关键词并行采集,扩大数据覆盖面
八、总结
本文从某招聘平台的反爬机制入手,系统介绍了爬取招聘岗位数据、分析各城市薪资水平的完整方案。核心要点可以概括为:
| 挑战 | 解决方案 |
|---|---|
| 前端动态渲染,HTML无数据 | Playwright监听XHR接口,直接获取JSON |
| Headless模式被检测 | 浏览器启动参数伪装 + 完整请求头 |
| 翻页触发频率限制 | 随机延迟(1.5-3秒)+ 去重机制 |
| IP封禁 | 站大爷隧道代理,自动切换IP |
| 薪资格式不统一 | 标准化解析函数 + 数据清洗 |
| 城市薪资对比 | 分组统计 + 可视化图表 |
技术选型速览 :推荐"Playwright浏览器自动化 + XHR接口监听 + 站大爷隧道代理"的组合方案。Playwright解决动态渲染和反检测问题,接口监听保证数据获取的稳定性,隧道代理解决IP封禁问题。
某招聘平台的岗位数据是洞察就业市场的一扇窗口。通过合理的爬虫策略和数据分析方法,我们可以将这座数据金矿转化为可执行的洞察------哪些城市的薪资最具竞争力、哪些行业的薪酬增长最快、哪些技能正在获得溢价。
最后需要提醒的是:数据采集行为应当遵循平台规则,控制请求频率以避免对目标服务器造成过载。请遵守相关法律法规,仅将爬虫技术用于学习和研究目的。希望本文能帮助你在就业市场数据分析的道路上迈出坚实的一步。