前言

Selenium是Python自动化测试和爬虫中最常用的工具之一,但它有一个致命的问题:网站一眼就能看出你在用Selenium

打开Chrome DevTools控制台输入navigator.webdriver,如果返回true,恭喜你已经暴露了。Cloudflare、Akamai、Imperva等反爬系统检测到这个标志后,会直接给你一个验证码页面或者403。

本文将深入分析Selenium被检测的原因,然后给出实际可用的反检测方案。

Selenium为什么会被检测

1. navigator.webdriver标志

这是最基本的检测点。ChromeDriver启动时会自动设置这个标志:

from selenium import webdriver

driver = webdriver.Chrome()
driver.get('https://bot.sannysoft.com')

# 在控制台中
# navigator.webdriver = true  ← 暴露!

2. Chrome DevTools Protocol痕迹

ChromeDriver通过CDP协议控制浏览器,会留下多个痕迹:

// 这些属性在自动化浏览器中存在,正常浏览器中不存在
window.cdc_adoQpoasnfa76pfcZLmcfl_Array
window.cdc_adoQpoasnfa76pfcZLmcfl_Promise
window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol

// chrome对象中的异常
window.chrome.runtime  // 正常浏览器中有完整的runtime API

3. 缺少用户态特征

自动化浏览器缺少正常用户的特征:

# 正常浏览器有的,Selenium默认没有的
missing_features = {
    'plugins': '浏览器插件列表为空',
        'languages': '语言列表异常',
            'permissions': '权限API行为异常',
                'webgl': 'WebGL渲染结果可能不同',
                    'chrome_app': 'chrome.app 对象缺失',
                    }
                    ```
## 方案一:undetected-chromedriver(推荐入门)

undetected-chromedriver(简称UC)是最流行的Selenium反检测库,通过修改ChromeDriver二进制文件来移除自动化痕迹。

### 安装

```bash
pip install undetected-chromedriver

基本使用

import undetected_chromedriver as uc

# 创建反检测浏览器实例
driver = uc.Chrome()

# 访问受保护的网站
driver.get('https://nowsecure.nl')

# 检查 - navigator.webdriver 现在返回 undefined
result = driver.execute_script('return navigator.webdriver')
print(f'webdriver标志: {result}')  # None (undefined)

# 正常操作
driver.get('https://target-site.com')
content = driver.page_source
print(f'页面长度: {len(content)}')

driver.quit()

配置选项

import undetected_chromedriver as uc

options = uc.ChromeOptions()

# 设置用户数据目录(保持登录状态)
options.add_argument('--user-data-dir=/tmp/chrome-profile')

# 设置窗口大小
options.add_argument('--window-size=1920,1080')

# 设置语言
options.add_argument('--lang=zh-CN')

# 禁用GPU加速(服务器环境)
options.add_argument('--disable-gpu')

# 指定Chrome版本(可选)
driver = uc.Chrome(
    options=options,
        version_main=124,  # 指定大版本号
        )
driver.get('https://bot.sannysoft.com')
import time
time.sleep(10)  # 查看检测结果
driver.quit()

使用代理

import undetected_chromedriver as uc

options = uc.ChromeOptions()

# HTTP代理
options.add_argument('--proxy-server=http://user:pass@proxy:8080')

# 或者SOCKS5代理
options.add_argument('--proxy-server=socks5://proxy:1080')

driver = uc.Chrome(options=options)
driver.get('https://httpbin.org/ip')
print(driver.page_source)
driver.quit()

方案二:Patchright(Playwright的反检测版)

如果你更喜欢Playwright的API风格,Patchright是一个很好的选择。它是Playwright的补丁版本,移除了自动化检测标志。

安装

pip install patchright
patchright install chromium

基本使用

from patchright.sync_api import sync_patchright

with sync_patchright() as p:
    browser = p.chromium.launch(headless=False)
        page = browser.new_page()
            
                page.goto('https://bot.sannysoft.com')
                    
                        # 检查webdriver标志
                            result = page.evaluate('navigator.webdriver')
                                print(f'webdriver: {result}')  # undefined
                                    
                                        page.screenshot(path='sannysoft.png')
                                            browser.close()
                                            ```
### 异步使用

```python
from patchright.async_api import async_patchright
import asyncio

