某招聘平台爬虫:爬取招聘岗位数据,分析各城市薪资水平

引言

"想在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封禁问题。

某招聘平台的岗位数据是洞察就业市场的一扇窗口。通过合理的爬虫策略和数据分析方法,我们可以将这座数据金矿转化为可执行的洞察------哪些城市的薪资最具竞争力、哪些行业的薪酬增长最快、哪些技能正在获得溢价。

最后需要提醒的是:数据采集行为应当遵循平台规则,控制请求频率以避免对目标服务器造成过载。请遵守相关法律法规,仅将爬虫技术用于学习和研究目的。希望本文能帮助你在就业市场数据分析的道路上迈出坚实的一步。

相关推荐
2501_933670791 小时前
2026秋招量化分析岗技能栈:Python、SQL、统计建模、回测项目怎么准备
开发语言·python·sql
2601_962077601 小时前
python Dejavu库快速识别音频指纹实例探究
python·音乐识别·dejavu库·音频指纹识别·实例探究
科技苑1 小时前
如何用Python编程实现一个简单的Web爬虫?
人工智能·python
李少兄2 小时前
JavaScript 数据类型完全指南
开发语言·javascript·ecmascript
dayDayupbetter2 小时前
Visual C++ 2010安装与使用高手秘籍
python
隐擎fox2 小时前
深入理解网络传输层安全:TLS 指纹识别(JA3/JA4)原理与 Python 协议层检测实战
爬虫·python·网络协议·安全·网络安全·https
医疗信息化王工2 小时前
DataForge:基于 Python 的数据库批量导出 Excel 工具——从架构到部署的全流程实战
数据库·python·excel
Seoyoneh2 小时前
Agentic Workflow编排架构:云客服从“被动响应”迈向“主动执行”的技术实现
java·开发语言·架构
Tangyuewei3 小时前
Java 写 Agent:模型只是组件
java·开发语言