跳到主要内容

Selenium 浏览器自动化

Selenium 是一个强大的浏览器自动化工具,最初用于 Web 应用测试,现在广泛用于爬虫开发。它可以模拟真实用户操作浏览器,能够处理 JavaScript 渲染的动态页面。

官方文档

本教程内容基于 Selenium 官方文档

为什么需要 Selenium?

传统的 requests + BeautifulSoup 方案无法处理以下情况:

  • JavaScript 动态渲染:页面内容通过 JS 加载
  • 需要交互操作:点击按钮、滚动页面、填写表单
  • 登录验证:需要模拟登录才能访问的内容
  • 反爬检测:网站检测浏览器指纹

Selenium 通过控制真实浏览器来解决这些问题,但代价是速度较慢、资源消耗较大。

安装配置

安装 Selenium

pip install selenium

# 验证安装
python -c "import selenium; print(selenium.__version__)"

安装浏览器驱动

Selenium 需要浏览器驱动来控制浏览器。推荐使用 webdriver-manager 自动管理驱动:

pip install webdriver-manager

手动安装驱动(可选)

如果需要手动安装,请根据浏览器类型下载对应驱动:

浏览器驱动名称下载地址
ChromeChromeDriverhttps://chromedriver.chromium.org/
FirefoxGeckoDriverhttps://github.com/mozilla/geckodriver
EdgeEdgeDriverhttps://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/
SafariSafariDrivermacOS 自带,无需下载

驱动版本必须与浏览器版本匹配。下载后将驱动放入系统 PATH 路径中。

快速开始

第一个示例

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

# 配置 Chrome 选项
options = Options()
options.add_argument('--headless') # 无头模式,不显示浏览器窗口

# 创建 WebDriver 实例
driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=options
)

try:
# 访问网页
driver.get('https://www.example.com')

# 获取页面标题
print(f'页面标题: {driver.title}')

# 获取页面源码
print(driver.page_source[:500])

finally:
# 关闭浏览器
driver.quit()

使用 Firefox

from selenium import webdriver
from selenium.webdriver.firefox.service import Service
from selenium.webdriver.firefox.options import Options
from webdriver_manager.firefox import GeckoDriverManager

options = Options()
options.add_argument('--headless')

driver = webdriver.Firefox(
service=Service(GeckoDriverManager().install()),
options=options
)

driver.get('https://www.example.com')
print(driver.title)
driver.quit()

浏览器配置

Chrome 常用配置

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

options = Options()

# 无头模式
options.add_argument('--headless')

# 禁用 GPU(无头模式下推荐)
options.add_argument('--disable-gpu')

# 禁用沙箱(Linux 下可能需要)
options.add_argument('--no-sandbox')

# 禁用共享内存(解决内存不足问题)
options.add_argument('--disable-dev-shm-usage')

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

# 设置 User-Agent
options.add_argument('user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')

# 禁用图片加载(加速爬取)
prefs = {'profile.managed_default_content_settings.images': 2}
options.add_experimental_option('prefs', prefs)

# 禁用自动化检测
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)

# 忽略证书错误
options.add_argument('--ignore-certificate-errors')

driver = webdriver.Chrome(options=options)

启动速度优化

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

options = Options()

# 禁用不必要的功能
options.add_argument('--disable-extensions')
options.add_argument('--disable-infobars')
options.add_argument('--disable-notifications')
options.add_argument('--disable-popup-blocking')

# 禁用日志
options.add_experimental_option('excludeSwitches', ['enable-logging'])

driver = webdriver.Chrome(options=options)

元素定位

Selenium 提供了多种定位元素的方法,通过 By 类指定定位策略。

八种定位方式

from selenium import webdriver
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))
driver.get('https://example.com')

# 1. 通过 ID 定位(最快最准确)
element = driver.find_element(By.ID, 'username')

# 2. 通过 name 属性定位
element = driver.find_element(By.NAME, 'password')

# 3. 通过 class 名定位
element = driver.find_element(By.CLASS_NAME, 'btn-primary')

# 4. 通过标签名定位
element = driver.find_element(By.TAG_NAME, 'input')

# 5. 通过链接文本定位(精确匹配)
element = driver.find_element(By.LINK_TEXT, '登录')

# 6. 通过部分链接文本定位(模糊匹配)
element = driver.find_element(By.PARTIAL_LINK_TEXT, '注册')

# 7. 通过 CSS 选择器定位(推荐)
element = driver.find_element(By.CSS_SELECTOR, 'div.content > a.link')

# 8. 通过 XPath 定位(最灵活)
element = driver.find_element(By.XPATH, '//div[@class="content"]//a[text()="登录"]')

driver.quit()

选择器最佳实践

选择合适的定位策略对爬虫的稳定性和可维护性至关重要。以下是官方推荐的选择器优先级:

选择器优先级(从高到低)

优先级选择器类型说明
1ID (By.ID)最快、最准确,ID 应该是唯一的
2Name (By.NAME)表单元素常用,但可能不唯一
3CSS Selector (By.CSS_SELECTOR)灵活、性能好,推荐用于复杂选择
4Class Name (By.CLASS_NAME)简单易用,但 class 可能重复
5XPath (By.XPATH)最强大,但可读性和性能较差
6Link Text仅用于链接,依赖文本内容
7Tag Name最不具体,通常用于获取同类元素列表

