做爬虫这件事,我从requests入坑,后来被Selenium折磨过,再后来遇到DrissionPage,才算是把“采集网页数据”这件事真正理顺了。先说结论:如果你要对付的是需要登录、数据由JS动态渲染、或者带着各种反爬策略的网站,那么DrissionPage可能是近些年最值得上手的库之一。它最猛的地方在于,把HTTP请求(requests)和浏览器自动化(类似于Selenium)统一成同一套API,你可以在浏览器模式和requests模式之间无缝切换,登录态、cookie、localStorage全都不用自己手动去抠。
我上一份工作里有个数据看板,必须在登录之后点半天筛选条件才能看到汇总表格。早期用requests模拟Ajax接口,被前端的一个签名参数折腾到怀疑人生;换用Selenium倒是能点击了,可网站有webdriver检测,登录状态也总是莫名其妙地丢。后来换成DrissionPage,整个采集脚本从两百多行缩到八十多行,效率还翻了一倍。这篇文章就把我在这几次实战里的一些经验和踩过的坑整理出来,给正在纠结爬虫方案的朋友一个参考。
1. 为什么我把requests和Selenium都先放到一边
1.1 requests请求库的“天真”之处
requests本身是一个非常优秀的HTTP库,轻量、快速、语义清晰,我在很多简单场景下依旧用它。可一旦目标网站复杂起来,它的问题就暴露得很明显:
- 页面数据如果是JS异步加载的,requests拿到的只是空壳HTML,抓不到实际内容。
- 登录后的接口往往带签名、加密参数、时间戳,甚至每次请求的header都不同,靠手工抓包去逆向,成本高还不稳定。
- cookie一旦过期就得重新模拟一遍登录流程,中间只要漏掉一个header字段,接口就给你返回401。
我举个例子,之前想抓一个平台的后台报表数据,浏览器Network里明明能看到一个返回JSON的接口,但用requests一访问,对方直接返回“签名错误”。我把所有header都复刻过去了还是不行,后来才发现它在前端页面里用一段加密JS生成了动态token,而这个token只存在于浏览器环境里。requests再厉害,也没办法替你执行那段混淆后的JS。
1.2 Selenium的笨重与暴露感
Selenium的价值在于它是“真浏览器”,用户看到什么它就拿到什么。但它身上有几个让我不太舒服的点:
- 环境配置繁琐。Chrome版本一升级,selenium还要重新下载匹配的driver,CI环境里经常出现“session not created”这类报错。
- 特征暴露太明显。
navigator.webdriver这个属性一测一个准,很多网站就靠这个特征拦住自动化脚本。 - 代码写起来相对啰嗦。查找元素要写
find_element(By.ID, "xxx"),各种显式等待、隐式等待混在一起,维护起来很痛苦。 - 资源占用大。每个Selenium会话本质都是拉起一个完整浏览器,如果同时跑几十个任务,服务器内存直接告急。
而DrissionPage给我的感觉是,它把requests和Selenium各自的长处都留下来了,又把各自的短板基本补齐了。它既可以直接驱动本机的Chromium浏览器完成“真人操作”,也可以切换到requests模式去高频请求接口,两者之间还能互相转手。
1.3 DrissionPage的设计哲学
这个库的核心设计很简单:统一。它把“元素查找”这件事从底层重做了一套语法,让requests模式的SessionPage和浏览器模式的ChromiumPage共用同一套API。你学了ele()怎么用,就同时会在两种模式下找元素了。
另一个贴合实际的设计是,ChromiumPage在启动时可以直接接管你本机已有的Chrome浏览器配置文件,也就是说,你平时登录过的网站、保持的登录态,它都能一口气继承过来。这一点在需要登录的采集场景里太实用了——不需要自己在代码里走一遍复杂登录,直接复用浏览器里已有的session就行。
2. 核心概念与整体设计思路拆解
2.1 SessionPage与ChromiumPage
DrissionPage里有两大核心类。我先用最简单的方式解释它们:
- SessionPage:底层基于requests实现,本质是HTTP请求封装。它速度快、占用资源小,适合抓取接口、静态页面、纯数据读取。
- ChromiumPage:底层通过DevTools协议控制Chromium内核浏览器,本质是浏览器自动化。它能渲染JS、点击按钮、填写表单,适合对付动态网页和需要真实交互的场景。
这两个类不是孤立存在的。DrissionPage通过一个跨模式的元素语法,让SessionPage和ChromiumPage之间的代码风格保持高度一致。更关键的是ChromiumPage对象可以调用change_mode()方法,直接转换成SessionPage模式,同时把浏览器的cookie、user-agent等身份信息一并带过去。这样一来,你完全可以让浏览器先完成登录、滑块验证、点击操作这些“高难度动作”,然后立刻切到requests模式去批量拉数据,减少浏览器进程的压力。
from DrissionPage import ChromiumPage # 浏览器模式:完成需要真实环境的操作 page = ChromiumPage() page.get('https://example.com/login') page.ele('@name=username').input('demo_user') page.ele('@name=password').input('DemoPass123') page.ele('@type=submit').click() # 切换到requests模式:继续抓接口数据 session_page = page.change_mode() data = session_page.get('https://example.com/api/user/info').json print(data)上面的代码里,登录操作发生在浏览器环境,切换后发起HTTP请求时,登录cookie已经自动附带上了。放在以前,你得手动从浏览器DevTools里把cookie字符串复制出来,再拼到requests的header里,操作繁琐且易错。
2.2 一套好记的元素查找语法
Selenium里查找元素要写一长串find_element,而DrissionPage把查找动作浓缩成了ele()和eles()方法,配合字符串表达式。常见的定位写法有:
| 定位方式 | 表达式示例 | 说明 |
|---|---|---|
| 按标签 | tag:h1 | 找第一个h1标签 |
| 按id | id:username | 找id为username的元素 |
| 按class | class:title | 找class含title的元素 |
| 按文本 | text:登录 | 找文本为“登录”的元素 |
| 按属性 | @name=username | 找name属性为username的元素 |
| CSS选择器 | css:#main .data | 用CSS语法定位 |
| XPath | xpath://div[@id="main"] | 用XPath定位 |
多个条件还可以组合:ele('@id=user@@text:张三')表示同时满足两个条件,ele('@class=a||@class=b')表示满足其中任意一个即可,属于and和or的语义。这种写法比Selenium的条件链简洁很多,在日常抓取中真的能节省不少代码量。
2.3 浏览器自动化的细节能力
ChromiumPage除了可以像Selenium那样点击、输入、滚动之外,还支持不少有意思的特性:
- 多标签页管理:可以打开多个标签页,按标签对象切换,相当于操作一个真正的浏览器窗口。
- iframe直接进入:用
get_frame()方法切入frame内部,不用像selenium那样来回switch_to.frame。 - 监听网络请求:可以用
listen方法捕获页面发起的接口请求,用来分析数据来源。 - 文件下载处理:可以直接接管浏览器下载,保存到指定目录。
- 执行JS脚本:需要绕开某些限制时,可以调用
run_js()直接注入代码。
这些能力让DrissionPage不只是爬虫库,做Web自动化测试也很顺手。
3. 实操过程:一个需要登录的报表采集项目
3.1 场景设定与环境准备
假设我要采集一个后台系统的销售报表数据。流程是这样的:访问登录页,输入账号密码,点击登录,进入报表页面,筛选日期,读取表格,最后把数据保存到Excel。这个场景里,页面是动态渲染的,接口也加了签名校验,用requests直接抓接口是行不通的。
先安装DrissionPage:
pip install DrissionPage这个库不要求你额外下载driver,前提是电脑上装了Chrome或者Edge浏览器。我第一次用的时候还挺惊讶的,安装完就能启动浏览器,完全没有Selenium那种“版本不匹配”的烦恼。如果你的Chrome安装在非默认位置,或者有特殊参数需求,可以用ChromiumOptions来做配置。
3.2 第一步:启动浏览器并打开登录页
from DrissionPage import ChromiumPage page = ChromiumPage() page.get('https://example.com/login')这一步会弹出一个真实的Chrome窗口,而且默认不带自动化特征标识。和我以前用Selenium时那种“一启动就被网站认出来”的体验完全不同。
有一点需要注意:ChromiumPage()直接启动时会自动使用临时用户目录,登录状态不会保存到本地。要做到“登录一次,后续复用”,可以指定一个固定的用户数据目录:
from DrissionPage import ChromiumOptions, ChromiumPage co = ChromiumOptions() co.set_user_data_path('./my_user_data') # 固定用户目录 page = ChromiumPage(co) page.get('https://example.com/login')第一次运行到这里时,你手动在弹出来的浏览器窗口里登录一次,之后cookie和登录状态都会保存在./my_user_data目录下。下次运行脚本,打开就是已登录状态,连代码里的登录逻辑都可以省掉。
3.3 第二步:表单填写与登录触发
大多数网站的登录表单无非是用户名、密码、登录按钮,DrissionPage的写法很直观:
page.ele('@name=username').clear().input('demo_user') page.ele('@name=password').clear().input('DemoPass123') page.ele('@type=submit').click()input()方法会先清空原有内容再输入,免得表单里有默认值导致输入错误。点击登录按钮之后,我习惯加一句等待加载完成的逻辑:
page.wait.load_start()这条语句会阻塞到页面跳转完成,避免下一步操作时元素还没渲染出来。
3.4 第三步:进入报表页并抓取表格数据
登录成功后,通过URL直接跳到报表页面:
page.get('https://example.com/report') page.wait.ele_displayed('css:table.data-table')这里用了一个显式等待:等到table.data-table出现且可见,再往下执行。相比固定的sleep(5),这种等待方式既高效又稳妥。
读取表格所有行:
rows = page.eles('css:table.data-table tbody tr') for row in rows[:5]: cells = row.eles('tag:td') values = [cell.text.strip() for cell in cells] print(values)row.eles('tag:td')是查找当前行内所有td单元格。注意这里用的查找方法是在元素对象上调用的,而不是在page上调用的,DrissionPage的元素对象自带查找能力,这个设计让层级遍历变得非常自然。
3.5 第四步:数据落盘成Excel
抓到的数据可以直接转成DataFrame,再保存为Excel。我在本地测试时用的pandas版本是2.x,导出Excel需要openpyxl库,一并装好就行。
import pandas as pd all_rows = [] for row in rows: cells = row.eles('tag:td') all_rows.append([cell.text.strip() for cell in cells]) df = pd.DataFrame(all_rows) df.to_excel('report.xlsx', index=False, engine='openpyxl') print(f'共导出 {len(df)} 行数据')整个采集脚本跑下来,从启动浏览器到Excel生成,也就几十秒。放在以前用requests逆向接口加签名的方案里,这个过程可能要排查一整天。
3.6 进阶:隐藏数据接口的监听与直采
很多报表页面的表格数据其实是Ajax接口返回的,页面只是把JSON渲染成了表格。如果接口数据结构非常规整,我更推荐直接监听接口请求,然后解析JSON,比每次读表格再清洗HTML效率高得多。
DrissionPage可以用listen方法捕获网络请求:
page.listen.start('api/report') # 开始监听url中包含api/report的请求 page.get('https://example.com/report') # 刷新页面,触发接口请求 resp = page.listen.wait() # 等待捕获 json_data = resp.response.body这里的思路是:先开始监听,再触发页面操作,拿到接口返回的JSON,直接按字段提取。我在实际项目中,经常用这种方式跳过HTML解析步骤,让数据采集精准命中数据源头。不过要注意,接口请求的URL或参数有时候会带随机化前缀,监听条件需要写得宽一点,拿到响应后再在代码里做过滤。
4. 常见问题与排查技巧实录
4.1 一个问题和对应排查思路的速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 启动报错,找不到浏览器 | 本机没有Chrome/Edge,或浏览器路径非常规 | 用ChromiumOptions指定browser_path |
| 登录后重启脚本又掉登录态 | 使用了临时用户数据目录 | 设置固定的user_data_path |
| 元素一直定位不到 | 页面还没加载完,或元素在iframe里 | 用wait.ele_displayed,或get_frame()进入iframe再查找 |
| 点击按钮没反应 | 元素被遮挡或滚动条没到位 | 先scroll_to_see到元素,再执行点击 |
| 页面提示检测到自动化脚本 | 网站做了爬虫特征检测 | 使用真实用户目录,不要用headless模式,避免并发过高加随机延迟 |
| 切换到requests模式后接口仍报错 | cookie带过去了但某些header缺失 | 检查接口必须的X-CSRF-Token、Referer等字段,手动补充 |
4.2 定位不到元素时不要盲目加延时
新手最容易踩的坑是一旦元素找不到,就在代码里疯狂加sleep(3),结果不是慢就是还报错。DrissionPage提供了一套wait方法来处理这类问题,我实测下来最常用的是两个:
page.wait.ele_displayed('@id=result') # 等待某个元素可见 page.wait.load_start() # 等待页面加载启动如果用了wait还是找不到,优先考虑两个方向:一是元素在iframe里,需要先切入frame;二是页面发生了弹窗遮挡,需要先关闭或绕过。定位问题前,先用page.html把当前页面源码输出到本地文件,确认元素到底在不在DOM里。这一步能帮你筛掉大部分错觉。
4.3 浏览器被识别的问题
尽管DrissionPage比Selenium低调很多,但大型网站的反爬策略一直在升级。如果你的脚本被识别了,第一个反应不应该是找更隐蔽的工具,而是反思自己的行为模式:是不是请求频率太猛?是不是启动参数暴露了特征?是不是用户目录太干净、没有任何历史记录?
我常用的方法是给请求之间加随机小延迟,行为尽量模拟真人:
import random import time for page_num in range(1, 20): page.get(f'https://example.com/report?page={page_num}') # 解析数据... time.sleep(random.uniform(1.5, 3.5))这样虽然慢一点,但稳定性高很多。爬虫采集的本质是和数据源博弈,速度和安全是一对矛盾,你得根据项目规模做取舍。
4.4 我对这套方案的整体感受
DrissionPage真正打动我的不是某个单独功能,而是它把“浏览器身份”和“HTTP速度”缝合在了一起。遇到需要登录的网站,我现在的标准操作是:先用浏览器模式登录并把cookie持久化到用户目录,然后切到requests模式进行批量请求,必要时用listen监听关键的接口响应。这套组合拳下来,几乎能覆盖我工作中遇到的大部分采集需求。
如果你手头正好有一个需要登录才能看到数据、又不想逆向繁琐加密逻辑的项目,我建议你从本文的登录示例开始跑一遍,哪怕只是抓一个无关紧要的标题元素,把环境跑通的感觉也很有价值。做爬虫这件事,工具永远不是最难的,难的是你愿意花一个下午把流程彻底摸透。DrissionPage至少帮你把最脏最累的那部分活给省了大半。