news 2026/9/29 17:30:15

DrissionPage 实战:招聘平台动态数据采集与反爬对抗

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DrissionPage 实战:招聘平台动态数据采集与反爬对抗

1. 为什么我最终选了 DrissionPage 而不是 Selenium

1.1 从一次真实的抓取需求说起

前段时间我需要批量采集某招聘平台上的职位信息,包括职位名称、薪资范围、公司名称、工作地点、经验要求、学历要求以及职位详情的完整描述。乍一看这需求很普通,用 requests 加 BeautifulSoup 就能搞定,但实际打开页面一看就明白了——这活儿没那么简单。

该平台的职位列表页和详情页大量依赖 JavaScript 动态渲染,直接请求 HTML 拿到的是一堆空壳,关键数据全在接口返回后由前端拼装。更麻烦的是,列表页往下滚动才会加载更多职位卡片,典型的懒加载机制。如果只抓第一屏,数据量少得可怜。

我一开始用的是 Selenium,毕竟这是最成熟的浏览器自动化方案。但实际跑下来问题不少:一是速度慢,每次启动浏览器、等待元素渲染、切换页面都要消耗大量时间;二是资源占用高,开几个并发实例机器就吃不消了;三是反爬检测越来越严,Selenium 启动的浏览器带有明显的自动化特征,容易被识别。

后来在社区里看到有人推荐 DrissionPage,抱着试试看的心态用了一下,结果确实解决了我大部分的痛点。这篇文章就把我整个实操过程、踩过的坑、以及最终稳定运行的方案完整分享出来。

1.2 DrissionPage 到底解决了什么问题

DrissionPage 是一个基于 Python 的网页自动化工具,它的核心设计思路是把浏览器控制和请求发送两种模式融合在一起。你可以把它理解成一个既能像 requests 一样直接发请求、又能像 Selenium 一样操控浏览器的“双模”工具。

它的几个关键特性值得展开说:

第一,接管已打开的浏览器。这是我觉得最实用的功能。你可以手动打开一个浏览器,登录好账号,然后让 DrissionPage 接管这个浏览器实例。这样一来,登录态、Cookie、浏览器指纹都是真实的,反爬检测很难把你和正常用户区分开。

第二,无需 WebDriver。传统 Selenium 需要下载对应版本的 WebDriver 并配置路径,版本不匹配就报错。DrissionPage 直接通过调试协议与浏览器通信,省去了这层麻烦。

第三,语法简洁。它的元素定位语法比 Selenium 的 find_element 系列方法要直观得多,比如page.ele('@class=job-title')这种写法,上手很快。

第四,请求模式与浏览器模式无缝切换。对于不需要渲染的页面直接用请求模式,速度快;对于动态页面切到浏览器模式,灵活度高。

注意:DrissionPage 的版本更新比较频繁,不同版本 API 可能有差异。建议锁定一个稳定版本使用,不要盲目追新。

1.3 方案选型的对比分析

为了让你更清楚地理解为什么选 DrissionPage,我把几种常见方案做了个对比:

方案动态渲染支持反爬对抗速度资源占用上手难度
requests + BeautifulSoup不支持弱快低低
Selenium支持中慢高中
Playwright支持中中中中
DrissionPage支持强中快中低

从表格能看出来,DrissionPage 在反爬对抗和上手难度上有明显优势。对于招聘平台这种反爬策略比较激进的站点,接管真实浏览器的能力几乎是刚需。

2. 环境搭建与核心配置细节

2.1 安装与浏览器准备

安装本身很简单,一条命令的事:

pip install DrissionPage

但这里有个细节要注意:DrissionPage 需要本机安装 Chrome 或 Edge 浏览器。它默认会去找系统里的 Chrome,如果你用的是其他 Chromium 内核的浏览器,需要手动指定路径。

我建议单独准备一个浏览器用户目录,不要用你日常使用的那个。原因有两个:一是避免污染你正常的浏览数据,二是方便管理登录态。具体做法是在启动浏览器时指定一个独立的用户数据目录。

