Selenium采集反爬实战:从WebDriver检测到行为模拟全攻略
我做了三年多的数据采集从最初的 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-agentMozilla/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(--langzh-CN) options.add_argument(--accept-langzh-CN,zh;q0.9,en;q0.8) driver webdriver.Chrome(optionsoptions)这里需要注意设置 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))这么简单而是要模拟不同场景下的思考时间。比如打开页面后先停顿 12 秒模拟阅读内容滚动页面时分好几次滚动不要一次滚到底点击按钮前把鼠标先移动到按钮附近再点击填写表单时按一定的输入速度模拟打字。下面是一个简单的封装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(fwindow.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, proxyNone): self.proxy proxy def create_driver(self): options uc.ChromeOptions() options.add_argument(--window-size1920,1080) options.add_argument(--disable-blink-featuresAutomationControlled) 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(optionsoptions) driver.execute_cdp_cmd(Network.enable, {}) return driver这里有几个细节要说一下。--disable-blink-featuresAutomationControlled这个参数是为了关闭一些自动化控制的特征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-factor1页面能打开但数据不全懒加载未触发执行滚动脚本或使用WebDriverWait等待特定元素出现脚本运行一段时间后被封访问频率过高或指纹特征明显降低采集频率换用undetected_chromedrivernavigator.webdriver为 trueSelenium 原生特征未处理使用uc.Chrome()或手动修改 JS 属性滑块验证码反复失败轨迹模拟不真实用随机轨迹模拟而不是一步拖完这里也补充一个小技巧在采集前先用 selenium 打开目标网站的主页访问两三次清除browser_state和缓存再开始正式采集这相当于人类用户先熟悉了一下网站环境能降低一部分被标记的概率。6. 合规与可持续采集心态比技术更重要最后我特别想说做采集不能只盯着“怎么绕过去”更要想清楚“这件事我该不该做”。我现在接到任何采集需求第一件事不是写代码而是确认这几件事目标网站有没有 robots.txt里面是不是明确禁止了某些路径的采集要采集的数据是不是公开的、可合法使用的有没有涉及用户隐私或版权请求频率会不会对目标服务器的稳定性造成影响如果你的答案是“不确定”或“会有一点影响”那我会建议把采集频率降到最低优先使用官方 API或者在页面明显标注了“禁止采集”时直接放弃。这几年的经验告诉我靠技术手段强行绕过封禁本身就是一个无底洞你花了很多精力换来的数据到最后可能还会带来法律风险。真正稳定的采集策略是尊重规则、控制频率、合理使用数据在这个前提下再叠加高效的技术框架才能长久地跑下去。所以我实际做项目的时候现在会更倾向于把重心放在“如何更优雅、更节制地采集”这件事上。比如通过robots.txt判断可爬取范围、设置并发数上限、在访问间隔上做更科学的随机分布。这些看起来不酷但能让你少踩很多坑也让整个采集项目走得更远。