我做了三年多的数据采集,从最初的 requests 到后来的 selenium,再到现如今的 Playwright,可以说把反爬这个坑踩了个遍。尤其是用 selenium 采集数据,很多人觉得“浏览器操作嘛,跟真实用户一样,应该不会被封”,但实际上,selenium 的自动化特征太明显了,稍微有点防护的网站一眼就能识别出来。这篇博文我就结合自己的实战经历,把 selenium 采集数据时怎么应对反爬机制这件事,从底层原理到具体代码,再到踩坑记录,一次讲透。
先说下适合什么人看:刚接触爬虫、想用 selenium 做简单数据采集的朋友;正在被各种验证码、IP 封锁、WebDriver 检测搞得焦头烂额的开发者;还有需要在公司或者个人项目里搭建一套稳定采集体系、但又不想频繁被反爬策略打断的工程师。你不需要有多高深的算法基础,只要会 python 基础语法、懂一点 selenium 的基本操作,今天的内容都能跟得上。
1. 反爬机制的本质与 Selenium 的“原罪”
1.1 网站为什么要反爬,以及反爬的底层逻辑
在我刚开始做采集的时候,我一直想不通一件事:我不过是打开网页看了下公开数据,为什么网站要费这么大劲挡我?后来想明白了,反爬的根源不在“访问”本身,而在于“访问模式”。正常的用户一天打开一个网站大概几十次,每次浏览几页到几十页,中间还夹杂着阅读、思考、鼠标滑动这些动作。而你的 selenium 脚本一旦跑起来,每秒钟可能发出好几个请求,每个请求之间的间隔几乎固定,浏览行为也高度模板化。网站的反爬系统要做的事情,就是在海量请求里找出那些“不像人”的流量,然后对其进行限制。
这个判断过程通常分为几个层级:第一层是请求头检测,检查 User-Agent、Accept-Language 这些字段是不是常见浏览器的标准配置;第二层是 IP 维度的频率统计,比如同一个 IP 在一分钟内访问了多少个页面、是不是均匀地访问;第三层是行为分析,检测鼠标轨迹、点击位置、滚动速度、键盘事件这些真实用户才有的细节;最后一层是更复杂的检测,比如浏览器指纹、WebDriver 标记、Canvas 指纹等等。对于 selenium 来说,最容易被抓的就是第三层和最后一层,因为你的脚本再怎么模拟,底层还是有自动化特征。
1.2 Selenium 为什么容易被识别:几个关键特征
Selenium 之所以被很多网站轻松识别,主要是因为它会在浏览器里留下几个非常明显的“小尾巴”。最典型的几个特征包括:
window.navigator.webdriver属性值为true。正常浏览器这个值是undefined,但 selenium 启动的浏览器默认会把它设为true。网站只需要在页面里插入几行 JS 检查这个属性,就能确定你是不是在用自动化工具。navigator.plugins和navigator.languages的数值异常。部分环境里,selenium 启动的浏览器插件数组长度是 0,语言配置也不够完整。chrome.cdc_前缀的变量。这是 chromedriver 在浏览器里注入的调试命令对象,懂行的人扫一下就可以检测到。- 时间行为特征。脚本请求之间的间隔可能是毫秒级,甚至并发打开多个浏览器实例,真实用户很难做到。
很多人以为伪装一下 User-Agent 就万事大吉了,实际上这只能应付最基础的防御。要想真正避开反爬,必须从这几个特征下手。
2. 基础防线:请求伪装与浏览器指纹优化
2.1 从 User-Agent 到完整请求头
我在早期做采集时,第一个改进就是给 selenium 的浏览器实例设置一个完整的请求头,而不是只改 User-Agent。因为在大多数浏览器里,请求头是一个整体,而 UA 只是其中的一部分。如果只有 UA 是 Chrome 的,但 Accept-Language 却是空的,或者其他字段不像真实浏览器,照样会被判定为异常。
具体的做法是在 selenium 中使用 Chrome DevTools Protocol 的Network.setExtraHTTPHeaders方法,或者直接通过ChromeOptions添加参数。这里我给出一个最简单的配置示例:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--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") options.add_argument("--lang=zh-CN") options.add_argument("--accept-lang=zh-CN,zh;q=0.9,en;q=0.8") driver = webdriver.Chrome(options=options)这里需要注意,设置 UA 的时候尽量模拟最新版的 Chrome 或 Edge,不要用太旧的版本,因为网站端会统计浏览器版本的使用比例,太老的反而不真实。
2.2 干掉 WebDriver 标记的几种思路
真正让 selenium 区分于普通浏览器的是前面提到的webdriver属性。我测试过很多方法,最简单的有两种:一种是用undetected-chromedriver这个库,它内部已经帮你处理了大部分自动化特征;另一种是手动执行 JS 脚本改写属性。我个人更推荐前者,因为它的维护更频繁,适配性也更好。
undetected-chromedriver的使用方式和原版 selenium 差不多,只需要改一下导入方式就行:
import undetected_chromedriver as uc driver = uc.Chrome() driver.get("https://example.com")这个库会修改 chromedriver 的源码,让navigator.webdriver变成undefined,同时还会模拟更多浏览器行为,实测下来通过率比原版高不少。但也不是百分百,遇到比较厉害的指纹检测网站,还需要配合别的方案。
2.3 代理 IP 与流量控制,但要注意合规
IP 被封锁是采集过程中最让人的头疼的问题之一。尤其是在跑大规模任务的时候,单 IP 请求频率稍微高一点,就会触发服务器端的黑名单策略。解决这个问题的方法通常是准备一批代理 IP,并在每次创建浏览器实例时轮换使用。市面上有很多代理服务提供商,既有数据中心代理,也有住宅代理,住宅代理的伪装程度更高,但价格也贵。从成本角度出发,如果只是学习或小规模采集,用少量数据中心代理把请求频率控制在合理范围内就够了。
需要特别强调一句:使用代理 IP 本身并不违法违规,但前提是你采集的数据是公开的、合法的,并且你没有对目标网站造成过大的访问压力。爬虫的边界在于“爬得合规”,而不是“爬得猛”。如果你用代理去暴力绕过封禁、大批量抓取用户隐私数据,那就越界了,风险巨大。
3. 行为模拟:让脚本像人一样“磨蹭”
3.1 随机等待与人性化操作
很多初学者采集失败,往往不是因为反爬检测有多强,而是因为脚本的动作太“急”。比如请求页面之后立即点击下一个按钮,中间完全没有思考停顿,或者每步操作的时间间隔恒定不变。真实用户不可能这么精准,所以网站的反爬系统会盯上这种规律性。
我在写采集逻辑的时候,通常会在每个关键动作之间加入随机延时。这里说的随机延时,不只是time.sleep(random.uniform(1, 3))这么简单,而是要模拟不同场景下的思考时间。比如:
- 打开页面后,先停顿 1~2 秒,模拟阅读内容;
- 滚动页面时,分好几次滚动,不要一次滚到底;
- 点击按钮前,把鼠标先移动到按钮附近再点击;
- 填写表单时,按一定的输入速度模拟打字。
下面是一个简单的封装:
import random import time def human_pause(): time.sleep(random.uniform(1, 3)) def scroll_page(driver): for i in range(random.randint(3, 6)): driver.execute_script(f"window.scrollBy(0, {random.randint(400, 800)});") time.sleep(random.uniform(0.5, 1.5))这个函数看起来简单,实际上能非常有效地降低被行为分析系统识别的概率。我试过,不加这个逻辑跑 200 条数据就出验证码,加了之后能跑几千条都没问题。
3.2 鼠标轨迹与页面事件模拟
除了延时,鼠标轨迹也是行为检测的重要部分。真实用户在屏幕上移动鼠标时,轨迹通常是不规则的曲线而不是直线。如果用ActionChains直接把鼠标从一个点移到另一个点,很容易被检测到。更好的做法是分段移动,在移动的过程中加入一些随机偏转。
这里分享一个我一直在用的方法:通过pyautogui或者ActionChains配合随机算法模拟鼠标移动。以 Selenium 的ActionChains为例,先让鼠标移动到某个位置,再小步移动到目标位置:
from selenium.webdriver.common.action_chains import ActionChains def human_mouse_move(driver, element): actions = ActionChains(driver) actions.move_to_element(element) actions.pause(random.uniform(0.2, 0.5)) actions.click() actions.perform()如果你的页面需要验证鼠标轨迹,比如滑块验证码,那纯靠ActionChains拖动是不够的,因为轨迹太笔直。更靠谱的方案是先根据曲线方程生成一组坐标点,再逐步移动。下面这个函数是我之前处理滑块时用的:
import numpy as np def generate_track(distance): track = [] current = 0 mid = distance * 3 / 4 t = 0.2 while current < distance: if current < mid: a = random.uniform(2, 4) else: a = -random.uniform(2, 4) v = a * t current += v * t track.append(round(current, 2)) return track然后使用ActionChains按照轨迹逐步拖动滑块。这么做不保证一定能通过所有验证码,但对于常见的前端滑块验证,成功率能达到 80% 以上。不过要提醒大家,对付验证码的核心思路是“模拟行为”而不是“破解”,真正的破解涉及到逆向工程和算法识别,涉嫌违法行为,这里不展开。
4. 实战:构建一个抗封禁的 Selenium 采集框架
4.1 整体架构设计与模块拆分
当我需要大规模跑一个采集任务时,我绝对不会只写一个几十行的脚本,而会搭建一个可复用的采集框架,把浏览器管理、代理切换、重试机制、日志输出、数据存储全部拆分开。这样做的好处是,某个网站被封了,我只需要改一小部分代码,就能换一个目标继续跑,不用推倒重来。
这个框架我一般分为以下模块:
- 浏览器工厂:负责创建浏览器实例,处理 UA、代理、WebDriver 隐藏等操作,同时启动时做一系列初始化动作。
- 请求调度器:负责控制访问频率,维护一个代理 IP 池,当某个 IP 被封时自动切换。
- 页面解析器:负责解析 HTML 或者接口数据,提取目标字段。
- 数据存储:把解析结果写入文件或数据库,支持断点续采。
- 日志与告警:记录每一步的耗时、错误类型、代理使用情况,当连续失败达到阈值时发送告警通知。
我以 Python 为例,分享一个简化版的核心代码。
4.2 浏览器工厂实现
这里我以undetected_chromedriver为基础实现浏览器工厂。它的优势在于能够让 selenium 从底层摆脱 webdriver 的检测,在前面的基础上,再配合自定义请求头和一些启动参数,基本能应对大多数中小型网站。
import undetected_chromedriver as uc from selenium import webdriver from selenium.webdriver.chrome.options import Options class BrowserFactory: def __init__(self, proxy=None): self.proxy = proxy def create_driver(self): options = uc.ChromeOptions() options.add_argument("--window-size=1920,1080") options.add_argument("--disable-blink-features=AutomationControlled") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") if self.proxy: options.add_argument(f"--proxy-server={self.proxy}") driver = uc.Chrome(options=options) driver.execute_cdp_cmd("Network.enable", {}) return driver这里有几个细节要说一下。--disable-blink-features=AutomationControlled这个参数是为了关闭一些自动化控制的特征,execute_cdp_cmd("Network.enable", {})则是为了后续可以通过 CDP 继续修改请求头。
4.3 请求调度与重试机制
采集的时候难免会遇到请求超时、页面加载失败、元素找不到等情况。所以我在调度器里加入了一个重试机制,核心逻辑就是“失败次数太多就放弃当前代理,切换下一个代理”。
下面是一个简单的调度器实现:
import time import random class Scheduler: def __init__(self, proxies): self.proxies = proxies self.index = 0 self.fail_count = 0 def next_proxy(self): if self.fail_count >= 5: self.index = (self.index + 1) % len(self.proxies) self.fail_count = 0 return self.proxies[self.index] def run(self, driver, url): for attempt in range(3): try: driver.set_page_load_timeout(30) driver.get(url) time.sleep(random.uniform(2, 5)) return True except Exception as e: print(f"请求失败: {e}, 重试 {attempt + 1} 次") self.fail_count += 1 time.sleep(random.uniform(3, 8)) return False这个调度器在实际使用中,我会根据目标网站的严格程度调整失败阈值和重试次数。有些网站只要失败 2 次就可能封 IP,那就需要更谨慎地设置。
4.4 数据解析任务整合
完成页面加载后,就进入解析环节。以采集商品列表为例,我会先用find_element定位到容器,再循环提取每个商品项。但这里有个坑:页面可能是懒加载的,滚动没有到底部时,后面的元素根本不会出现在 DOM 中。所以我在解析之前,会先执行一个滚动到底部的动作,然后等待 1 秒,再获取页面源码。
def parse_page(driver): # 模拟滚动加载 for i in range(5): driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(1) items = driver.find_elements_by_css_selector(".item") data = [] for item in items: title = item.find_element_by_css_selector(".title").text price = item.find_element_by_css_selector(".price").text data.append({"title": title, "price": price}) return data这里要注意选择器别写太死,很多网站的 class 名是动态变化的。更好的做法是用更稳定的属性,比如>from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def wait_for_element(driver, selector): return WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, selector)) )
还有一个容易忽略的问题:有些页面元素在一个 iframe 里,如果你直接在当前页面找,永远找不到。这时候需要先用driver.switch_to.frame()切进去,操作完再切回主页面。
5.3 验证码处理:从滑块到点选
验证码是反爬最常见的手段。滑块验证码的应对我已经在上面提过,这里再说一种情况:当某个 IP 频繁出现验证码时,一定要优先考虑换代理 IP,而不是继续手动去拖动滑块,因为验证码出现频率本身就是一种信号,说明这个 IP 已经被标记了。我处理点选验证码的方式,一般是先用 OCR 识别出文字,然后根据文字坐标去点击。但这类验证码的误判率很高,并且盲目识别可能涉及绕过网站安全策略,我在这里只提思路,不建议大家投入太多精力去专门处理。最佳策略还是控制频率,让网站觉得你是“一个正常的用户”,而不是和验证码死磕。
5.4 常见问题速查表
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 打开页面空白 | 无头模式缺少某些渲染参数 | 加上--window-size和--force-device-scale-factor=1 |
| 页面能打开但数据不全 | 懒加载未触发 | 执行滚动脚本,或使用WebDriverWait等待特定元素出现 |
| 脚本运行一段时间后被封 | 访问频率过高或指纹特征明显 | 降低采集频率,换用undetected_chromedriver |
navigator.webdriver为 true | Selenium 原生特征未处理 | 使用uc.Chrome()或手动修改 JS 属性 |
| 滑块验证码反复失败 | 轨迹模拟不真实 | 用随机轨迹模拟,而不是一步拖完 |
这里也补充一个小技巧:在采集前先用 selenium 打开目标网站的主页,访问两三次,清除browser_state和缓存,再开始正式采集,这相当于人类用户先熟悉了一下网站环境,能降低一部分被标记的概率。
6. 合规与可持续采集:心态比技术更重要
最后我特别想说,做采集不能只盯着“怎么绕过去”,更要想清楚“这件事我该不该做”。我现在接到任何采集需求,第一件事不是写代码,而是确认这几件事:
- 目标网站有没有 robots.txt?里面是不是明确禁止了某些路径的采集?
- 要采集的数据是不是公开的、可合法使用的?有没有涉及用户隐私或版权?
- 请求频率会不会对目标服务器的稳定性造成影响?
如果你的答案是“不确定”或“会有一点影响”,那我会建议把采集频率降到最低,优先使用官方 API,或者在页面明显标注了“禁止采集”时直接放弃。这几年的经验告诉我,靠技术手段强行绕过封禁本身就是一个无底洞,你花了很多精力换来的数据,到最后可能还会带来法律风险。真正稳定的采集策略,是尊重规则、控制频率、合理使用数据,在这个前提下,再叠加高效的技术框架,才能长久地跑下去。
所以我实际做项目的时候,现在会更倾向于把重心放在“如何更优雅、更节制地采集”这件事上。比如通过robots.txt判断可爬取范围、设置并发数上限、在访问间隔上做更科学的随机分布。这些看起来不酷,但能让你少踩很多坑,也让整个采集项目走得更远。