一、网站反爬虫检测到底在检测什么

很多爬虫新手经常碰到辛辛苦苦写好的脚本一跑就被网站封了,这背后是各类反爬虫机制在默默工作。常见的检测点包括:浏览器特征(UserAgent、浏览器指纹)、自动化标识(navigator.webdriver属性)、访问频率(单IP请求间隔)、行为模式(鼠标轨迹、点击、滚动)等。其中UserAgent是服务器识别客户端的第一道关,如果发送的是Selenium默认的UserAgent(比如“HeadlessChrome”),就很容易暴露。另外,Chrome浏览器在自动化模式下会暴露一些特殊标志,比如window.navigator.webdriver会返回true,有些网站会检查这个属性。代理方面,如果频繁用同一个IP请求,触发频率限制也很正常。最后,人类访问网页是有随机性的,会看内容、有停顿、会移动鼠标,而脚本访问往往直来直去,这也容易被识别。

二、从改头换面开始:修改UserAgent

2.1 为什么要改UserAgent

Selenium默认的UserAgent会包含“HeadlessChrome”或者“Selenium”等字样,很多网站直接看这个字段就拒绝了。所以我们要伪装成正常的浏览器版本。最常见的做法就是从真实浏览器拷贝一个UserAgent字符串,然后注入到Selenium的启动参数里。

2.2 代码实现(技术栈:Python + Selenium)

# 先导入需要的库
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def create_driver_with_ua():
    # 创建一个Chrome选项对象
    options = Options()
    
    # 设置一个真实的UserAgent(这里用Chrome 120 的Windows版本)
    # 如果你不确定,可以去你常用的浏览器随便打开一个网站,
    # 按F12打开开发者工具,在Network里随便点一个请求,
    # 看Request Headers里的User-Agent字段,复制过来就行
    fake_ua = "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(f'user-agent={fake_ua}')
    
    # 初始化驱动时使用这些选项
    driver = webdriver.Chrome(options=options)
    return driver

# 测试一下
if __name__ == "__main__":
    driver = create_driver_with_ua()
    driver.get("https://httpbin.org/user-agent")  # 这个网站会返回你的UserAgent
    print(driver.page_source)  # 应该看到我们伪造的UA
    driver.quit()

注意:有些网站会同时检查其他浏览器指纹(如WebGL、Canvas等),但修改UserAgent是最简单有效的第一步。

三、藏起自动化标识:让Selenium看起来像真人

3.1 常见的自动化标识

浏览器在自动化模式下会多出一些特征,最典型的就是navigator.webdriver属性。正常浏览器这个值是falseundefined,但Selenium默认设为true。同时还有一些Chrome启动时的命令行标志(如--enable-automation)也会暴露。我们可以通过两种方式来消除这些痕迹:一是用Chrome的--disable-blink-features=AutomationControlled参数,二是用execute_cdp_cmd动态注入JavaScript来覆盖这些属性。

3.2 代码实现:彻底隐藏Selenium标记

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

def create_stealth_driver():
    options = Options()
    
    # 1. 先改UA
    fake_ua = "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(f'user-agent={fake_ua}')
    
    # 2. 禁用自动化控制标记
    options.add_argument('--disable-blink-features=AutomationControlled')
    
    # 3. 排除自动化开关(这会让浏览器好像不是被程序控制的一样)
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_experimental_option('useAutomationExtension', False)
    
    driver = webdriver.Chrome(options=options)
    
    # 4. 在页面加载前注入一段JavaScript,把webdriver属性改成undefined
    # 这是最关键的一步,因为很多网站会检测这个属性
    driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
        'source': '''
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined
            });
            // 顺便把一些其他常见的自动化特征也抹掉
            window.chrome = {
                runtime: {}
            };
            // 覆盖plugins数组,让它有正常长度
            Object.defineProperty(navigator, 'plugins', {
                get: () => [1, 2, 3, 4, 5]  // 至少要有几个插件才算正常
            });
            // 覆盖languages数组
            Object.defineProperty(navigator, 'languages', {
                get: () => ['zh-CN', 'zh']
            });
        '''
    })
    
    return driver

# 测试
if __name__ == "__main__":
    driver = create_stealth_driver()
    driver.get("https://bot.sannysoft.com/")  # 这是个很有名的检测自动化工具的网站
    # 可以手动查看结果,或者打印页面上的检测结果
    driver.quit()