from DrissionPage import ChromiumOptions co = ChromiumOptions() co.set_user_data_path(r'D:\browser_data\recruit') co.set_local_port(9222)

这里的set_local_port指定了调试端口,后面接管浏览器的时候要用到。端口号可以随便选,只要不冲突就行,我习惯用 9222。

2.2 接管浏览器的正确姿势

接管浏览器这个操作,很多人第一次用会搞不清楚流程。正确的步骤是这样的:

第一步,先用命令行启动一个带调试端口的 Chrome:

chrome.exe --remote-debugging-port=9222 --user-data-dir="D:\browser_data\recruit"

第二步,在这个浏览器里手动打开目标网站,完成登录操作。招聘平台一般都需要登录才能看到完整的职位信息,有的还需要验证码。

第三步,在 Python 代码里接管这个浏览器:

from DrissionPage import ChromiumPage page = ChromiumPage(9222)

这样你就拿到了这个浏览器实例的控制权,后续所有操作都在这个已经登录的浏览器里进行。

提示:接管之前确保浏览器没有被其他程序占用调试端口。如果报连接错误,检查一下端口是否被占用,或者浏览器是否正常启动。

2.3 请求模式与浏览器模式的切换逻辑

DrissionPage 的一个核心优势是两种模式可以灵活切换。我的策略是这样的:

列表页用浏览器模式,因为需要滚动加载;详情页如果数据是通过接口返回的,可以尝试用请求模式直接拿 JSON 数据,速度会快很多。

# 浏览器模式获取列表 page = ChromiumPage(9222) page.get('https://example.com/jobs') # 切换到请求模式 from DrissionPage import SessionPage sp = SessionPage() sp.get('https://example.com/api/job/detail?id=123')

不过实际测试下来,该平台的详情页数据虽然来自接口,但接口带了签名参数,直接构造请求比较麻烦。所以我最终还是统一用了浏览器模式,稳定优先。

3. 列表页滚动加载与数据提取实战

3.1 滚动加载的触发机制

招聘平台的列表页通常是这样设计的:首屏加载一定数量的职位卡片,当你滚动到页面底部附近时,触发 AJAX 请求加载下一页数据,然后追加到列表末尾。

这里的关键是搞清楚它的触发条件。有的网站是监听 scroll 事件,判断滚动条位置;有的是用 IntersectionObserver 监听底部哨兵元素。不管哪种方式,你只需要模拟滚动到底部的行为就能触发加载。

DrissionPage 里滚动页面的方法有几种:

# 方式一:滚动到页面底部 page.scroll.to_bottom() # 方式二:按像素滚动 page.scroll.down(500) # 方式三:滚动到指定元素 page.scroll.to_see(ele)

我实测下来,scroll.to_bottom()最省事,但有时候加载有延迟,需要配合等待。

3.2 滚动加载的完整实现

单纯滚动一次是不够的,因为每次滚动只加载一批数据。你需要循环滚动,直到没有新数据加载为止。这里有个判断逻辑:记录当前页面上的职位卡片数量,滚动后等待一段时间,再统计一次,如果数量没变,说明已经到底了。

import time def scroll_to_load_all(page, card_locator='@class=job-card-wrapper'): last_count = 0 while True: # 统计当前卡片数量 cards = page.eles(card_locator) current_count = len(cards) # 数量没变化说明加载完毕 if current_count == last_count: break last_count = current_count page.scroll.to_bottom() time.sleep(2) # 等待加载 return page.eles(card_locator)

这段代码看起来简单,但有几个坑要注意:

坑一:等待时间不够。网络慢的时候,2 秒可能不够接口返回和 DOM 更新。我后来改成了轮询检测,每隔 0.5 秒检查一次数量变化,最多等 5 秒。

坑二:卡片定位不准确。有些网站的卡片 class 名是动态生成的,每次刷新都不一样。这时候需要用更稳定的属性来定位,比如@data-*属性或者层级关系。