选择器选择原则

  1. 优先使用 ID:如果元素有唯一 ID,这是最佳选择
  2. 语义化选择:选择能表达意图的选择器,如 name="email" 而非 nth-child(3)
  3. 避免依赖结构:不要使用 div/div/div[3]/input 这种依赖 DOM 结构的选择器
  4. 使用稳定属性:优先使用 data-* 等自定义属性,而非动态生成的 class
  5. 避免使用索引//div[3] 这种选择器很脆弱,DOM 变化就会失败

好的选择器示例

# 使用 ID
driver.find_element(By.ID, 'username')

# 使用语义化的 name
driver.find_element(By.NAME, 'email')

# 使用稳定的 data 属性
driver.find_element(By.CSS_SELECTOR, '[data-testid="submit-button"]')

# 使用语义化的 CSS 选择器
driver.find_element(By.CSS_SELECTOR, 'form.login-form input[name="password"]')

# 组合使用(精确且稳定)
driver.find_element(By.CSS_SELECTOR, '#login-form .submit-btn')

不好的选择器示例

# 依赖索引,DOM 变化会失败
driver.find_element(By.XPATH, '//div[3]/div[2]/input')

# 使用动态生成的 class
driver.find_element(By.CSS_SELECTOR, '.css-1xy3z ab')

# 依赖完整路径,太脆弱
driver.find_element(By.XPATH, '/html/body/div/div[2]/form/input[1]')

# 使用模糊的标签名
driver.find_element(By.TAG_NAME, 'div') # 页面可能有几十个 div

实践建议

对于需要长期维护的爬虫,建议将选择器集中管理:

# selectors.py - 集中管理选择器
class LoginPageSelectors:
USERNAME_INPUT = (By.ID, 'username')
PASSWORD_INPUT = (By.ID, 'password')
SUBMIT_BUTTON = (By.CSS_SELECTOR, 'button[type="submit"]')
ERROR_MESSAGE = (By.CSS_SELECTOR, '.alert-error')

# 使用
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

def login(driver, username, password):
wait = WebDriverWait(driver, 10)

# 使用集中管理的定位器
wait.until(EC.visibility_of_element_located(LoginPageSelectors.USERNAME_INPUT)).send_keys(username)
driver.find_element(*LoginPageSelectors.PASSWORD_INPUT).send_keys(password)
driver.find_element(*LoginPageSelectors.SUBMIT_BUTTON).click()

# 验证登录结果
try:
error = driver.find_element(*LoginPageSelectors.ERROR_MESSAGE)
return {'success': False, 'error': error.text}
except:
return {'success': True}

定位多个元素

from selenium import webdriver
from selenium.webdriver.common.by import By

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

# find_elements 返回列表,找不到返回空列表
links = driver.find_elements(By.TAG_NAME, 'a')

for link in links:
print(link.text, link.get_attribute('href'))

driver.quit()

CSS 选择器详解

CSS 选择器语法简洁,性能好,是推荐的定位方式:

# 基本 CSS 选择器
driver.find_element(By.CSS_SELECTOR, '#username') # id 选择器
driver.find_element(By.CSS_SELECTOR, '.btn-primary') # class 选择器
driver.find_element(By.CSS_SELECTOR, 'input[name="user"]') # 属性选择器

# 层级关系
driver.find_element(By.CSS_SELECTOR, 'div p') # 后代选择器
driver.find_element(By.CSS_SELECTOR, 'div > p') # 子选择器
driver.find_element(By.CSS_SELECTOR, 'div + p') # 相邻兄弟
driver.find_element(By.CSS_SELECTOR, 'div ~ p') # 后续兄弟

# 属性匹配
driver.find_element(By.CSS_SELECTOR, '[type="text"]') # 精确匹配
driver.find_element(By.CSS_SELECTOR, '[class*="btn"]') # 包含
driver.find_element(By.CSS_SELECTOR, '[href^="http"]') # 开头匹配
driver.find_element(By.CSS_SELECTOR, '[href$=".pdf"]') # 结尾匹配

# 伪类选择器
driver.find_element(By.CSS_SELECTOR, 'li:first-child') # 第一个子元素
driver.find_element(By.CSS_SELECTOR, 'li:nth-child(2)') # 第二个子元素
driver.find_element(By.CSS_SELECTOR, 'li:last-child') # 最后一个子元素

XPath 详解

XPath 功能强大,适合复杂的定位场景:

# 基本 XPath
driver.find_element(By.XPATH, '//input') # 所有 input
driver.find_element(By.XPATH, '//div[@id="main"]') # 带属性的 div
driver.find_element(By.XPATH, '//input[@type="text"]') # 带属性的 input

# 文本定位
driver.find_element(By.XPATH, '//a[text()="登录"]') # 精确文本
driver.find_element(By.XPATH, '//a[contains(text(), "登")]') # 包含文本

# 属性包含
driver.find_element(By.XPATH, '//div[contains(@class, "content")]')

# 层级关系
driver.find_element(By.XPATH, '//div[@id="main"]/p') # 子元素
driver.find_element(By.XPATH, '//div[@id="main"]//p') # 后代元素

# 位置选择
driver.find_element(By.XPATH, '//li[1]') # 第一个 li
driver.find_element(By.XPATH, '//li[last()]') # 最后一个 li
driver.find_element(By.XPATH, '//li[position() < 3]') # 前两个 li