这里用到的execute_cdp_cmd是Chrome DevTools Protocol的能力,可以让我们在浏览器打开新页面之前就注入脚本,这样脚本执行时自动化特征已经被覆盖了。

四、代理轮换:让IP不再背锅

4.1 为什么需要代理轮换

如果同一个IP在短时间内大量请求同一个网站,服务器很容易触发反爬策略,比如返回验证码或者直接封IP。解决思路就是使用代理池,每次请求(或每几次请求)换一个IP,让流量看起来来自不同的用户。注意:这里说的代理包括HTTP代理和SOCKS代理,免费代理虽然便宜但质量参差不齐,建议使用付费代理或自建代理池。

4.2 代码实现:动态切换代理

import random
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

# 假设我们有一个代理列表,实际使用中可以从代理API获取
proxy_list = [
    "http://proxy1.example.com:8080",
    "http://proxy2.example.com:8080",
    "http://proxy3.example.com:8080",
    # ... 更多代理
]

def create_driver_with_proxy(proxy):
    options = Options()
    # 同样先设置UA和隐藏自动化(这里省略重复代码)
    fake_ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."  # 真实UA
    options.add_argument(f'user-agent={fake_ua}')
    options.add_argument('--disable-blink-features=AutomationControlled')
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_experimental_option('useAutomationExtension', False)
    
    # 设置代理
    options.add_argument(f'--proxy-server={proxy}')
    
    driver = webdriver.Chrome(options=options)
    # 注入JavaScript隐藏自动化特征
    driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
        'source': '''
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined
            });
        '''
    })
    return driver

def crawl_with_proxy_rotation(url, max_requests=10):
    for i in range(max_requests):
        # 随机选一个代理
        proxy = random.choice(proxy_list)
        print(f"第{i+1}次请求,使用代理:{proxy}")
        try:
            driver = create_driver_with_proxy(proxy)
            driver.get(url)
            # 简单处理页面内容...
            time.sleep(random.uniform(3, 6))  # 模拟人类慢速浏览
        except Exception as e:
            print(f"代理{proxy}失败:{e}")
            # 可以把这个代理标记为不可用
        finally:
            if 'driver' in locals():
                driver.quit()
        # 每次请求之间随机等待,不要有规律
        time.sleep(random.uniform(5, 10))

if __name__ == "__main__":
    crawl_with_proxy_rotation("https://httpbin.org/ip")
    # 这个网站会返回你的IP,可以观察每次是否变了

注意:代理不是万能的。有些网站会检测代理IP的公开性质(比如免费代理IP在黑名单上),所以尽量用高质量代理。另外,每次创建新driver效率比较低,如果你需要连续爬取很多页面,可以复用driver但更换代理会更复杂(Selenium不支持运行中直接改代理),所以通常的做法是每次请求都重新启动浏览器。

五、模拟人类行为:让每一步都像真人

5.1 人类行为特征

真人访问网页会有这些特征:不会一打开就马上点击;会花时间阅读内容;鼠标会有轨迹,不是瞬间移动到目标;会有随机的滚动;会偶尔犯错(比如点错又退回来)。而Selenium默认的操作是瞬间完成的,很容易被检测。所以我们需要在每一步加入随机延时,并且模拟鼠标缓慢移动。

5.2 代码实现:模拟鼠标轨迹和随机等待

import random
import time
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By

def human_like_click(driver, element):
    """
    模拟人类的点击:先缓慢移动鼠标,再停一下,然后点击
    """
    # 1. 随机获取目标元素的坐标
    location = element.location
    size = element.size
    target_x = location['x'] + size['width'] / 2 + random.randint(-10, 10)
    target_y = location['y'] + size['height'] / 2 + random.randint(-10, 10)
    
    # 2. 模拟鼠标从当前位置移动到目标位置(分段移动)
    actions = ActionChains(driver)
    current_pos = driver.execute_script("return {x: window.mouseX, y: window.mouseY}") if False else (0, 0)
    # 为了方便,我们直接使用move_by_offset的随机步长
    # 注意:真实环境中需要获取鼠标位置,但这里简化了
    # 更好的做法是用Selenium的move_to_element_with_offset
    actions.move_to_element_with_offset(element, 0, 0)
    actions.pause(random.uniform(0.1, 0.3))  # 小停顿
    actions.click()
    actions.perform()

