通过DrissionPage爬取boss直聘,绕过__zp_stoken__解析

前言

最近想用自己之前写的脚本看看市场行情,结果发现代码竟然跑不通了!

仔细一查,发现BOSS直聘的岗位搜索结果页做了三个改动:

  • 反爬检测:打开开发者工具就进入无限循环,或者整个网页闪退关闭
  • 分页机制取消:以前通过 page 参数翻页的方式失效了。
  • 薪资数据加密:列表页的薪资数字变成了乱码,用字体加密的方式防止直接采集。

不过我发现虽然搜索结果页加密了,但每个岗位的详情页里的薪资,还是明文的!

基于这个发现,我的主要调整思路是:

  • 临时禁用JavaScript,让页面以纯静态形式加载。
  • 适配新的页面加载方式,改为滚动加载职位信息。
  • 无视搜索结果页的薪资加密,直接解析详情页。

缺点:由于是通过自动化的方式采集数据,效率比较一般(300个岗位详情20分钟左右采集完)

优点:稳定,不容易触发反爬,试了五个关键词采集,也就是1500个详情页解析,个人的话应该够用

我把脚本整理了一下,发出来供有类似需求的朋友参考。欢迎交流~

1.cookie处理

脚本打开浏览器后,打开登录界面,扫码登录,等待脚本保存 cookie

python 复制代码
def get_cookie(self, url='https://www.zhipin.com', timeout=30):
    """获取Cookie(手动登录)"""
    logger.info("请打开登录窗口,扫码登录您的个人账号...")
    self.page.get(url)
    time.sleep(timeout)  # 等待手动登录

    cookies = self.page.cookies()
    self.file_manager.save_json(cookies, self.config.cookie_file)
    logger.info(f"Cookie已保存到 {self.config.cookie_file}")

def load_cookie(self) -> bool:
    """加载Cookie"""
    if not os.path.exists(self.config.cookie_file):
        logger.warning(f"Cookie文件不存在: {self.config.cookie_file}")
        return False

    logger.info("开始加载Cookie")
    self.page.get("https://www.zhipin.com")
    time.sleep(2)

    cookies = self.file_manager.load_json(self.config.cookie_file)
    for cookie in cookies:
        self.page.set.cookies(cookie)

    self.page.refresh()
    time.sleep(2)
    logger.info("Cookie加载完成")

    # 保存首页HTML
    if self.config.save_html_debug:
        self.file_manager.save_html(self.page.html, "01_首页_加载Cookie后")

    return True

2.首页处理

2.1 前置步骤(绕过反爬检测,仅调试时勾选!)

注:正常访问网站时发生异常(比如一直在空白页加载),可能是这个禁用js的选项没有取消勾选,取消勾选后再刷新网站即可正常访问

由于目标网站设置了严格的反爬虫机制,直接按 F12 进入开发者工具,网站会闪退。我们可以通过临时禁用浏览器JavaScript来绕过大部分检测,让页面以纯静态形式加载,这个模式下我们可以随意定位元素。

操作步骤:

  1. 打开浏览器,按 F12 进入开发者工具
  2. 点击右上角的 齿轮图标 ⚙ 进入设置
  3. 向下找到 "Debugger"(调试器) 部分
  4. 勾选 "Disable JavaScript"(禁用 JavaScript)

图示:

2.2 职位搜索

如图所示,通过选择器选择输入框,可以得到整个输入框的元素

由此可以得到首页输入岗位名称并搜索的代码:访问首页,等待搜索框元素,清空搜索框,输入关键词,回车

回车后即可进入搜索结果页面

python 复制代码
# 访问首页
logger.info("正在访问BOSS直聘首页...")
self.page.get("https://www.zhipin.com/")
# 等待搜索框出现(匹配class或placeholder符合的input元素)
self.page.wait.ele_displayed('xpath://input[@class="ipt-search" or @placeholder="搜索职位、公司"]', timeout=10)
# 输入搜索词
logger.info("正在输入搜索关键词...")
# 获取搜索框元素
search_input = self.page.ele('xpath://input[@class="ipt-search" or @placeholder="搜索职位、公司"]', timeout=10)
self.page.scroll.to_see(search_input)  # 滚动到搜索框
search_input.clear()  # 清空搜索框
time.sleep(0.5)
search_input.input(self.config.job_name)  # 输入搜索词
time.sleep(0.5)
search_input.input('\n')  # 回车确认
# 等待搜索结果
time.sleep(1)  # 等待页面刷新
new_url = re.sub(r'city=\d+', f'city={self.config.city_code}', self.page.url) if self.config.city_code else self.page.url  # 替换城市代码,通过get_city_code方法赋值给self.config.city_code,就可以获取目标城市的搜索请求URL
logger.info(f"正在访问搜索结果页: {new_url}")
self.page.get(new_url)
logger.info(f"等待 {self.config.search_wait_time} 秒,请开始你的条件筛选(例如:学历,工作经验)")
time.sleep(self.config.search_wait_time)
logger.info(f"搜索结果页URL未发生变化") if self.page.url == new_url else logger.info(f"搜索结果页URL发生变化: {self.page.url}")