# 逻辑运算
driver.find_element(By.XPATH, '//input[@type="text" and @name="user"]')
driver.find_element(By.XPATH, '//input[@type="text" or @type="password"]')

# 轴定位
driver.find_element(By.XPATH, '//div[@id="main"]/parent::div') # 父元素
driver.find_element(By.XPATH, '//div[@id="main"]/following::p') # 之后的所有 p
driver.find_element(By.XPATH, '//div[@id="main"]/preceding::p') # 之前的所有 p

相对定位(Selenium 4 新增)

Selenium 4 引入了相对定位器,可以根据元素之间的空间关系定位:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.relative_locator import locate_with

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

# 找到某个元素
password = driver.find_element(By.ID, 'password')

# 在 password 上方的元素
username = driver.find_element(locate_with(By.TAG_NAME, 'input').above(password))

# 在 password 下方的元素
submit = driver.find_element(locate_with(By.TAG_NAME, 'button').below(password))

# 在某个元素左侧
label = driver.find_element(locate_with(By.TAG_NAME, 'label').to_left_of(password))

# 在某个元素右侧
icon = driver.find_element(locate_with(By.TAG_NAME, 'span').to_right_of(password))

# 在某个元素附近
nearby = driver.find_element(locate_with(By.TAG_NAME, 'div').near(password))

driver.quit()

等待机制

等待机制是 Selenium 中最关键的概念之一,也是导致测试不稳定(flaky tests)的主要原因。理解并正确使用等待机制对于编写可靠的爬虫至关重要。

为什么需要等待?

浏览器自动化面临一个核心挑战:确保 Web 应用在执行 Selenium 命令时处于正确的状态。这是一个典型的竞态条件问题:

  • 有时浏览器先进入正确状态(一切正常)
  • 有时 Selenium 代码先执行(操作失败)

所有导航命令(如 driver.get())都会根据页面加载策略等待特定的 readyState 值(默认等待 "complete")后才返回控制权。但 readyState 只关心 HTML 中定义的资源加载,而 JavaScript 加载的资源经常会改变页面状态。

典型场景

  1. 动态添加元素:点击按钮后通过 JavaScript 创建新元素
  2. 显示隐藏元素:点击后改变元素的可见性
  3. AJAX 加载:异步请求数据后更新页面

在这些情况下,当 Selenium 准备执行下一个命令时,需要交互的元素可能还不在页面上。

不要使用 sleep

很多初学者的第一反应是使用 time.sleep() 来暂停代码执行:

import time

# 不推荐的做法
driver.find_element(By.ID, 'button').click()
time.sleep(3) # 等待 3 秒
element = driver.find_element(By.ID, 'result') # 可能仍然失败

sleep 的问题

  1. 时间不确定:无法知道确切需要等多久
  2. 可能失败:等待时间不够长时会失败
  3. 效率低下:等待时间过长会浪费大量时间
  4. 维护困难:到处添加 sleep 会使代码难以维护

Selenium 提供了两种更好的同步机制:隐式等待和显式等待。

隐式等待

隐式等待是 Selenium 内置的自动等待机制,通过 timeouts 能力或驱动方法设置:

from selenium import webdriver

driver = webdriver.Chrome()

# 设置隐式等待时间为 10 秒
driver.implicitly_wait(10)

# 如果元素未立即找到,会等待最多 10 秒
element = driver.find_element(By.ID, 'content')

driver.quit()

工作原理

隐式等待是一个全局设置,对整个会话期间的每次元素定位都生效。默认值为 0,意味着如果元素未找到,会立即返回错误。如果设置了隐式等待,驱动会在指定的持续时间内轮询 DOM,直到找到元素或超时。

特点

  • 全局生效,只需设置一次
  • 找到元素后立即返回,不会等待完整时间
  • 找不到元素会抛出 NoSuchElementException
  • 适用于元素存在于 DOM 中的情况

注意事项

隐式等待只等待元素存在于 DOM 中,不保证元素可见或可交互。如果需要等待元素可见或可点击,应该使用显式等待。

显式等待

显式等待是代码中添加的循环,它会轮询应用程序直到特定条件为真,然后退出循环继续执行下一个命令。如果条件在指定的超时时间内未满足,会抛出超时错误。

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

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

# 创建 WebDriverWait 对象,最长等待 10 秒
wait = WebDriverWait(driver, 10)

# 等待元素出现(存在于 DOM 中)
element = wait.until(EC.presence_of_element_located((By.ID, 'content')))

# 等待元素可见(不仅存在,而且显示在页面上)
element = wait.until(EC.visibility_of_element_located((By.ID, 'content')))

# 等待元素可点击(可见且可交互)
element = wait.until(EC.element_to_be_clickable((By.ID, 'submit')))

# 等待元素消失
wait.until(EC.invisibility_of_element_located((By.ID, 'loading')))

# 等待标题包含特定文本
wait.until(EC.title_contains('Example'))

# 等待 URL 包含特定内容
wait.until(EC.url_contains('dashboard'))

driver.quit()

显式等待的优势

  1. 精确控制:可以指定每个位置需要等待的确切条件
  2. 自动处理:Wait 类默认会等待元素存在
  3. 灵活定制:可以自定义等待条件、轮询间隔等