def human_scroll(driver, scroll_pause=0.5):
    """
    模拟人类慢慢向下滚动
    """
    # 获取页面高度
    last_height = driver.execute_script("return document.body.scrollHeight")
    while True:
        # 随机滚动一小段
        scroll_distance = random.randint(100, 300)
        driver.execute_script(f"window.scrollBy(0, {scroll_distance})")
        # 随机停一下,假装在看内容
        time.sleep(random.uniform(0.5, 1.5))
        # 检查是否到达底部
        new_height = driver.execute_script("return document.body.scrollHeight")
        if new_height == last_height:
            break
        last_height = new_height

def visit_page_like_human(driver, url):
    # 访问页面
    driver.get(url)
    
    # 1. 先等待页面加载
    time.sleep(random.uniform(2, 4))
    
    # 2. 模拟滚动
    human_scroll(driver)
    
    # 3. 找到某个链接(比如文章标题)
    link = driver.find_element(By.CSS_SELECTOR, "a[href*='article']")  # 示例选择器
    if link:
        # 4. 先把鼠标移动过去(模拟悬浮)
        ActionChains(driver).move_to_element(link).pause(random.uniform(0.3, 0.8)).perform()
        time.sleep(random.uniform(0.5, 1))
        # 5. 点击时采用模拟人类的方式
        human_like_click(driver, link)
    
    # 6. 在目标页面继续模拟浏览
    time.sleep(random.uniform(3, 5))

# 完整示例
if __name__ == "__main__":
    from selenium import webdriver
    driver = webdriver.Chrome()
    try:
        visit_page_like_human(driver, "https://example.com")
    finally:
        driver.quit()

这里我们用到了ActionChains来模拟鼠标动作。要注意,Selenium本身只能发送坐标移动,但真正的鼠标移动是有加速度的,而且每个用户的习惯不同。如果你追求极致,可以自己写一个贝塞尔曲线函数来生成移动轨迹,但一般场景下分段移动加上随机停顿已经够用了。

六、综合应用:把这几招组合起来

6.1 应用场景

这些技巧主要用在需要大规模采集网页内容但目标网站有一定反爬能力的场景,比如抓取电商商品信息、新闻聚合、价格监控等。它们不是万能的,但能绕过大部分中小型网站的检测。对于大厂如某宝、某东,还需要更高级的手段如验证码识别、使用Headful模式(有界面)等。

6.2 技术优缺点

方法 优点 缺点
修改UserAgent 简单高效,能过第一道门槛 遇到检测浏览器指纹的网站无效
隐藏自动化标识 能过掉大部分检测webdriver的网站 需要不断更新脚本应对新检测方式
代理轮换 有效分散IP压力,降低被封概率 代理质量影响速度,免费代理不可靠
模拟人类行为 使流量更像真实用户,提升隐蔽性 增加了爬取时长,需要更复杂的代码

6.3 注意事项

  • 不要过度伪装:比如使用非常老的浏览器版本,反而会引起怀疑。
  • 保持特征一致性:UserAgent、语言、时区等要和一个真实操作系统匹配,比如你用了Windows的UA,却暴露了Linux的字体渲染,也可能被识别。
  • 合理设置等待:如果每个页面都固定等待5秒,也很容易被识别为机器,应该用随机分布(如正态分布)。
  • 注意会话管理:如果网站需要登录,你的代理轮换可能会导致每次都是新会话,需要处理好cookie和session。
  • 法律合规:确保你的爬虫不违反网站robots.txt以及当地法律。

七、总结

写Selenium反爬虫脚本的核心就是“模仿”:模仿真实的浏览器头、模仿真实的浏览器行为、模仿真实的访问模式。没有一劳永逸的方法,因为反爬虫技术也在不断升级。但是掌握了修改UserAgent、隐藏自动化标识、代理轮换和模拟人类行为这四板斧,你已经能应对绝大多数常见的反爬检测了。实际开发中,建议从最简单的UserAgent开始,一步步加上其他措施,并随时观察目标网站的响应(比如是否弹出验证码),逐步调优。记住,爬虫和反爬虫是一场持续的技术博弈。