async def main():
    async with async_patchright() as p:
            browser = await p.chromium.launch(headless=False)
                    page = await browser.new_page()
                            
                                    await page.goto('https://nowsecure.nl')
                                            
                                                    content = await page.content()
                                                            print(f'页面长度: {len(content)}')
                                                                    
                                                                            await browser.close()
asyncio.run(main())

方案三:手动修补Selenium

如果不想用第三方库,可以手动修补Selenium来绕过检测:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service

options = Options()

# 1. 排除自动化开关
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)

# 2. 禁用Blink特征检测
options.add_argument('--disable-blink-features=AutomationControlled')

# 3. 设置合理的窗口大小和UA
options.add_argument('--window-size=1920,1080')
options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36')

driver = webdriver.Chrome(options=options)

# 4. 通过CDP命令覆盖webdriver属性
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
    'source': '''
            Object.defineProperty(navigator, 'webdriver', {
                        get: () => undefined
                                });
                                        
                                                // 伪造plugins
                                                        Object.defineProperty(navigator, 'plugins', {
                                                                    get: () => [1, 2, 3, 4, 5]
                                                                            });
                                                                                    
                                                                                            // 伪造languages
                                                                                                    Object.defineProperty(navigator, 'languages', {
                                                                                                                get: () => ['zh-CN', 'zh', 'en']
                                                                                                                        });
                                                                                                                                
                                                                                                                                        // 删除CDP痕迹
                                                                                                                                                const originalQuery = window.navigator.permissions.query;
                                                                                                                                                        window.navigator.permissions.query = (parameters) => (
                                                                                                                                                                    parameters.name === 'notifications'
                                                                                                                                                                                    ? Promise.resolve({ state: Notification.permission })
                                                                                                                                                                                                    : originalQuery(parameters)
                                                                                                                                                                                                            );
                                                                                                                                                                                                                '''
                                                                                                                                                                                                                })
driver.get('https://bot.sannysoft.com')

注意:手动修补的成功率不如UC或Patchright,因为检测点很多且持续更新。

实战:完整的反检测爬虫

import undetected_chromedriver as uc
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import random
import json