2.3 拓展-高级搜索

示例,我需要输入城市即可返回对应的城市代码

python 复制代码
def get_city_code(self, city_name: str) -> Optional[str]:
    """根据城市名称获取城市代码"""
    try:
        logger.debug(f"正在从Boss直聘API获取城市 [{city_name}] 的代码...")
        response = requests.get("https://www.zhipin.com/wapi/zpCommon/data/city.json")
        if response.status_code != 200:
            logger.error(f"请求城市数据失败,状态码: {response.status_code}")
            return None
        data = response.json()
        # 遍历查找城市
        for province in data['zpData']['cityList']:
            for city in province['subLevelModelList']:
                if city['name'] == city_name:
                    city_code = city['code']
                    self.cache[city_name] = city_code
                    return city_code
        return None
    except Exception as e:
        logger.error(f"获取城市代码失败: {e}")
        return None

由于筛选条件有点多,我这边只做了一个自定义,其他筛选就通过search_wait_time等待的时候,在网页上直接筛选,如图所示

如果需要构造高级搜索条件,可以参考

python 复制代码
def fetch_job_conditions(output_file):
    """获取筛选条件的相关数据,并保存为 JSON 文件"""
    data = requests.get("https://www.zhipin.com/wapi/zpgeek/search/job/condition.json", verify=False).json()
    # 构建新的字典格式
    output_data = {
        "experience": [],
        "degree": [],
        "payType": [],
        "salary": [],
        "stage": [],
        "scale": [],
        "partTime": [],
        "jobType": []
    }
    # 提取工作经验数据
    for experience in data['zpData']['experienceList']:
        output_data["experience"].append({
            "code": experience["code"],
            "name": experience["name"]
        })
    # 提取学历数据
    for degree in data['zpData']['degreeList']:
        output_data["degree"].append({
            "code": degree["code"],
            "name": degree["name"]
        })
    # 提取薪资类型数据
    for pay in data['zpData']['payTypeList']:
        output_data["payType"].append({
            "code": pay["code"],
            "name": pay["name"]
        })
    # 提取薪资范围数据
    for salary in data['zpData']['salaryList']:
        output_data["salary"].append({
            "code": salary["code"],
            "name": salary["name"]
        })
    # 提取融资阶段数据
    for stage in data['zpData']['stageList']:
        output_data["stage"].append({
            "code": stage["code"],
            "name": stage["name"]
        })
    # 提取公司规模数据
    for scale in data['zpData']['scaleList']:
        output_data["scale"].append({
            "code": scale["code"],
            "name": scale["name"]
        })
    # 提取兼职类型数据
    for part_time in data['zpData']['partTimeList']:
        output_data["partTime"].append({
            "code": part_time["code"],
            "name": part_time["name"]
        })
    # 提取职位类型数据
    for job_type in data['zpData']['jobTypeList']:
        output_data["jobType"].append({
            "code": job_type["code"],
            "name": job_type["name"]
        })
        
    # 保存为 JSON 文件
    with open(output_file, "w", encoding="utf-8") as f:
        json.dump(output_data, f, ensure_ascii=False, indent=4)
    print(f"筛选条件数据已保存到 {output_file} 文件中。")

3.搜索结果页处理

同 2.1 步骤,先临时禁用浏览器JavaScript防止反爬检测,方便我们使用开发者工具定位元素

如图所示,通过选择器选择职位任意参数,得到右边的信息

3.1 滚动加载职位信息

注:平台限制,最多加载300个岗位信息,如果想搜索匹配的信息,建议先做条件筛选再进行采集

python 复制代码
def _scroll_load(self):
    """滚动加载更多职位"""
    logger.info(f"开始滚动加载,最大滚动次数: {self.config.search_max_scrolls}")

    last_count = 0
    no_increase = 0

    for i in range(self.config.search_max_scrolls):
        self.page.scroll.to_bottom()  # 滚动到底部,触发懒加载
        time.sleep(1.5)  # 等待加载

        # 定位职位列表容器:匹配class包含"rec-job-list"的ul元素
        job_list = self.page.ele('xpath://ul[contains(@class, "rec-job-list")]', timeout=5)
        if job_list:
            # 获取当前页职位数量:在job_list内部搜索所有class包含"job-card-box"的li元素,eles() 返回多个元素(列表)
            current = len(job_list.eles('xpath:.//li[contains(@class, "job-card-box")]'))
            logger.info(f"第{i + 1}次滚动后: {current}个职位")

            if current > last_count:
                no_increase = 0
                last_count = current
            else:
                no_increase += 1
                if no_increase >= 3:
                    logger.info("连续3次无新增,停止滚动")
                    break

        time.sleep(1)