常用等待条件

expected_conditions 模块提供了丰富的预定义条件,涵盖了大多数常见场景:

from selenium.webdriver.support import expected_conditions as EC

# 元素相关
EC.presence_of_element_located(locator) # 元素存在于 DOM
EC.visibility_of_element_located(locator) # 元素可见
EC.invisibility_of_element_located(locator) # 元素不可见
EC.element_to_be_clickable(locator) # 元素可点击
EC.element_to_be_selected(locator) # 元素被选中
EC.presence_of_all_elements_located(locator) # 所有匹配元素

# 文本相关
EC.text_to_be_present_in_element(locator, text) # 元素包含文本
EC.text_to_be_present_in_element_value(locator, text) # value 包含文本

# 页面相关
EC.title_is(title) # 标题等于
EC.title_contains(title) # 标题包含
EC.url_to_be(url) # URL 等于
EC.url_contains(url) # URL 包含

# 框架和窗口
EC.frame_to_be_available_and_switch_to_it(locator) # 框架可用
EC.new_window_is_opened(current_handles) # 新窗口打开
EC.number_of_windows_to_be(num) # 窗口数量

# Alert
EC.alert_is_present() # 弹窗出现

自定义等待条件

对于预定义条件无法满足的场景,可以使用 lambda 表达式或自定义函数:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait

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

# 使用 lambda 表达式
wait = WebDriverWait(driver, 10)
element = wait.until(lambda d: d.find_element(By.ID, 'content'))

# 自定义条件函数
def element_has_text(driver, locator, text):
"""检查元素是否包含指定文本"""
element = driver.find_element(*locator)
return text in element.text

# 使用自定义条件
wait.until(lambda d: element_has_text(d, (By.ID, 'content'), '完成'))

# 更复杂的自定义条件:等待元素属性变化
def element_attribute_contains(locator, attribute, value):
"""返回一个检查元素属性是否包含指定值的条件"""
def _predicate(driver):
element = driver.find_element(*locator)
return value in element.get_attribute(attribute)
return _predicate

# 使用方式
wait.until(element_attribute_contains((By.ID, 'status'), 'class', 'success'))

driver.quit()

Fluent Wait(高级等待)

Fluent Wait 是显式等待的高级形式,提供更精细的控制,可以设置轮询间隔、忽略特定异常等:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import (
NoSuchElementException,
ElementNotInteractableException,
StaleElementReferenceException
)

driver = webdriver.Chrome()

# 创建 Fluent Wait
wait = WebDriverWait(
driver,
timeout=10, # 最长等待时间(秒)
poll_frequency=0.5, # 轮询间隔(默认 0.5 秒)
ignored_exceptions=[ # 忽略的异常列表
NoSuchElementException,
ElementNotInteractableException
]
)

# 使用等待
element = wait.until(lambda d: d.find_element(By.ID, 'content'))

driver.quit()

Fluent Wait 参数说明

参数类型默认值说明
timeoutfloat必填最长等待时间(秒)
poll_frequencyfloat0.5轮询间隔(秒)
ignored_exceptionslist[]轮询期间忽略的异常类型列表

适用场景

  • 元素加载过程中会出现临时异常(如 StaleElementReferenceException
  • 需要自定义轮询频率以优化性能
  • 元素状态变化频繁,需要忽略特定异常

等待机制对比

特性隐式等待显式等待Fluent Wait
作用范围全局局部局部
等待条件元素存在可自定义可自定义
精确控制
可忽略异常
轮询间隔固定固定可自定义
适用场景简单页面大多数场景复杂场景

等待机制选择建议

重要警告:不要混用隐式等待和显式等待

混用隐式等待和显式等待可能导致不可预测的等待时间。例如,设置 10 秒的隐式等待和 15 秒的显式等待,可能导致实际等待 20 秒后才超时。

官方推荐:只使用显式等待,避免使用隐式等待。

选择建议

场景推荐方式原因
简单静态页面可以不用等待元素立即可用
动态加载内容显式等待精确控制等待条件
需要特定状态显式等待可自定义条件
元素有临时异常Fluent Wait可忽略异常
快速原型开发隐式等待简单易用(但不推荐生产使用)

最佳实践

# 推荐:统一使用显式等待
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class PageBase:
"""页面基类,封装常用等待方法"""

def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)

def wait_for_element(self, locator):
"""等待元素可见"""
return self.wait.until(EC.visibility_of_element_located(locator))

def wait_for_clickable(self, locator):
"""等待元素可点击"""
return self.wait.until(EC.element_to_be_clickable(locator))

def wait_for_text(self, locator, text):
"""等待元素包含文本"""
return self.wait.until(EC.text_to_be_present_in_element(locator, text))

def wait_until(self, condition):
"""自定义等待条件"""
return self.wait.until(condition)

元素操作

基本操作

from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get('https://example.com/form')

# 点击元素
button = driver.find_element(By.ID, 'submit')
button.click()

# 输入文本
input_box = driver.find_element(By.ID, 'username')
input_box.clear() # 先清空
input_box.send_keys('admin') # 输入内容

# 获取元素文本
title = driver.find_element(By.TAG_NAME, 'h1')
print(title.text)