坑三:滚动到底部后页面弹出了登录框或者验证码。这种情况需要人工介入处理,代码里要做好异常捕获。

3.3 职位卡片字段的提取

滚动加载完成后,就可以提取每个卡片上的信息了。以该平台为例,一个职位卡片通常包含以下字段:

  • 职位名称
  • 薪资范围
  • 公司名称
  • 工作城市和区域
  • 经验要求
  • 学历要求
  • 公司规模
  • 行业标签

提取的时候,我建议用相对定位而不是绝对定位。什么意思呢?就是先定位到卡片元素,然后在这个卡片内部查找子元素,而不是从整个页面去找。这样即使页面结构有微调,只要卡片内部结构不变,代码就不会挂。

for card in cards: job_name = card.ele('@class=job-name').text salary = card.ele('@class=salary').text company = card.ele('@class=company-name').text # ... 其他字段

实操心得:提取文本的时候,先打印几个样本看看格式。有的字段可能包含多余的空格、换行符,需要做清洗。比如薪资字段可能是“15-25K·13薪”这种格式,后续如果要分析,需要拆分处理。

3.4 数据存储的临时方案

在爬取过程中,我习惯先把数据存到一个列表里,最后统一写入文件。这样做的好处是万一中途出错,已经抓到的数据不会丢。

import csv all_jobs = [] # 爬取过程中 all_jobs.append({ 'job_name': job_name, 'salary': salary, 'company': company, # ... }) # 最后写入 CSV with open('jobs.csv', 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=all_jobs[0].keys()) writer.writeheader() writer.writerows(all_jobs)

注意encoding='utf-8-sig'这个细节,用 Excel 打开 CSV 的时候不会乱码。这是个小技巧,但很实用。

4. 详情页数据抓取与反爬对抗

4.1 详情页的打开方式

列表页只提供了职位的基本信息,完整的职位描述、任职要求、公司介绍等内容都在详情页。所以需要逐个打开详情页抓取。

打开详情页有两种方式:一是直接在当前标签页跳转,二是新开标签页。我推荐用新开标签页的方式,因为这样列表页的状态不会丢失,方便后续继续操作。

# 获取详情页链接 detail_url = card.ele('@class=job-name').attr('href') # 新开标签页 detail_page = page.new_tab(detail_url) # ... 抓取数据 detail_page.close() # 关闭标签页

这里有个性能优化的点:不要每抓一个就开关一次标签页,可以复用同一个标签页,只是每次跳转到新的 URL。这样能减少浏览器开销。

4.2 详情页字段的解析

详情页的信息比列表页丰富得多,通常包括:

  • 职位描述(工作内容)
  • 任职要求
  • 公司介绍
  • 工作地址
  • 福利待遇

这些内容一般在页面的特定容器里,用 DrissionPage 提取文本很方便:

job_desc = detail_page.ele('@class=job-detail-section').text

但要注意,有些内容可能是折叠的,需要点击“展开”按钮才能看到完整内容。这种情况要先模拟点击:

expand_btn = detail_page.ele('@class=expand-btn') if expand_btn: expand_btn.click()

4.3 反爬检测的应对策略

招聘平台的反爬策略主要有以下几种:

频率限制。短时间内大量请求会触发限流,表现为返回 403 或者弹出验证码。应对方法是控制请求频率,在每次请求之间加随机延迟。

import random import time time.sleep(random.uniform(1.5, 3.5))

行为检测。平台会分析你的操作行为是否符合人类特征。比如鼠标移动轨迹、点击位置、滚动速度等。DrissionPage 接管真实浏览器的优势在这里就体现出来了,因为浏览器本身就是真实的,行为特征天然接近人类。

指纹检测。通过 JavaScript 检测浏览器的一些特征,比如 navigator.webdriver 属性。Selenium 启动的浏览器这个属性是 true,很容易被识别。DrissionPage 接管手动打开的浏览器,这个属性是正常的。