class StealthBrowser:
    def __init__(self, proxy=None, headless=False):
            options = uc.ChromeOptions()
                    
                            if proxy:
                                        options.add_argument(f'--proxy-server={proxy}')
                                                
                                                        if headless:
                                                                    options.add_argument('--headless=new')
                                                                            
                                                                                    options.add_argument('--window-size=1920,1080')
                                                                                            options.add_argument('--lang=zh-CN')
                                                                                                    
                                                                                                            self.driver = uc.Chrome(options=options)
                                                                                                                    self.wait = WebDriverWait(self.driver, 15)
                                                                                                                        
                                                                                                                            def get(self, url, wait_selector=None):
                                                                                                                                    """访问页面,可选等待特定元素加载"""
                                                                                                                                            self.driver.get(url)
                                                                                                                                                    self._random_delay()
                                                                                                                                                            
                                                                                                                                                                    if wait_selector:
                                                                                                                                                                                self.wait.until(
                                                                                                                                                                                                EC.presence_of_element_located((By.CSS_SELECTOR, wait_selector))
                                                                                                                                                                                                            )
                                                                                                                                                                                                                    
                                                                                                                                                                                                                            return self.driver.page_source
                                                                                                                                                                                                                                
                                                                                                                                                                                                                                    def scroll_page(self, times=3):
                                                                                                                                                                                                                                            """模拟人类滚动行为"""
                                                                                                                                                                                                                                                    for _ in range(times):
                                                                                                                                                                                                                                                                scroll_amount = random.randint(300, 800)
                                                                                                                                                                                                                                                                            self.driver.execute_script(f'window.scrollBy(0, {scroll_amount})')
                                                                                                                                                                                                                                                                                        self._random_delay(0.5, 1.5)
                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                def click_element(self, selector):
                                                                                                                                                                                                                                                                                                        """点击元素,带随机偏移"""
                                                                                                                                                                                                                                                                                                                element = self.wait.until(
                                                                                                                                                                                                                                                                                                                            EC.element_to_be_clickable((By.CSS_SELECTOR, selector))
                                                                                                                                                                                                                                                                                                                                    )
                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                    # 先滚动到元素可见
                                                                                                                                                                                                                                                                                                                                                            self.driver.execute_script('arguments[0].scrollIntoView({block: "center"})', element)
                                                                                                                                                                                                                                                                                                                                                                    self._random_delay(0.3, 0.8)
                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                    element.click()
                                                                                                                                                                                                                                                                                                                                                                                            self._random_delay()
                                                                                                                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                                                                                                                    def fill_input(self, selector, text):
                                                                                                                                                                                                                                                                                                                                                                                                            """模拟人类输入"""
                                                                                                                                                                                                                                                                                                                                                                                                                    element = self.wait.until(
                                                                                                                                                                                                                                                                                                                                                                                                                                EC.presence_of_element_located((By.CSS_SELECTOR, selector))
                                                                                                                                                                                                                                                                                                                                                                                                                                        )
                                                                                                                                                                                                                                                                                                                                                                                                                                                element.clear()
                                                                                                                                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                                                                                                                                                for char in text:
                                                                                                                                                                                                                                                                                                                                                                                                                                                                            element.send_keys(char)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        time.sleep(random.uniform(0.05, 0.15))
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                def _random_delay(self, min_s=1.0, max_s=3.0):
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        time.sleep(random.uniform(min_s, max_s))
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                def close(self):
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        self.driver.quit()
# 使用示例
browser = StealthBrowser(headless=False)
try:
    # 访问受保护的网站
        html = browser.get(
                'https://target-site.com',
                        wait_selector='div.content'
                            )
                                
                                    # 模拟滚动
                                        browser.scroll_page(times=3)
                                            
                                                # 提取数据
                                                    elements = browser.driver.find_elements(By.CSS_SELECTOR, '.item')
                                                        for el in elements:
                                                                print(el.text)
                                                                        
                                                                        finally:
                                                                            browser.close()
                                                                            ```
## 处理验证码

即使用了反检测浏览器,某些场景仍然会遇到验证码。这时需要结合API解决:

```python
import undetected_chromedriver as uc
from passxapi import PassXAPI
import re
import time

class AntiDetectScraper:
    def __init__(self, captcha_api_key=None):
            self.driver = uc.Chrome()
                    self.solver = PassXAPI(api_key=captcha_api_key) if captcha_api_key else None
                        
                            def navigate(self, url):
                                    self.driver.get(url)
                                            time.sleep(3)
                                                    
                                                            # 检查是否遇到验证码
                                                                    page_source = self.driver.page_source
                                                                            
                                                                                    if self._has_captcha(page_source):
                                                                                                print('检测到验证码,尝试解决...')
                                                                                                            self._solve_captcha(page_source, url)
                                                                                                                    
                                                                                                                            return self.driver.page_source
                                                                                                                                
                                                                                                                                    def _has_captcha(self, html):
                                                                                                                                            captcha_signs = [
                                                                                                                                                        'data-sitekey', 'g-recaptcha', 'h-captcha',
                                                                                                                                                                    'cf-turnstile', 'challenge-platform'
                                                                                                                                                                            ]
                                                                                                                                                                                    return any(sign in html for sign in captcha_signs)
                                                                                                                                                                                        
                                                                                                                                                                                            def _solve_captcha(self, html, url):
                                                                                                                                                                                                    if not self.solver:
                                                                                                                                                                                                                print('未配置验证码API')
                                                                                                                                                                                                                            return
                                                                                                                                                                                                                                    
                                                                                                                                                                                                                                            # 提取sitekey
                                                                                                                                                                                                                                                    match = re.search(r'data-sitekey="([^"]+)"', html)
                                                                                                                                                                                                                                                            if not match:
                                                                                                                                                                                                                                                                        return
                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                        sitekey = match.group(1)
                                                                                                                                                                                                                                                                                                
                                                                                                                                                                                                                                                                                                        # 根据类型调用对应的解决方法
                                                                                                                                                                                                                                                                                                                if 'cf-turnstile' in html:
                                                                                                                                                                                                                                                                                                                            result = self.solver.solve_turnstile(sitekey=sitekey, url=url)
                                                                                                                                                                                                                                                                                                                                    elif 'h-captcha' in html:
                                                                                                                                                                                                                                                                                                                                                result = self.solver.solve_hcaptcha(sitekey=sitekey, url=url)
                                                                                                                                                                                                                                                                                                                                                        else:
                                                                                                                                                                                                                                                                                                                                                                    result = self.solver.solve_recaptcha(sitekey=sitekey, url=url)
                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                    token = result.get('token')
                                                                                                                                                                                                                                                                                                                                                                                            if token:
                                                                                                                                                                                                                                                                                                                                                                                                        # 注入token
                                                                                                                                                                                                                                                                                                                                                                                                                    self.driver.execute_script(f'''
                                                                                                                                                                                                                                                                                                                                                                                                                                    document.querySelector('[name="cf-turnstile-response"], [name="g-recaptcha-response"], [name="h-captcha-response"]').value = "{token}";
                                                                                                                                                                                                                                                                                                                                                                                                                                                ''')
                                                                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                                                                                        # 触发验证回调
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    self.driver.execute_script('''
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    if (window.turnstileCallback) turnstileCallback(arguments[0]);
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    if (window.onCaptchaSuccess) onCaptchaSuccess(arguments[0]);
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                ''', token)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        time.sleep(2)
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    print('验证码已解决')
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            def close(self):
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    self.driver.quit()
# 使用
scraper = AntiDetectScraper(captcha_api_key='your_passxapi_key')
try:
    html = scraper.navigate('https://protected-site.com')
        print(f'获取到内容: {len(html)} 字符')
        finally:
            scraper.close()
            ```
## 检测你的浏览器

在开发反检测爬虫时,用这些网站验证你的伪装效果:

```python
import undetected_chromedriver as uc
import time