# 获取元素属性
link = driver.find_element(By.TAG_NAME, 'a')
print(link.get_attribute('href'))
print(link.get_attribute('target'))

# 获取元素的 HTML
div = driver.find_element(By.ID, 'content')
print(div.get_attribute('innerHTML'))

driver.quit()

表单操作

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select

driver = webdriver.Chrome()
driver.get('https://example.com/form')

# 文本输入
username = driver.find_element(By.NAME, 'username')
username.send_keys('admin')

# 密码输入
password = driver.find_element(By.NAME, 'password')
password.send_keys('123456')

# 复选框
checkbox = driver.find_element(By.NAME, 'remember')
if not checkbox.is_selected():
checkbox.click()

# 单选框
radio = driver.find_element(By.NAME, 'gender')
radio.click()

# 下拉选择框
select_element = driver.find_element(By.NAME, 'country')
select = Select(select_element)

# 选择方式
select.select_by_visible_text('China') # 通过可见文本
select.select_by_value('cn') # 通过 value 属性
select.select_by_index(0) # 通过索引

# 获取选中项
selected_option = select.first_selected_option
print(selected_option.text)

# 取消选择
select.deselect_all()

# 文件上传
file_input = driver.find_element(By.NAME, 'file')
file_input.send_keys('C:/path/to/file.txt') # 发送文件路径

driver.quit()

键盘操作

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

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

input_box = driver.find_element(By.NAME, 'search')
input_box.send_keys('Python')

# 特殊按键
input_box.send_keys(Keys.ENTER) # 回车
input_box.send_keys(Keys.TAB) # Tab 键
input_box.send_keys(Keys.ESCAPE) # Esc 键
input_box.send_keys(Keys.BACK_SPACE) # 退格
input_box.send_keys(Keys.DELETE) # 删除
input_box.send_keys(Keys.CONTROL, 'a') # Ctrl+A 全选
input_box.send_keys(Keys.CONTROL, 'c') # Ctrl+C 复制
input_box.send_keys(Keys.CONTROL, 'v') # Ctrl+V 粘贴

# 组合键
from selenium.webdriver.common.action_chains import ActionChains

ActionChains(driver).key_down(Keys.CONTROL).send_keys('a').key_up(Keys.CONTROL).perform()

driver.quit()

鼠标操作

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

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

element = driver.find_element(By.ID, 'target')
actions = ActionChains(driver)

# 单击
actions.click(element).perform()

# 双击
actions.double_click(element).perform()

# 右键点击
actions.context_click(element).perform()

# 悬停(鼠标移到元素上)
actions.move_to_element(element).perform()

# 拖拽
source = driver.find_element(By.ID, 'source')
target = driver.find_element(By.ID, 'target')
actions.drag_and_drop(source, target).perform()

# 按偏移量拖拽
actions.click_and_hold(source).move_by_offset(100, 0).release().perform()

# 移动到元素偏移位置
actions.move_to_element_with_offset(element, 10, 10).perform()

driver.quit()

滚动操作

from selenium import webdriver

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

# 滚动到页面底部
driver.execute_script('window.scrollTo(0, document.body.scrollHeight)')

# 滚动到页面顶部
driver.execute_script('window.scrollTo(0, 0)')

# 滚动到指定位置
driver.execute_script('window.scrollTo(0, 500)')

# 滚动到元素可见
element = driver.find_element(By.ID, 'target')
driver.execute_script('arguments[0].scrollIntoView()', element)

# 水平滚动
driver.execute_script('window.scrollTo(500, 0)')

driver.quit()

多窗口与框架

多窗口处理

from selenium import webdriver
from selenium.webdriver.common.by import By

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

# 获取当前窗口句柄
main_window = driver.current_window_handle
print(f'主窗口: {main_window}')

# 点击链接打开新窗口
driver.find_element(By.LINK_TEXT, '打开新窗口').click()

# 获取所有窗口句柄
all_windows = driver.window_handles
print(f'所有窗口: {all_windows}')

# 切换到新窗口
for window in all_windows:
if window != main_window:
driver.switch_to.window(window)
break

# 在新窗口中操作
print(driver.title)

# 关闭当前窗口
driver.close()

# 切换回主窗口
driver.switch_to.window(main_window)

driver.quit()

iframe 处理

from selenium import webdriver
from selenium.webdriver.common.by import By

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

# 通过索引切换(从 0 开始)
driver.switch_to.frame(0)

# 通过 name 或 id 切换
driver.switch_to.frame('frame_name')
driver.switch_to.frame('frame_id')

# 通过 WebElement 切换
frame_element = driver.find_element(By.TAG_NAME, 'iframe')
driver.switch_to.frame(frame_element)

# 在 frame 内操作
print(driver.find_element(By.ID, 'content').text)

# 切换回主文档
driver.switch_to.default_content()

# 切换到父 frame(嵌套 frame 时使用)
driver.switch_to.parent_frame()

driver.quit()

Alert 弹窗处理

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

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

# 触发 alert
driver.find_element(By.ID, 'alert_button').click()

# 等待 alert 出现
wait = WebDriverWait(driver, 10)
alert = wait.until(EC.alert_is_present())

# 获取 alert 文本
print(alert.text)

# 接受 alert(点击确定)
alert.accept()

# 取消 alert(点击取消)
alert.dismiss()

