1. 项目概述:当Selenium遇上“反爬”高墙
如果你做过Web自动化测试或者数据采集,大概率对Selenium又爱又恨。爱的是它功能强大,能模拟真人操作浏览器,几乎无所不能;恨的是,现在稍微有点规模的网站,都能轻易识别出你是“机器人”。你精心编写的脚本,可能刚跑起来,页面就弹出一个验证码,或者直接返回一个空白页,告诉你“访问过于频繁”。这感觉就像你穿着夜行衣去执行任务,结果一进门,所有的警报器都响了,保安拿着手电筒直接照在你脸上。
这就是我们今天要聊的核心问题:如何让Selenium驱动的浏览器,在目标网站眼里变得“隐形”。我最近在一个需要模拟真实用户行为进行大规模数据验证的项目里,就深陷这个泥潭。常规的Selenium脚本被目标站的风控系统识别率高达90%以上,测试流程寸步难行。直到我系统地研究和应用了Selenium Stealth这套理念和技术组合,才真正解决了问题。这不是某一个具体的、叫做“Stealth”的库(虽然有一个同名的Python包),而是一整套让自动化浏览器指纹“隐身”的实战策略。
简单来说,Selenium Stealth的目标,就是通过一系列技术手段,修改或掩盖那些能被网站检测到的、属于自动化程序的“非人类”特征,使其指纹与普通用户通过Chrome、Firefox等浏览器手动访问时几乎一致。这不仅仅是加个user-agent那么简单,它涉及到WebDriver属性、JavaScript变量、浏览器特性、行为模式等数十个维度的对抗。接下来,我将结合我的实战经验,把这套“隐形术”从原理到操作,掰开揉碎了讲清楚。
2. 核心需求解析:为什么你的Selenium脚本会被“发现”?
在深入技术细节前,我们必须先弄明白对手是如何工作的。网站检测自动化脚本,主要依赖于“浏览器指纹”技术。指纹就像你的网络身份证,由浏览器暴露给网站的众多属性信息组合而成。自动化工具会无意中留下一些独特的“指纹”。
2.1 关键检测点剖析
网站通常从以下几个层面进行检测:
- WebDriver对象属性:这是最直接的证据。通过
navigator.webdriver这个属性,普通浏览器返回undefined或false,而由Selenium控制的浏览器会返回true。这是许多风控系统的首要检查项。 - 插件与语言列表:
navigator.plugins(插件列表)和navigator.languages(语言列表)在自动化环境中可能为空、格式异常或与声称的浏览器版本不匹配。 - 屏幕分辨率与色彩深度:自动化脚本有时会使用非标准的窗口大小,或者色彩深度(
screen.colorDepth,screen.pixelDepth)与常见设备不符。 - HTTP请求头特征:虽然Selenium可以设置
User-Agent,但其他头部信息如Accept-Language,Sec-CH-UA(用户代理客户端提示)等可能缺失或格式不正确,无法完美模拟特定浏览器版本。 - JavaScript执行环境差异:一些原生JavaScript对象的属性或方法(如
window.chrome,document.hidden)在自动化环境下的行为可能有细微差别。 - 行为模式异常:这是更高级的检测。人类的操作有随机延迟、不精确的鼠标移动轨迹、不规律的滚动速度。而自动化脚本的点击、输入、滚动往往过于精准和迅速,像是机器在“瞬移”。
2.2 不同场景下的需求差异
你的“隐形”需求强度,取决于目标网站的风控等级:
- 基础内容抓取与测试:对于个人博客、技术文档等无风控或弱风控站点,可能只需要处理
webdriver属性即可。 - 电商价格监控、社交媒体数据收集:这类网站通常有反爬机制,需要处理核心的JavaScript环境变量和请求头。
- 金融数据、高级别平台自动化:这是“地狱难度”。需要综合运用所有技术,并模拟人类行为模式,任何一环出错都可能导致失败。
我的项目属于第二种,目标是几个主流电商和内容平台,因此需要一套中等偏上强度的隐身方案。
3. 技术方案选型:从“补丁”到“系统工程”
面对这些检测点,我们有不同层次的技术方案可以选择。我将其分为三个层级:
3.1 层级一:基础属性覆盖(治标不治本)
这是最初级的做法,也是很多人唯一知道的做法:
- 修改User-Agent:使用
options.add_argument('user-agent=...')。 - 移除“自动化控制”提示:
options.add_experimental_option("excludeSwitches", ["enable-automation"])和options.add_experimental_option('useAutomationExtension', False)。 - 执行CDP命令:通过Chrome DevTools Protocol (CDP) 执行
Page.addScriptToEvaluateOnNewDocument来覆盖navigator.webdriver等属性。
为什么这不够?因为这些修改是“打补丁”,只覆盖了最明显的几个点。现代风控系统会进行多维度、交叉验证。例如,你声称自己是Chrome 120,但你的navigator.plugins列表是空的,这立刻就会暴露。
3.2 层级二:专用隐身库(快速上手,但有局限)
这就是标题中提到的selenium-stealth(Python包)。它是一个对层级一技术的封装,提供了一键式解决方案。
from selenium_stealth import stealth # ... 初始化driver后 stealth(driver, languages=["en-US", "en"], vendor="Google Inc.", platform="Win32", webgl_vendor="Intel Inc.", renderer="Intel Iris OpenGL Engine", fix_hairline=True, )它的优点:方便,开箱即用,覆盖了十多个常见检测点。它的缺点:
- 更新可能滞后:浏览器和检测技术都在更新,库的更新可能跟不上。
- 灵活性不足:参数是预设的,如果你想模拟一台MacBook上的Safari,可能需要自己修改源码。
- 可能被特征识别:大量用户使用相同的
selenium-stealth配置,其修改指纹的模式本身可能成为新的检测特征。
3.3 层级三:自定义CDP与行为模拟(终极方案)
这是最彻底、最灵活的方法。核心是直接使用CDP命令,在页面加载前注入JavaScript,精确地、按需地修改浏览器环境。同时,结合ActionChains等工具模拟人类操作。
为什么我最终选择了层级三?在项目中期,使用selenium-stealth后,识别率从90%降到了50%,但仍然不够。分析日志发现,目标网站增加了一些新的、冷门的检测点。selenium-stealth没有覆盖到。我需要能够动态调整策略,针对特定网站进行“外科手术式”的指纹修改。层级三方案给了我完全的掌控权。
4. 实战:构建你自己的“隐形”Selenium驱动
下面,我将以Chrome浏览器为例,详细拆解如何从零开始,构建一个高度隐身的WebDriver。这套方案是我在项目中反复调试后总结的,你可以直接复制使用,并根据需要调整。
4.1 环境准备与基础配置
首先,确保你安装了最新版的Selenium和浏览器驱动。
pip install selenium webdriver-manager使用webdriver-manager可以自动管理驱动版本,避免兼容性问题。
初始化Driver时,我们就要开始添加“隐身”配置:
from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.options import Options def create_stealth_driver(): chrome_options = Options() # 1. 基础隐身设置(必须) chrome_options.add_argument('--disable-blink-features=AutomationControlled') chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) chrome_options.add_experimental_option('useAutomationExtension', False) # 2. 模拟真实用户代理和语言(示例为Windows上的Chrome) chrome_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') chrome_options.add_argument('accept-language=en-US,en;q=0.9') # 3. 其他实用参数,减少特征 chrome_options.add_argument('--disable-gpu') # 某些虚拟环境可能需要 chrome_options.add_argument('--no-sandbox') # Linux环境常用 chrome_options.add_argument('--disable-dev-shm-usage') # 解决共享内存问题 chrome_options.add_argument('--disable-blink-features=AutomationControlled') # 以无头模式运行时,这个参数很重要 # chrome_options.add_argument('--headless=new') # 如果需要无头模式,使用new headless # 4. 禁用自动化密码保存提示等 prefs = { "credentials_enable_service": False, "profile.password_manager_enabled": False } chrome_options.add_experimental_option("prefs", prefs) # 使用webdriver-manager自动获取驱动 service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=chrome_options) return driver注意:
--headless=new是Chrome较新版本的无头模式,比旧的--headless更隐蔽,但并非完全无法检测。对于高风控网站,能不用无头尽量不用。
4.2 核心:使用CDP进行深度指纹修改
这是隐身技术的核心。我们通过CDP在文档创建前执行JS,覆盖关键属性。
def apply_stealth_js(driver): # 这段JavaScript是隐身的关键,它在页面任何脚本执行前就修改了环境。 stealth_js = """ // 1. 覆盖webdriver属性 Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); // 2. 覆盖plugins,模拟常见的插件 Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); // 3. 覆盖languages Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); // 4. 覆盖chrome对象(仅针对Chrome) // 注意:这里我们模拟了chrome对象的存在,但移除了其runtime属性(自动化特征) window.chrome = { runtime: {}, // 可以添加其他chrome属性 }; // 5. 覆盖permissions API const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters) => ( parameters.name === 'notifications' ? Promise.resolve({ state: Notification.permission }) : originalQuery(parameters) ); // 6. 处理iframes中的检测 Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', { get: function() { return window; } }); console.log('Stealth JS injected'); """ # 使用CDP命令执行 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": stealth_js })如何使用:在driver.get(url)之前,调用apply_stealth_js(driver)。这样,在目标页面加载时,我们的修改已经生效。
4.3 模拟人类行为模式
即使指纹完美,机械式的操作也会出卖你。我们需要让脚本“像人一样”。
随机延迟:不要在操作间使用固定的time.sleep(2)。
import random import time def human_delay(min_s=1, max_s=3): """生成一个随机的延迟时间""" time.sleep(random.uniform(min_s, max_s)) # 使用示例 driver.find_element(...).click() human_delay(1, 2) # 等待1到2秒之间的一个随机时间 driver.find_element(...).send_keys("text")模拟鼠标移动:使用ActionChains进行非直线的移动。
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By element = driver.find_element(By.ID, "some-button") actions = ActionChains(driver) # 将鼠标先移动到页面某个随机位置,再移动到目标元素 actions.move_by_offset(random.randint(-50, 50), random.randint(-50, 50)).pause(0.5) actions.move_to_element(element).pause(0.2) # 在元素上稍作停顿 actions.click() actions.perform()随机滚动:不要一次性滚动到底。
def human_like_scroll(driver, scroll_to_height=None): total_height = scroll_to_height or driver.execute_script("return document.body.scrollHeight") current_position = 0 scroll_step = random.randint(200, 500) # 每次滚动200-500像素 while current_position < total_height: # 随机决定这次滚动多少 this_step = min(scroll_step, total_height - current_position) this_step = random.randint(int(this_step*0.8), this_step) # 加入随机性 driver.execute_script(f"window.scrollBy(0, {this_step});") current_position += this_step human_delay(0.5, 1.5) # 滚动后随机停顿5. 进阶技巧与疑难排坑
在实际项目中,你会遇到各种各样奇怪的问题。下面是我踩过坑后总结的“排雷手册”。
5.1 如何检测自己的隐身效果?
你不能盲目相信代码,必须验证。有以下几种方法:
- 使用检测网站:访问像
pixelscan.net、browserleaks.com这样的浏览器指纹检测站。用你的隐身Driver打开它们,查看检测报告。重点关注“WebDriver”、“自动化”等项目的检测结果。 - 本地JS检测:在页面中执行一些检测脚本。
driver.get("about:blank") test_js = """ return { webdriver: navigator.webdriver, plugins: navigator.plugins.length, languages: navigator.languages, chrome: typeof window.chrome, } """ result = driver.execute_script(test_js) print("检测结果:", result) # 理想结果:webdriver应为undefined或false,plugins>0,chrome为'object'但可能没有runtime
5.2 常见问题与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
navigator.webdriver仍为true | CDP脚本注入失败或顺序不对 | 确保在driver.get()前执行execute_cdp_cmd。检查Chrome版本与CDP兼容性。 |
| 网站提示“请使用现代浏览器” | User-Agent或HTTP头信息不完整/错误 | 使用更真实的UA字符串,并通过CDP或selenium-wire等库设置完整的请求头(如Accept,Sec-CH-UA)。 |
| 仅在无头模式下被检测到 | 无头模式有额外特征 | 尝试使用--headless=new。如果不行,考虑使用“有头但最小化”模式:chrome_options.add_argument('--window-size=1920,1080')并配合虚拟帧缓冲(xvfb)在服务器运行。 |
| 操作太快被封IP | 行为模式异常,与指纹无关 | 严格遵守“人类行为模拟”,增加随机延迟和错误操作(如偶尔点击错位置再纠正)。使用代理IP池轮换请求。 |
| 部分iframe内内容仍检测到自动化 | iframe是独立上下文,我们的JS未注入 | 在切换到iframe后,重新在该iframe的上下文中执行CDP命令注入JS。 |
5.3 关于“免费”与可持续性
标题中提到“亲测免费”,这里的免费指的是:
- Selenium Stealth技术本身是开源的,无论是理念还是
selenium-stealth库,都无需付费。 - 核心解决方案(CDP)是浏览器原生能力,不需要购买任何第三方服务。
但是,请注意:隐身是一场持续的“军备竞赛”。今天有效的方法,明天可能就失效了。因此,最宝贵的“免费”资源是你的调试能力和知识储备。你需要:
- 保持更新:关注Chrome/WebDriver的版本更新,它们可能会引入或移除某些特征。
- 建立测试用例:为你的目标网站编写简单的检测脚本,定期运行,确保隐身策略依然有效。
- 理解原理而非死记代码:明白每个CDP命令和JS片段是在修改什么,为什么这样改。这样当出现新检测点时,你才能快速找到对策。
6. 项目实战:一个完整的隐身数据采集流程示例
假设我们需要从某个电商网站(我们称其为SiteX)列表页采集商品名称和价格,该网站有基础的反爬。
import random import time from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.options import Options class StealthScraper: def __init__(self, use_headless=False): self.driver = self._create_stealth_driver(use_headless) self._apply_stealth() def _create_stealth_driver(self, use_headless): options = Options() if use_headless: options.add_argument('--headless=new') # 使用新的无头模式 # 基础隐身与配置 options.add_argument('--disable-blink-features=AutomationControlled') options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option('useAutomationExtension', False) options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...') service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options) driver.maximize_window() # 最大化窗口,更符合用户习惯 return driver def _apply_stealth(self): """应用深度隐身JS""" js = """ Object.defineProperty(navigator, 'webdriver', {get: () => undefined}); // ... 此处包含上一节4.2中的所有JS代码 ... """ self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": js}) def _human_delay(self, min_t=1, max_t=3): time.sleep(random.uniform(min_t, max_t)) def scrape_sitex(self, url): print(f"正在访问: {url}") self.driver.get(url) self._human_delay(2, 4) # 初始页面加载等待 data = [] page_num = 1 while True: print(f"正在处理第 {page_num} 页...") # 1. 模拟人类随机滚动阅读 self._human_like_scroll() # 2. 等待商品元素加载 try: WebDriverWait(self.driver, 10).until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, ".product-item")) ) except: print("未找到商品元素,可能页面结构已变或已被拦截。") break products = self.driver.find_elements(By.CSS_SELECTOR, ".product-item") for product in products: # 3. 模拟鼠标悬停(随机选择部分商品) if random.random() > 0.7: # 30%的概率执行悬停 actions = ActionChains(self.driver) actions.move_to_element(product).pause(random.uniform(0.5, 1.5)).perform() # 4. 提取数据 try: name_elem = product.find_element(By.CSS_SELECTOR, ".product-name") price_elem = product.find_element(By.CSS_SELECTOR, ".product-price") # 模拟人类阅读速度,逐个元素查看 self._human_delay(0.1, 0.3) name = name_elem.text.strip() price = price_elem.text.strip() if name and price: data.append({"name": name, "price": price}) except: continue # 某个元素提取失败,跳过该商品 # 5. 寻找下一页按钮并模拟人类点击 try: next_btn = self.driver.find_element(By.CSS_SELECTOR, ".next-page") # 点击前随机延迟 self._human_delay(1, 2) # 滚动到按钮位置 self.driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", next_btn) self._human_delay(0.5, 1) next_btn.click() page_num += 1 self._human_delay(2, 4) # 等待新页面加载 except: print("已到达最后一页或找不到下一页按钮。") break return data def _human_like_scroll(self): """模拟人类滚动""" scroll_pause_time = random.uniform(0.5, 1.5) scroll_step = random.randint(300, 700) current_scroll = 0 max_scroll = self.driver.execute_script("return document.body.scrollHeight") - self.driver.execute_script("return window.innerHeight") while current_scroll < max_scroll: # 每次滚动距离随机 this_step = min(scroll_step, max_scroll - current_scroll) this_step = random.randint(int(this_step*0.7), this_step) self.driver.execute_script(f"window.scrollBy(0, {this_step});") current_scroll += this_step time.sleep(scroll_pause_time) # 随机决定是否进行一小段回滚(模仿人类犹豫) if random.random() > 0.9 and current_scroll > 100: rollback = random.randint(-50, -10) self.driver.execute_script(f"window.scrollBy(0, {rollback});") current_scroll += rollback time.sleep(random.uniform(0.3, 0.8)) def close(self): self.driver.quit() # 使用示例 if __name__ == "__main__": scraper = StealthScraper(use_headless=False) # 高风控站建议先用有头模式调试 try: data = scraper.scrape_sitex("https://www.example-sitex.com/products") print(f"共采集到 {len(data)} 条商品数据。") for item in data[:5]: # 打印前5条看看 print(item) finally: scraper.close()这个示例融合了之前所有的知识点:深度指纹修改、人类行为模拟、健壮的错误处理。它不是一个万能脚本,但为你提供了一个高度可定制和可调试的框架。
7. 总结与个人心得
让Selenium“隐形”不是调用一个魔法函数就能完成的,它是一个系统工程,涉及浏览器指纹、网络协议和行为模式三个层面的对抗。我的经验是:
分步实施,逐步加强:不要一开始就上最复杂的方案。先用基础配置和selenium-stealth测试,如果被识别,再逐步添加CDP深度修改和行为模拟。用检测网站作为你的“试金石”。
理解高于复制:网上有很多代码片段,但如果不明白Object.defineProperty在做什么,不明白CDP命令的执行时机,当脚本失效时你将束手无策。花时间阅读MDN上关于Web API的文档,理解navigator、screen、window这些对象。
环境隔离与代理管理:对于大规模任务,一定要使用干净的浏览器环境(可以考虑每次启动新用户目录)和高质量的代理IP。一个被标记的IP地址会让任何隐身技术失效。
保持敬畏,合法使用:技术是把双刃剑。这套方法旨在帮助测试工程师更好地完成自动化测试工作,或研究人员在遵守robots.txt和网站条款的前提下进行合规的数据分析。请勿将其用于恶意爬取、攻击或干扰网站正常运行。
最后,再分享一个调试小技巧:在开发隐身脚本时,我通常会打开Chrome的开发者工具(options.add_argument('--auto-open-devtools-for-tabs')),在Console里实时查看navigator.webdriver等属性的值,这比任何外部检测都更直接。隐身是一场猫鼠游戏,而你的耐心和对细节的把握,是赢得这场游戏的关键。