3.2 搜索结果页解析脚本

python 复制代码
def _parse_job_card(self, card, index: int) -> Optional[Dict]:
"""解析单个职位卡片"""
job = {
    '序号': index,
    '职位名称': '',
    '薪资': '',
    '公司': '',
    '工作地点': '',
    '经验要求': '',
    '学历要求': '',
    '职位详情链接': '',
    '公司详情链接': '',
    '职位唯一ID': ''
}

# 职位名称和链接
name_elem = card.ele('xpath:.//a[contains(@class, "job-name")]')
if name_elem:
    job['职位名称'] = self.clean_text(name_elem.text)
    href = name_elem.attr('href')
    if href:
        job['职位详情链接'] = 'https://www.zhipin.com' + href if not href.startswith('http') else href

# # 薪资(该页面的薪资是加密的,直接跳过)
# salary_elem = card.ele('xpath:.//span[contains(@class, "job-salary")]')
# job['薪资'] = self.clean_text(salary_elem.text)

# 公司
company_elem = card.ele('xpath:.//span[contains(@class, "boss-name")]')
if company_elem:
    job['公司'] = self.clean_text(company_elem.text)

# 地点
loc_elem = card.ele('xpath:.//span[contains(@class, "company-location")]')
if loc_elem:
    job['工作地点'] = self.clean_text(loc_elem.text)

# 经验学历标签
tag_list = card.ele('xpath:.//ul[contains(@class, "tag-list")]')
if tag_list:
    tags = [tag.text for tag in tag_list.eles('tag:li')]  # 提取所有li元素的文本
    if len(tags) > 0:
        job['经验要求'] = tags[0]
    if len(tags) > 1:
        job['学历要求'] = tags[1]

# 公司链接
company_link_elem = card.ele('xpath:.//a[contains(@class, "boss-info")]')
if company_link_elem:
    href = company_link_elem.attr('href')
    if href:
        job['公司详情链接'] = 'https://www.zhipin.com' + href

return job if job['职位名称'] else None

4.详情页处理

同 2.1 步骤,先临时禁用浏览器JavaScript防止反爬检测,方便我们使用开发者工具定位元素

如图所示,通过选择器选择职位任意参数,得到右边的信息

4.1详情页解析脚本