# 在 prompt 中输入文本
alert.send_keys('输入内容')
alert.accept()

driver.quit()

执行 JavaScript

Selenium 可以直接执行 JavaScript 代码,实现更灵活的操作:

from selenium import webdriver

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

# 执行简单脚本
driver.execute_script('alert("Hello")')

# 执行脚本并获取返回值
title = driver.execute_script('return document.title')
print(title)

# 传递参数
element = driver.find_element(By.ID, 'target')
driver.execute_script('arguments[0].style.border = "3px solid red"', element)

# 滚动到元素
driver.execute_script('arguments[0].scrollIntoView()', element)

# 点击被遮挡的元素
driver.execute_script('arguments[0].click()', element)

# 修改元素属性
driver.execute_script('arguments[0].setAttribute("value", "new value")', element)

# 获取元素的计算样式
style = driver.execute_script('return window.getComputedStyle(arguments[0])', element)

# 异步执行脚本
driver.execute_async_script('''
var callback = arguments[arguments.length - 1];
setTimeout(function() {
callback('done');
}, 1000);
''')

driver.quit()
from selenium import webdriver

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

# 获取所有 Cookie
cookies = driver.get_cookies()
for cookie in cookies:
print(cookie)

# 获取单个 Cookie
cookie = driver.get_cookie('session_id')
print(cookie)

# 添加 Cookie
driver.add_cookie({
'name': 'user_token',
'value': 'abc123',
'domain': 'example.com',
'path': '/',
'secure': True
})

# 删除单个 Cookie
driver.delete_cookie('user_token')

# 删除所有 Cookie
driver.delete_all_cookies()

driver.quit()

截图与页面源码

from selenium import webdriver

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

# 截取整个页面
driver.save_screenshot('screenshot.png')

# 截取元素
element = driver.find_element(By.ID, 'content')
element.screenshot('element.png')

# 获取截图为 base64
screenshot_base64 = driver.get_screenshot_as_base64()

# 获取截图为二进制数据
screenshot_png = driver.get_screenshot_as_png()

# 获取页面源码
html = driver.page_source
with open('page.html', 'w', encoding='utf-8') as f:
f.write(html)

driver.quit()

无头模式爬虫实战

下面是一个完整的无头模式爬虫示例:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
import time
import json
import random

class SeleniumSpider:
def __init__(self, headless=True):
self.options = Options()

if headless:
self.options.add_argument('--headless')

self.options.add_argument('--disable-gpu')
self.options.add_argument('--no-sandbox')
self.options.add_argument('--disable-dev-shm-usage')
self.options.add_argument('--window-size=1920,1080')

# 设置 User-Agent
user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
]
self.options.add_argument(f'user-agent={random.choice(user_agents)}')

# 禁用自动化检测
self.options.add_experimental_option('excludeSwitches', ['enable-automation'])
self.options.add_experimental_option('useAutomationExtension', False)

# 创建驱动
self.driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=self.options
)

# 设置隐式等待
self.driver.implicitly_wait(10)

# 显式等待
self.wait = WebDriverWait(self.driver, 15)

def get(self, url):
"""访问页面"""
self.driver.get(url)
# 随机延迟,模拟人类行为
time.sleep(random.uniform(0.5, 1.5))

def wait_for_element(self, locator, timeout=15):
"""等待元素出现"""
wait = WebDriverWait(self.driver, timeout)
return wait.until(EC.presence_of_element_located(locator))

def wait_for_clickable(self, locator, timeout=15):
"""等待元素可点击"""
wait = WebDriverWait(self.driver, timeout)
return wait.until(EC.element_to_be_clickable(locator))

def scroll_to_bottom(self):
"""滚动到页面底部"""
self.driver.execute_script(
'window.scrollTo(0, document.body.scrollHeight)'
)
time.sleep(1)

def scroll_page(self):
"""模拟滚动页面"""
total_height = self.driver.execute_script(
'return document.body.scrollHeight'
)
for i in range(0, total_height, random.randint(200, 400)):
self.driver.execute_script(f'window.scrollTo(0, {i})')
time.sleep(random.uniform(0.1, 0.3))

def extract_data(self, url):
"""提取数据示例"""
self.get(url)

# 等待内容加载
self.wait_for_element((By.CLASS_NAME, 'content'))

# 滚动页面加载更多内容
self.scroll_page()

# 提取数据
items = []
elements = self.driver.find_elements(By.CSS_SELECTOR, '.item')

for elem in elements:
try:
title = elem.find_element(By.CSS_SELECTOR, '.title').text
price = elem.find_element(By.CSS_SELECTOR, '.price').text
link = elem.find_element(By.TAG_NAME, 'a').get_attribute('href')

items.append({
'title': title,
'price': price,
'link': link
})
except Exception as e:
print(f'提取失败: {e}')
continue

return items

def save_to_json(self, data, filename):
"""保存数据到 JSON"""
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
print(f'已保存 {len(data)} 条数据到 {filename}')

def close(self):
"""关闭浏览器"""
self.driver.quit()


# 使用示例
if __name__ == '__main__':
spider = SeleniumSpider(headless=True)

try:
# 访问页面
spider.get('https://example.com')

# 获取标题
print(f'页面标题: {spider.driver.title}')

# 提取数据
items = spider.extract_data('https://example.com/products')

# 保存数据
spider.save_to_json(items, 'products.json')

