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
手动安装驱动(可选)
如果需要手动安装,请根据浏览器类型下载对应驱动:
| 浏览器 | 驱动名称 | 下载地址 |
|---|---|---|
| Chrome | ChromeDriver | https://chromedriver.chromium.org/ |
| Firefox | GeckoDriver | https://github.com/mozilla/geckodriver |
| Edge | EdgeDriver | https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ |
| Safari | SafariDriver | macOS 自带,无需下载 |
驱动版本必须与浏览器版本匹配。下载后将驱动放入系统 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()
选择器最佳实践
选择合适的定位策略对爬虫的稳定性和可维护性至关重要。以下是官方推荐的选择器优先级:
选择器优先级(从高到低):
| 优先级 | 选择器类型 | 说明 |
|---|---|---|
| 1 | ID (By.ID) | 最快、最准确,ID 应该是唯一的 |
| 2 | Name (By.NAME) | 表单元素常用,但可能不唯一 |
| 3 | CSS Selector (By.CSS_SELECTOR) | 灵活、性能好,推荐用于复杂选择 |
| 4 | Class Name (By.CLASS_NAME) | 简单易用,但 class 可能重复 |
| 5 | XPath (By.XPATH) | 最强大,但可读性和性能较差 |
| 6 | Link Text | 仅用于链接,依赖文本内容 |
| 7 | Tag Name | 最不具体,通常用于获取同类元素列表 |
选择器选择原则:
- 优先使用 ID:如果元素有唯一 ID,这是最佳选择
- 语义化选择:选择能表达意图的选择器,如
name="email"而非nth-child(3) - 避免依赖结构:不要使用
div/div/div[3]/input这种依赖 DOM 结构的选择器 - 使用稳定属性:优先使用
data-*等自定义属性,而非动态生成的 class - 避免使用索引:
//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 加载的资源经常会改变页面状态。
典型场景:
- 动态添加元素:点击按钮后通过 JavaScript 创建新元素
- 显示隐藏元素:点击后改变元素的可见性
- 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 的问题:
- 时间不确定:无法知道确切需要等多久
- 可能失败:等待时间不够长时会失败
- 效率低下:等待时间过长会浪费大量时间
- 维护困难:到处添加 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()
显式等待的优势:
- 精确控制:可以指定每个位置需要等待的确切条件
- 自动处理:Wait 类默认会等待元素存在
- 灵活定制:可以自定义等待条件、轮询间隔等
常用等待条件
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 参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
timeout | float | 必填 | 最长等待时间(秒) |
poll_frequency | float | 0.5 | 轮询间隔(秒) |
ignored_exceptions | list | [] | 轮询期间忽略的异常类型列表 |
适用场景:
- 元素加载过程中会出现临时异常(如
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()
Cookie 管理
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()
小结
本章我们学习了:
- 安装配置 - Selenium 和浏览器驱动的安装,推荐使用 webdriver-manager 自动管理驱动
- 浏览器配置 - 无头模式、窗口大小、User-Agent、性能优化选项等
- 元素定位 - 八种传统定位方式、选择器最佳实践、相对定位器
- 等待机制 - 隐式等待、显式等待、Fluent Wait 的区别和使用场景
- 元素操作 - 点击、输入、表单、键盘鼠标操作
- 高级操作 - 多窗口、iframe、Alert、JavaScript 执行
- 错误处理 - 常见异常类型、异常处理最佳实践、健壮爬虫设计
- 实战技巧 - 无头模式爬虫、动态加载、反爬绕过、性能优化
最佳实践总结
- 选择器优先级:优先使用 ID 和 CSS 选择器,避免依赖 DOM 结构
- 等待策略:统一使用显式等待,避免混用隐式等待和显式等待
- 错误处理:捕获特定异常,实现重试机制,记录日志
- 资源管理:使用
try/finally或上下文管理器确保浏览器正确关闭 - 性能优化:无头模式、禁用图片、复用会话
进阶学习
- 官方文档:https://www.selenium.dev/documentation/
- WebDriver W3C 规范:https://w3c.github.io/webdriver/
- Selenium Grid:用于分布式测试和爬取
- BiDi API:双向通信协议,支持更复杂的场景
练习
- 使用 Selenium 登录一个网站并获取登录后的页面内容
- 实现一个处理无限滚动页面的爬虫
- 编写一个自动填写表单的脚本