driver = uc.Chrome()

# 测试网站列表
test_sites = [
    'https://bot.sannysoft.com',        # 综合检测
        'https://nowsecure.nl',              # Cloudflare检测
            'https://abrahamjuliot.github.io/creepjs/',  # CreepJS指纹
                'https://pixelscan.net',             # 浏览器指纹
                ]
for site in test_sites:
    driver.get(site)
        time.sleep(5)
            driver.screenshot(f'test_{site.split("/")[2]}.png')
                print(f'已截图: {site}')
driver.quit()

常见坑

1. Headless模式更容易被检测

# 不推荐:headless模式检测点更多
driver = uc.Chrome(headless=True)

# 推荐:用xvfb在服务器上运行有头模式
# apt install xvfb
# xvfb-run python scraper.py

2. 不要频繁创建销毁浏览器实例

# 错误:每个请求都创建新浏览器
for url in urls:
    driver = uc.Chrome()  # 慢!每次启动Chrome
        driver.get(url)
            driver.quit()
# 正确:复用浏览器实例
driver = uc.Chrome()
for url in urls:
    driver.get(url)
        time.sleep(random.uniform(2, 5))
        driver.quit()
        ```
### 3. 记得模拟人类行为

```python
# 机器人行为:瞬间完成所有操作
driver.get(url)
driver.find_element(By.ID, 'search').send_keys('keyword')
driver.find_element(By.ID, 'submit').click()

# 人类行为:带随机延迟
driver.get(url)
time.sleep(random.uniform(1, 3))

search_box = driver.find_element(By.ID, 'search')
for char in 'keyword':
    search_box.send_keys(char)
        time.sleep(random.uniform(0.05, 0.2))
time.sleep(random.uniform(0.5, 1.5))
driver.find_element(By.ID, 'submit').click()

总结

Selenium反检测的核心思路:

  1. 用UC或Patchright替代原生Selenium,自动处理大部分检测点
    1. 避免headless模式,用xvfb在服务器上运行有头浏览器
    1. 模拟人类行为:随机延迟、滚动、鼠标移动
    1. 遇到验证码用API解决,比手动点击更可靠
      验证码解决推荐 PassXAPI(https://www.passxapi.com/en),注册即送1000次免费调用,支持reCAPTCHA、hCaptcha、Turnstile等主流验证码,$0.001/次。Python SDK:passxapi-python

觉得有帮助请点赞收藏,有问题欢迎评论区讨论。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