验证码。这是最麻烦的。我的策略是:控制频率尽量避免触发验证码;如果触发了,就暂停程序,手动处理后再继续。

注意:不要试图用自动化方式绕过验证码,这既不道德也可能违反平台规则。合理的做法是控制采集频率,尊重平台的负载能力。

4.4 异常处理与断点续爬

爬取过程中难免遇到各种异常:网络超时、元素找不到、页面结构变化等。良好的异常处理能让程序更健壮。

def safe_get_detail(page, url, retry=3): for i in range(retry): try: page.get(url) time.sleep(random.uniform(1, 2)) return page except Exception as e: print(f'第{i+1}次尝试失败: {e}') time.sleep(3) return None

断点续爬的思路是:把已经抓取的职位 ID 记录到一个文件里,每次启动时先读取这个文件,跳过已抓取的。

import os done_file = 'done_ids.txt' done_ids = set() if os.path.exists(done_file): with open(done_file, 'r') as f: done_ids = set(f.read().splitlines()) # 抓取时判断 if job_id in done_ids: continue # 抓取成功后记录 with open(done_file, 'a') as f: f.write(job_id + '\n')

这个机制在长时间运行时特别有用,万一程序崩溃或者需要暂停,下次可以从断点继续。

5. 常见问题排查与避坑指南

5.1 元素定位失败的排查思路

元素定位失败是最常见的问题,原因可能有很多。我总结了一个排查流程:

第一步,确认页面是否加载完成。有时候代码跑太快,元素还没渲染出来。加个等待或者用 DrissionPage 的等待方法:

ele = page.ele('@class=job-name', timeout=10)

第二步,确认定位表达式是否正确。可以在浏览器的开发者工具里用 Ctrl+F 搜索一下,看看能不能找到对应的元素。

第三步,确认是否在 iframe 里。有些页面的部分内容嵌套在 iframe 中,需要先切换进去:

iframe = page.get_frame('@id=content-frame') ele = iframe.ele('@class=job-name')

第四步,确认是否是动态 class。如果 class 名包含随机字符串,需要用其他属性定位。

5.2 滚动加载不触发的解决方案

有时候滚动到底部了,但新数据就是加载不出来。可能的原因:

  • 滚动速度太快,页面还没反应过来
  • 需要滚动到特定元素而不是页面底部
  • 加载触发的条件不是滚动,而是点击“加载更多”按钮

针对第一种情况,可以在滚动后加一个短暂的等待,或者分多次小幅度滚动。

针对第二种情况,找到那个触发加载的元素,用scroll.to_see()滚动到它。

针对第三种情况,直接定位按钮并点击:

load_more = page.ele('@class=load-more-btn') if load_more: load_more.click()

5.3 数据提取中的编码与格式问题

提取到的文本可能包含各种格式问题:

问题类型表现解决方法
多余空白文本前后有空格或换行用 strip() 清洗
特殊字符包含 \xa0 等不可见字符用 replace 替换
编码错误中文显示为乱码确保文件写入用 utf-8
格式不统一薪资有的写“15K”有的写“1.5万”写解析函数统一处理

薪资字段的处理尤其麻烦,我写了个简单的解析函数:

import re def parse_salary(salary_str): # 处理 "15-25K" 格式 match = re.search(r'(\d+)-(\d+)K', salary_str) if match: return int(match.group(1)), int(match.group(2)) # 处理其他格式... return None, None

5.4 程序运行稳定性优化

长时间运行爬虫,稳定性很重要。我踩过的坑包括:

内存泄漏。长时间运行后内存占用越来越高。解决方法是定期重启浏览器,或者及时关闭不需要的标签页。

连接断开。浏览器可能因为各种原因断开连接。加个心跳检测,发现断开就重新接管。

日志记录。一定要打日志,不然出了问题都不知道哪里错了。我用的是 Python 的 logging 模块,把关键操作和异常都记录下来。

import logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', filename='spider.log' )

6. 关于合规与效率的几点个人体会