python 复制代码
def parse_job_detail(self, page: ChromiumPage, index: int, job_url: str = '') -> Dict:
    """解析职位详情页,并与列表数据合并"""
    job_id = self.extract_job_id(job_url)

    # 初始化基础数据
    job = {
        '序号': index,
        '数据采集时间': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
        '职位状态': '',
        '职位标题': '',
        '薪资': '',
        '工作城市': '',
        '工作区域': '',
        '工作地点': '',
        '经验要求': '',
        '学历要求': '',
        '岗位标签': '',
        '职位描述': '',
        '职位详情链接': job_url,
        '职位唯一ID': job_id,
        '公司名称': '',
        '融资情况': '',
        '公司规模': '',
        '所属行业': '',
        '公司详情链接': '',
        '招聘负责人': '',
        '活跃状态': '',
        '招聘者职位': ''
    }

    # 从缓存中合并列表数据
    if job_id and job_id in self.job_list_cache:
        list_data = self.job_list_cache[job_id]
        job['职位标题'] = list_data.get('职位名称', '')
        job['薪资'] = list_data.get('薪资', '')
        job['工作区域'] = list_data.get('工作地点', '').split('·')[1] if '·' in list_data.get('工作地点', '') else ''
        job['工作地点'] = list_data.get('工作地点', '') if '·' in list_data.get('工作地点', '') else ''
        job['公司名称'] = list_data.get('公司', '')
        job['公司详情链接'] = list_data.get('公司详情链接', '')
        if list_data.get('经验要求'):
            job['经验要求'] = list_data.get('经验要求', '')
        if list_data.get('学历要求'):
            job['学历要求'] = list_data.get('学历要求', '')

    try:
        # 基本信息区域
        info_primary = page.ele('xpath://div[@class="info-primary"]', timeout=5)
        if info_primary:
            job['职位状态'] = self._get_text(info_primary, 'xpath:.//div[contains(@class, "job-status")]')
            job['职位标题'] = self._get_text(info_primary, 'xpath:.//h1[@title]')
            job['薪资'] = self._get_text(info_primary, 'xpath:.//span[contains(@class, "salary")]')

            # 城市、经验、学历
            info_text = info_primary.ele('tag:p')
            if info_text:
                job['工作城市'] = self._get_text(info_text, 'xpath:.//a[contains(@class, "text-city")]')

                exp = info_text.ele('xpath:.//span[contains(@class, "text-experiece")]')
                if exp:
                    job['经验要求'] = self.clean_text(exp.text)

                edu = info_text.ele('xpath:.//span[contains(@class, "text-degree")]')
                if edu:
                    job['学历要求'] = self.clean_text(edu.text)

        # 职位描述和标签
        job_detail = page.ele('xpath://div[@class="job-detail-section"]', timeout=5)
        if job_detail:
            job['职位描述'] = self._get_text(job_detail, 'xpath:.//div[contains(@class, "job-sec-text")]')
            # 技能标签
            skills = []
            keyword_list = job_detail.ele('xpath:.//ul[contains(@class, "job-keyword-list")]')
            if keyword_list:
                for item in keyword_list.eles('xpath:.//li', timeout=1):
                    skills.append(self.clean_text(item.text))
            if skills:
                job['岗位标签'] = '、'.join(skills)

        # 公司信息
        company = page.ele('xpath://div[@class="sider-company"]', timeout=5)
        if company:
            job['公司名称'] = self._get_text(company, 'xpath:.//a[@ka="job-detail-company_custompage"]')
            job['所属行业'] = self._get_text(company, 'xpath:.//a[@ka="job-detail-brandindustry"]')
            # 在company中查找内部包含class为"icon-stage"的i标签的p元素
            job['融资情况'] = self._get_text(company, 'xpath:.//p[i[contains(@class, "icon-stage")]]')
            job['公司规模'] = self._get_text(company, 'xpath:.//p[i[contains(@class, "icon-scale")]]')

        # 岗位标签
        welfare = []
        job_tags = page.ele('xpath://div[contains(@class, "job-tags")]', timeout=1)
        if job_tags:
            for span in job_tags.eles('xpath:.//span', timeout=1):
                if span.text.strip():
                    welfare.append(self.clean_text(span.text))
        job['岗位标签'] = '、'.join(welfare)

        # 招聘负责人信息(姓名、活跃状态、招聘者职位)
        boss = page.ele('xpath://div[@class="job-boss-info"]', timeout=5)
        if boss:
            # 获取姓名元素
            name_elem = boss.ele('xpath:.//h2[@class="name"]')
            if name_elem:
                full_text = name_elem.text.strip()
                # 获取状态元素(可能是boss-active-time或boss-online-tag)
                status_elem = boss.ele('xpath:.//span[contains(@class, "boss-active-time") or contains(@class, "boss-online-tag")]', timeout=1)
                status = status_elem.text.strip() if status_elem else ''
                # 如果状态在姓名里,从姓名中移除
                name = full_text.replace(status, '').strip() if status and status in full_text else full_text

                job['招聘负责人'] = name
                job['活跃状态'] = status

            boss_attr = self._get_text(boss, 'xpath:.//div[@class="boss-info-attr"]')
            if boss_attr:
                job['招聘者职位'] = boss_attr.split('·', 1)[-1].strip() if '·' in boss_attr else boss_attr

    except Exception as e:
        logger.warning(f"解析职位详情失败: {e}")

    return job

def _get_text(self, element, selector: str) -> str:
    """安全获取元素文本"""
    try:
        elem = element.ele(selector, timeout=1)
        return self.clean_text(elem.text) if elem else ''
    except:
        return ''

5.结果展示

岗位职责

基础参数与结果文件

相关推荐
爱打代码的小林2 小时前
识别盒装图标项目的一些功能函数
python·pycharm
小邓睡不饱耶2 小时前
基于Python的Q房网二手房数据爬虫实现
开发语言·爬虫·python
在屏幕前出油2 小时前
00. FastAPI——了解FastAPI
后端·python·pycharm·fastapi
深蓝电商API2 小时前
爬虫任务调度:APScheduler 定时执行
开发语言·爬虫·python
kang_jin2 小时前
超详细 Python 爬虫指南
开发语言·爬虫·python
亓才孓2 小时前
【Stream】讲常见数据结构转为map<String,Long>
数据结构·windows·python
weixin199701080162 小时前
网易考拉商品详情页前端性能优化实战
java·前端·python·性能优化
APO Research2 小时前
Virtual Commissioning产业趋势:数字孪生驱动的虚拟调试如何重塑工业自动化工程体系
大数据·自动化·智能制造·数字孪生·工业自动化·工程数字化
凌晨一点的秃头猪2 小时前
文件路径中 / 和 \ 的使用规则
python