拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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

1. 为什么我最终选了 DrissionPage 而不是 Selenium1.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(classjob-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(rD:\browser_data\recruit) co.set_local_port(9222)这里的set_local_port指定了调试端口后面接管浏览器的时候要用到。端口号可以随便选只要不冲突就行我习惯用 9222。2.2 接管浏览器的正确姿势接管浏览器这个操作很多人第一次用会搞不清楚流程。正确的步骤是这样的第一步先用命令行启动一个带调试端口的 Chromechrome.exe --remote-debugging-port9222 --user-data-dirD:\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?id123)不过实际测试下来该平台的详情页数据虽然来自接口但接口带了签名参数直接构造请求比较麻烦。所以我最终还是统一用了浏览器模式稳定优先。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_locatorclassjob-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(classjob-name).text salary card.ele(classsalary).text company card.ele(classcompany-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, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnamesall_jobs[0].keys()) writer.writeheader() writer.writerows(all_jobs)注意encodingutf-8-sig这个细节用 Excel 打开 CSV 的时候不会乱码。这是个小技巧但很实用。4. 详情页数据抓取与反爬对抗4.1 详情页的打开方式列表页只提供了职位的基本信息完整的职位描述、任职要求、公司介绍等内容都在详情页。所以需要逐个打开详情页抓取。打开详情页有两种方式一是直接在当前标签页跳转二是新开标签页。我推荐用新开标签页的方式因为这样列表页的状态不会丢失方便后续继续操作。# 获取详情页链接 detail_url card.ele(classjob-name).attr(href) # 新开标签页 detail_page page.new_tab(detail_url) # ... 抓取数据 detail_page.close() # 关闭标签页这里有个性能优化的点不要每抓一个就开关一次标签页可以复用同一个标签页只是每次跳转到新的 URL。这样能减少浏览器开销。4.2 详情页字段的解析详情页的信息比列表页丰富得多通常包括职位描述工作内容任职要求公司介绍工作地址福利待遇这些内容一般在页面的特定容器里用 DrissionPage 提取文本很方便job_desc detail_page.ele(classjob-detail-section).text但要注意有些内容可能是折叠的需要点击“展开”按钮才能看到完整内容。这种情况要先模拟点击expand_btn detail_page.ele(classexpand-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, retry3): for i in range(retry): try: page.get(url) time.sleep(random.uniform(1, 2)) return page except Exception as e: print(f第{i1}次尝试失败: {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(classjob-name, timeout10)第二步确认定位表达式是否正确。可以在浏览器的开发者工具里用 CtrlF 搜索一下看看能不能找到对应的元素。第三步确认是否在 iframe 里。有些页面的部分内容嵌套在 iframe 中需要先切换进去iframe page.get_frame(idcontent-frame) ele iframe.ele(classjob-name)第四步确认是否是动态 class。如果 class 名包含随机字符串需要用其他属性定位。5.2 滚动加载不触发的解决方案有时候滚动到底部了但新数据就是加载不出来。可能的原因滚动速度太快页面还没反应过来需要滚动到特定元素而不是页面底部加载触发的条件不是滚动而是点击“加载更多”按钮针对第一种情况可以在滚动后加一个短暂的等待或者分多次小幅度滚动。针对第二种情况找到那个触发加载的元素用scroll.to_see()滚动到它。针对第三种情况直接定位按钮并点击load_more page.ele(classload-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, None5.4 程序运行稳定性优化长时间运行爬虫稳定性很重要。我踩过的坑包括内存泄漏。长时间运行后内存占用越来越高。解决方法是定期重启浏览器或者及时关闭不需要的标签页。连接断开。浏览器可能因为各种原因断开连接。加个心跳检测发现断开就重新接管。日志记录。一定要打日志不然出了问题都不知道哪里错了。我用的是 Python 的 logging 模块把关键操作和异常都记录下来。import logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, filenamespider.log )6. 关于合规与效率的几点个人体会做数据采集这些年我越来越觉得合规意识比技术本身更重要。技术手段再高明如果越过了边界最终害的是自己。控制采集频率是最基本的。我一般会把请求间隔设置在 2 秒以上高峰期甚至会更长。这既是对目标网站的尊重也是保护自己不被封禁的有效手段。很多人追求速度把并发开到几十上百结果没跑多久 IP 就被封了反而得不偿失。数据用途也要想清楚。采集公开的职位信息用于个人分析研究和批量采集用于商业竞争性质是完全不同的。前者一般没问题后者可能涉及法律风险。我个人的原则是只采集自己真正需要的数据不囤积、不转卖。另外平台的规则要遵守。robots.txt 虽然不具有法律强制力但它表达了网站运营者的意愿。在合理范围内尊重它是行业良性发展的基础。从技术角度说DrissionPage 这类工具确实降低了自动化采集的门槛但工具本身是中性的怎么用取决于人。我分享这些经验是希望帮助有正当需求的朋友提高效率而不是鼓励滥用。最后说个实际的小技巧如果你需要长期采集某个平台的数据与其每次都重新登录、重新配置不如维护一个稳定的浏览器环境把登录态保持住。这样每次启动程序就能直接开始工作省去了很多重复操作。我现在就是用一个专门的浏览器用户目录里面保存了各个平台的登录状态需要的时候直接接管就行效率提升非常明显。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门