做数据采集这些年,我越来越觉得合规意识比技术本身更重要。技术手段再高明,如果越过了边界,最终害的是自己。

控制采集频率是最基本的。我一般会把请求间隔设置在 2 秒以上,高峰期甚至会更长。这既是对目标网站的尊重,也是保护自己不被封禁的有效手段。很多人追求速度,把并发开到几十上百,结果没跑多久 IP 就被封了,反而得不偿失。

数据用途也要想清楚。采集公开的职位信息用于个人分析研究,和批量采集用于商业竞争,性质是完全不同的。前者一般没问题,后者可能涉及法律风险。我个人的原则是:只采集自己真正需要的数据,不囤积、不转卖。

另外,平台的规则要遵守。robots.txt 虽然不具有法律强制力,但它表达了网站运营者的意愿。在合理范围内尊重它,是行业良性发展的基础。

从技术角度说,DrissionPage 这类工具确实降低了自动化采集的门槛,但工具本身是中性的,怎么用取决于人。我分享这些经验,是希望帮助有正当需求的朋友提高效率,而不是鼓励滥用。

最后说个实际的小技巧:如果你需要长期采集某个平台的数据,与其每次都重新登录、重新配置,不如维护一个稳定的浏览器环境,把登录态保持住。这样每次启动程序就能直接开始工作,省去了很多重复操作。我现在就是用一个专门的浏览器用户目录,里面保存了各个平台的登录状态,需要的时候直接接管就行,效率提升非常明显。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 17:30:01

手势识别康复系统:用MediaPipe关键点实现动作计数与质量评估

简介:面向手部康复与计算机视觉应用研究者,这份PDF是发表于《计算机测量与控制》2021年第7期的学术论文,提出基于计算机视觉的手势识别康复系统,针对传统康复器械功能单一、训练枯燥、恢复缓慢等问题,给出图像采集、分…

作者头像 李华
网站建设 2026/9/29 17:29:55

Python快速复习指南:高频语法与实用库一次梳理

很多朋友在面试前、考试前、或者要接手一个已有Python项目时,都会突然面临“快速复习Python”的需求。所谓“复习”,大概率不是想从零开始系统学一遍,而是想用最短的时间,把那些平时写业务代码经常用到、但一段时间没动手就会手生…

作者头像 李华
网站建设 2026/9/29 17:29:52

低显存跑大模型Agent:量化与混合卸载把5.9GB压到2.7GB

如果只给我一张6GB显存的老显卡,一个5.9GB体积的模型权重,还要拿来跑自养Agent,八成的人第一反应是:没戏。但这周我把这事干成了——模型照常跑,Agent照常用,推理峰值显存只有2.7GB,生成速度还稳…

作者头像 李华
网站建设 2026/9/29 17:29:13

Anthropic RSI方法论落地拆解:四循环协同与安全护栏设计

Anthropic 公开了内部 RSI 落地方法论。看到这条消息时,我第一反应是:这群人终于把“自己改自己”的代码路径讲明白了。作为长期做 AI 训练和推理系统的工程师,我太清楚 RSI(Recursive Self-Improvement,递归自我改进&…

作者头像 李华
网站建设 2026/9/29 17:29:03

分布式强化学习Checkpoint Engine设计指南:同步保存与故障恢复实践

凌晨三点,训练跑到了第 14 个小时,机房例行维护把一个采集节点踢下线了。放在两年前我大概只会骂一句然后手动重启流程,但那次不一样——我手头是一个分布式 PPO 任务,挂了的不是一个 Python 进程,而是整个训练循环里&…

作者头像 李华
网站建设 2026/9/29 17:29:02

创维E900-S高安版刷机教程:短接强刷+当贝桌面替换电信原厂系统

创维E900-S高安版刷机实录:用当贝桌面干掉电信原厂系统 先别急着动手,凡是玩机顶盒刷机的,都有一个共识:高安版不是普通版,普通版能用的刷机包放到高安版上大概率变砖。创维E900-S这机器,电信定制版&#x…

作者头像 李华