finally:
spider.close()

处理动态加载

很多网站使用无限滚动或点击加载更多,需要特殊处理:

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
import time

driver = webdriver.Chrome()
driver.get('https://example.com/infinite-scroll')

wait = WebDriverWait(driver, 10)

# 记录已加载的项目数
last_count = 0
max_scrolls = 10 # 限制滚动次数

for i in range(max_scrolls):
# 获取当前项目数
items = driver.find_elements(By.CSS_SELECTOR, '.item')
current_count = len(items)

print(f'滚动 {i+1}: 当前 {current_count} 个项目')

# 如果没有新项目加载,停止
if current_count == last_count:
print('没有更多内容')
break

last_count = current_count

# 滚动到底部
driver.execute_script(
'window.scrollTo(0, document.body.scrollHeight)'
)

# 等待新内容加载
time.sleep(2)

# 提取所有数据
items = driver.find_elements(By.CSS_SELECTOR, '.item')
print(f'总共加载 {len(items)} 个项目')

driver.quit()

绕过反爬检测

隐藏自动化特征

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

options = Options()

# 禁用自动化标志
options.add_experimental_option('excludeSwitches', ['enable-automation'])
options.add_experimental_option('useAutomationExtension', False)

driver = webdriver.Chrome(options=options)

# 执行脚本隐藏 webdriver 属性
driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
'source': '''
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
'''
})

driver.get('https://example.com')

模拟人类行为

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

driver = webdriver.Chrome()

def human_like_input(element, text):
"""模拟人类输入"""
for char in text:
element.send_keys(char)
time.sleep(random.uniform(0.05, 0.15))

def human_like_click(element):
"""模拟人类点击"""
actions = ActionChains(driver)

# 移动到元素附近
actions.move_to_element(element)
time.sleep(random.uniform(0.1, 0.3))

# 点击
actions.click()
actions.perform()

# 随机延迟
time.sleep(random.uniform(0.5, 1.5))

# 使用
driver.get('https://example.com')
input_box = driver.find_element(By.NAME, 'search')
human_like_input(input_box, 'Python 爬虫')

button = driver.find_element(By.ID, 'submit')
human_like_click(button)

性能优化

禁用图片和 CSS

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

options = Options()

# 禁用图片
prefs = {
'profile.managed_default_content_settings.images': 2,
'profile.managed_default_content_settings.stylesheets': 2,
}
options.add_experimental_option('prefs', prefs)

driver = webdriver.Chrome(options=options)

复用浏览器会话

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

options = Options()

# 使用远程调试端口
options.add_argument('--remote-debugging-port=9222')

driver = webdriver.Chrome(options=options)

# 后续可以连接到同一个端口复用浏览器
# options.add_argument('--remote-debugging-port=9222')
# driver = webdriver.Chrome(options=options)

错误处理

编写健壮的 Selenium 爬虫需要正确处理各种异常。了解常见的异常类型及其处理方式,可以大大提高爬虫的稳定性。

常见异常类型

Selenium 定义了丰富的异常类型,了解它们有助于精确定位问题:

from selenium.common.exceptions import (
# 元素相关异常
NoSuchElementException, # 元素未找到
NoSuchFrameException, # iframe 未找到
NoSuchWindowException, # 窗口未找到
NoSuchAttributeException, # 属性不存在

# 交互相关异常
ElementNotVisibleException, # 元素不可见
ElementNotInteractableException, # 元素不可交互
ElementClickInterceptedException, # 点击被拦截
StaleElementReferenceException, # 元素引用过期

# 超时相关异常
TimeoutException, # 操作超时

# 其他异常
InvalidSelectorException, # 选择器无效
SessionNotCreatedException, # 会话创建失败
WebDriverException, # WebDriver 通用异常
)

异常详解与处理

NoSuchElementException - 元素未找到

最常见异常,表示选择器未匹配到任何元素:

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException

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

try:
element = driver.find_element(By.ID, 'non-existent')
except NoSuchElementException:
print('元素不存在,可能原因:')
print('1. 选择器错误')
print('2. 页面未加载完成')
print('3. 元素在 iframe 中')
print('4. 元素动态生成,需要等待')

# 推荐:使用 find_elements 检查元素是否存在
elements = driver.find_elements(By.ID, 'non-existent')
if elements:
elements[0].click()
else:
print('元素不存在,跳过操作')

driver.quit()

TimeoutException - 操作超时

等待条件未满足时抛出:

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.common.exceptions import TimeoutException

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

wait = WebDriverWait(driver, 10)

try:
element = wait.until(EC.presence_of_element_located((By.ID, 'content')))
except TimeoutException:
print('等待超时,可能原因:')
print('1. 页面加载缓慢')
print('2. 元素选择器错误')
print('3. 元素在 iframe 中')
print('4. 页面结构与预期不符')

# 可以尝试增加等待时间或使用其他策略
try:
element = WebDriverWait(driver, 30).until(
EC.presence_of_element_located((By.ID, 'content'))
)
except TimeoutException:
print('即使延长等待时间仍未找到元素')

driver.quit()

StaleElementReferenceException - 元素引用过期

这是一个常见但容易被忽视的异常,发生在获取元素引用后页面发生变化(如刷新、AJAX 更新):

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import StaleElementReferenceException

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

# 获取元素引用
element = driver.find_element(By.ID, 'item')

# 页面可能在这里发生了变化(如 AJAX 更新)

try:
element.click() # 可能抛出 StaleElementReferenceException
except StaleElementReferenceException:
# 重新查找元素
element = driver.find_element(By.ID, 'item')
element.click()

# 推荐:封装重试逻辑
def safe_click(driver, locator, max_retries=3):
"""安全点击,自动重试过期元素"""
for attempt in range(max_retries):
try:
element = driver.find_element(*locator)
element.click()
return True
except StaleElementReferenceException:
if attempt == max_retries - 1:
raise
continue
return False

driver.quit()

ElementNotInteractableException - 元素不可交互

元素存在但无法与之交互(可能被遮挡、禁用或不可见):

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.common.exceptions import ElementNotInteractableException

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

try:
driver.find_element(By.ID, 'button').click()
except ElementNotInteractableException:
# 尝试使用 JavaScript 点击
element = driver.find_element(By.ID, 'button')
driver.execute_script('arguments[0].click()', element)

# 或者等待元素可交互
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, 'button')))
element.click()

driver.quit()

综合异常处理示例

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.common.exceptions import (
NoSuchElementException,
TimeoutException,
StaleElementReferenceException,
ElementNotInteractableException,
WebDriverException
)
import logging

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class RobustSpider:
"""健壮的 Selenium 爬虫基类"""

def __init__(self, headless=True):
self.driver = self._create_driver(headless)
self.wait = WebDriverWait(self.driver, 10)

def _create_driver(self, headless):
"""创建 WebDriver"""
from selenium.webdriver.chrome.options import Options

options = Options()
if headless:
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')

return webdriver.Chrome(options=options)

def safe_find(self, locator, timeout=10):
"""安全查找元素,带重试机制"""
wait = WebDriverWait(self.driver, timeout)
try:
return wait.until(EC.presence_of_element_located(locator))
except TimeoutException:
logger.warning(f'元素未找到: {locator}')
return None

def safe_click(self, locator, timeout=10, retries=3):
"""安全点击元素"""
for attempt in range(retries):
try:
wait = WebDriverWait(self.driver, timeout)
element = wait.until(EC.element_to_be_clickable(locator))
element.click()
return True
except StaleElementReferenceException:
logger.warning(f'元素引用过期,重试 {attempt + 1}/{retries}')
continue
except (TimeoutException, NoSuchElementException):
logger.error(f'元素不可点击: {locator}')
return False
except ElementNotInteractableException:
# 尝试 JavaScript 点击
logger.info('尝试使用 JavaScript 点击')
element = self.driver.find_element(*locator)
self.driver.execute_script('arguments[0].click()', element)
return True
return False

def safe_input(self, locator, text, timeout=10):
"""安全输入文本"""
try:
wait = WebDriverWait(self.driver, timeout)
element = wait.until(EC.visibility_of_element_located(locator))
element.clear()
element.send_keys(text)
return True
except (TimeoutException, NoSuchElementException):
logger.error(f'无法输入文本,元素不可见: {locator}')
return False

def get_with_retry(self, url, max_retries=3):
"""带重试的页面访问"""
for attempt in range(max_retries):
try:
self.driver.get(url)
return True
except WebDriverException as e:
logger.warning(f'页面访问失败: {e}, 重试 {attempt + 1}/{max_retries}')
if attempt == max_retries - 1:
raise
return False

def close(self):
"""关闭浏览器"""
if self.driver:
self.driver.quit()


# 使用示例
if __name__ == '__main__':
spider = RobustSpider(headless=True)

try:
spider.get_with_retry('https://example.com')

# 安全查找元素
element = spider.safe_find((By.ID, 'content'))
if element:
print(f'找到元素: {element.text[:50]}')

# 安全点击
spider.safe_click((By.ID, 'button'))

finally:
spider.close()

小结

本章我们学习了:

  1. 安装配置 - Selenium 和浏览器驱动的安装,推荐使用 webdriver-manager 自动管理驱动
  2. 浏览器配置 - 无头模式、窗口大小、User-Agent、性能优化选项等
  3. 元素定位 - 八种传统定位方式、选择器最佳实践、相对定位器
  4. 等待机制 - 隐式等待、显式等待、Fluent Wait 的区别和使用场景
  5. 元素操作 - 点击、输入、表单、键盘鼠标操作
  6. 高级操作 - 多窗口、iframe、Alert、JavaScript 执行
  7. 错误处理 - 常见异常类型、异常处理最佳实践、健壮爬虫设计
  8. 实战技巧 - 无头模式爬虫、动态加载、反爬绕过、性能优化

最佳实践总结

  1. 选择器优先级:优先使用 ID 和 CSS 选择器,避免依赖 DOM 结构
  2. 等待策略:统一使用显式等待,避免混用隐式等待和显式等待
  3. 错误处理:捕获特定异常,实现重试机制,记录日志
  4. 资源管理:使用 try/finally 或上下文管理器确保浏览器正确关闭
  5. 性能优化:无头模式、禁用图片、复用会话

进阶学习

练习

  1. 使用 Selenium 登录一个网站并获取登录后的页面内容
  2. 实现一个处理无限滚动页面的爬虫
  3. 编写一个自动填写表单的脚本