1. 项目概述:从“点点点”到“自动化”的质变

干了这么多年测试,最烦的就是那些重复的、机械的网页操作。比如每天上班第一件事,就是登录后台系统,导出昨天的数据报表,再手动填到另一个系统里生成图表。一次两次还行,日复一日,不仅枯燥,还容易因为手滑点错。后来我开始接触自动化测试,尤其是用 Python 的 Selenium 库,感觉就像给枯燥的“点点点”工作装上了机械臂。它不是什么高深莫测的黑科技,本质上就是一个能按照你写好的脚本,去模拟真人操作浏览器(点击、输入、滚动等)的工具。今天,我就结合自己踩过的无数坑,来聊聊怎么用 Python + Selenium 把这套“机械臂”玩转,让你从重复劳动中彻底解放出来。

简单说,Selenium 就是一个浏览器自动化工具。你写一段 Python 代码,告诉它:“打开 Chrome,访问某个网址,在用户名框里输入‘admin’,在密码框里输入‘123456’,然后点击登录按钮。” 它就能一丝不苟地执行。这不仅能用于测试网页功能是否正常(这就是自动化测试的核心),还能用来做一些日常的、规则明确的网页数据抓取或操作任务,比如定时签到、批量下载、数据填报等。无论你是测试工程师、数据分析师,还是任何需要和网页打交道的开发者,掌握它都能极大提升效率。接下来,我会从环境搭建、核心操作、实战技巧到避坑指南,带你完整走一遍。

2. 环境准备与核心工具选型

工欲善其事,必先利其器。在开始写自动化脚本之前,一套稳定、匹配的环境是成功的基石。这里面的坑,我几乎一个没落全踩过。

2.1 Python 环境搭建:别在版本上栽跟头

首先肯定是 Python。我的建议是, 直接安装 Python 3.7 到 3.11 之间的版本 。太老的版本(如 2.7)库支持不好,太新的版本(如 3.12+)有时会遇到一些第三方库兼容性问题。去 Python 官网下载安装包时,务必勾选 “Add Python to PATH” 这个选项,这是为了能在命令行里直接使用 python pip 命令。

安装完成后,打开命令行(CMD 或 Terminal),输入 python --version 检查是否安装成功。接下来,我们需要一个写代码的地方。对于新手,我强烈推荐 Visual Studio Code (VSCode) 。它轻量、免费,而且通过安装插件能获得非常好的 Python 开发体验。在 VSCode 中,你需要安装官方的 “Python” 扩展,它会帮你管理 Python 解释器、提供代码提示、调试等功能。配置环境时,在 VSCode 左下角选择你刚安装的 Python 解释器,这样就完成了最基本的编码环境搭建。

注意:很多教程会教你在系统环境变量里手动配置 PATH,如果你在安装时忘了勾选,确实需要手动加一下。但更常见的问题是,电脑里装了多个 Python(比如 Anaconda 自带一个,官网又装一个),导致命令混乱。这时候,在 VSCode 里明确指定使用哪一个解释器至关重要。

2.2 Selenium 库安装与浏览器驱动配置

这是新手最容易卡住的地方。Selenium 分为两部分: Python 客户端库 浏览器驱动

第一步,安装 Selenium 库。 这个很简单,在命令行里执行:

pip install selenium

如果速度慢,可以使用国内镜像源,例如:

pip install selenium -i https://pypi.tuna.tsinghua.edu.cn/simple

第二步,下载浏览器驱动。 Selenium 本身不能直接控制浏览器,它需要通过一个名为 “WebDriver” 的中间件来发送指令。你需要根据你使用的浏览器类型和版本,下载对应的驱动。

  • Chrome 浏览器 :驱动叫 ChromeDriver 。去 ChromeDriver 官网 下载。 关键点来了:驱动版本必须与你的 Chrome 浏览器主版本号完全一致! 你可以在 Chrome 的“设置”->“关于 Chrome”里查看版本号(比如 120.0.6099.110),然后下载版本号为 120 的 ChromeDriver。
  • Firefox 浏览器 :驱动叫 geckodriver 。去 GitHub 发布页 下载。
  • Microsoft Edge 浏览器 :驱动叫 msedgedriver 。去 Microsoft Edge 开发者网站 下载。同样需要版本匹配。

驱动下载下来是一个可执行文件(如 chromedriver.exe chromedriver )。接下来有三种配置方式:

  1. (推荐)放在 Python 脚本目录下 :将驱动文件直接复制到你的项目文件夹里。这样在代码中指定路径最简单。
  2. 放在系统 PATH 环境变量包含的目录里 :比如 Windows 的 C:\Windows\System32 。这样你可以在代码中直接使用 webdriver.Chrome() 而不指定路径,但管理多个版本时容易混乱。
  3. 在代码中指定绝对路径 :最明确,但代码移植性差。

我个人的习惯是 方法1 ,为每个项目单独管理驱动,清晰无污染。

2.3 可选工具:为什么我推荐 Playwright 作为进阶了解

在你熟练使用 Selenium 之后,可能会听到另一个工具: Playwright 。它是微软推出的新一代浏览器自动化库。这里简单对比一下,帮你建立认知:

  • Selenium :老牌、稳定、社区庞大、支持语言多(Java, Python, C#等)、支持浏览器多。但速度相对较慢,API 设计有些历史包袱,处理现代单页面应用(SPA)的异步加载有时需要写很多“等待”代码。
  • Playwright :后起之秀,默认支持无头模式且速度快,API 设计更现代优雅,自动等待机制(Auto-waiting)极大地减少了手动编写等待时间的代码,对 SPA 支持更好。但相对较新,某些极端场景的社区解决方案可能不如 Selenium 丰富。

对于初学者, 从 Selenium 开始绝对没错 ,它的资料最多,遇到的问题基本都能搜到答案。但了解 Playwright 的存在和优势,能让你在未来技术选型时多一个更优的选择。本次我们聚焦 Selenium。

3. 核心操作解析:模拟人的每一步

环境配好了,我们来拆解一个真人操作网页的过程,并用代码一一对应实现。假设我们要自动化登录一个假设的管理后台。

3.1 启动浏览器与访问页面

这是所有操作的起点。

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

# 1. 创建浏览器驱动实例
# 如果你的 chromedriver.exe 放在项目根目录
driver = webdriver.Chrome() 
# 或者指定路径
# driver = webdriver.Chrome(executable_path=r'./chromedriver.exe')

# 2. 访问目标网址
driver.get("https://example.com/admin/login")

# 3. 最大化窗口(可选,更接近人工操作)
driver.maximize_window()

time.sleep(2) # 等待2秒,让页面加载一下。这是初级做法,后面我们会改进。

webdriver.Chrome() 这行代码会打开一个全新的、干净的 Chrome 浏览器窗口。 driver.get() 相当于你在地址栏输入网址并回车。

3.2 元素定位:自动化测试的“眼睛”

要让程序在页面上找到输入框、按钮,你必须告诉它“在哪里”。这就是元素定位。Selenium 提供了8种主要的定位方式,常用的是前几种:

定位方式 代码示例 (By.XXX) 适用场景 优点 缺点
ID By.ID, “username” 元素有唯一id属性时 定位最快、最精确 不是所有元素都有id
Name By.NAME, “password” 表单元素常用 也比较精确 同ID,可能不唯一
Class Name By.CLASS_NAME, “btn-submit” 通过CSS类名定位 常用 类名经常不唯一
Tag Name By.TAG_NAME, “input” 定位特定类型的标签 简单 通常返回多个元素
Link Text By.LINK_TEXT, “忘记密码?” 定位纯文本链接 精确 只用于 <a> 标签
Partial Link Text By.PARTIAL_LINK_TEXT, “忘记” 链接文本部分匹配 更灵活 可能匹配多个
XPath By.XPATH, ‘//*[@id=“loginForm”]/input[1]’ 万能定位法,可遍历DOM 功能最强大 表达式复杂,性能稍差
CSS Selector By.CSS_SELECTOR, ‘#loginForm .btn’ 类似CSS选择器 语法简洁,性能好 学习CSS选择器语法

实操心得

  1. 优先级 ID > Name > CSS Selector > XPath 。ID 和 Name 是首选,因为它们通常由开发人员赋予业务含义且唯一。如果都没有,优先学习使用 CSS Selector,它的性能通常优于复杂的 XPath,且语法更简洁。
  2. 如何获取元素属性 :在浏览器中按 F12 打开开发者工具,使用左上角的箭头工具点击页面元素,就能在 Elements 面板看到其 HTML 代码和 id、name、class 等属性。
  3. XPath 和 CSS Selector 技巧
    • XPath // 表示从根节点开始查找, input[@name=‘user’] 表示查找 name 属性为 ‘user’ 的 input 标签。可以用 and or 组合条件。
    • CSS Selector #id 表示 id, .class 表示 class, input[type=‘text’] 表示属性选择。 > 表示直接子元素,空格表示后代元素。

3.3 元素操作:模拟手和键盘

找到元素后,就可以操作了。最常用的三种操作:点击、输入、获取信息。

# 假设页面有 <input id="username">, <input id="password">, <button id="loginBtn">
# 定位元素
username_input = driver.find_element(By.ID, "username")
password_input = driver.find_element(By.ID, "password")
login_button = driver.find_element(By.ID, "loginBtn")

# 1. 输入文本 - 模拟打字
username_input.send_keys("admin")
password_input.send_keys("secure_password_123")

# 2. 点击元素 - 模拟鼠标点击
login_button.click()

# 3. 获取元素信息 - 用于断言或记录
print(f"登录按钮的文本是:{login_button.text}")
print(f"用户名输入框的占位符是:{username_input.get_attribute('placeholder')}")
# 获取输入框内的值(对于某些动态生成的输入框)
# value = username_input.get_attribute('value')

send_keys() 不仅可以输入普通文本,还可以模拟快捷键,例如 send_keys(Keys.CONTROL, ‘a’) 表示全选(Ctrl+A)。 click() 方法很简单,但后面会讲到,不是所有时候都能直接点。

3.4 等待机制:让脚本“聪明”地等

这是 Selenium 自动化中最核心、也最容易出问题的一环。页面加载需要时间,元素出现需要时间。你不能让脚本在元素还没出现时就进行操作,否则会抛出 NoSuchElementException

三种等待方式:

  1. 强制等待 (time.sleep) :上面用过的 time.sleep(2) 。这是“死等”,无论页面是否加载完,都固定等2秒。 不推荐在正式脚本中使用 ,效率极低,且时间很难设定准确。
  2. 隐式等待 (Implicit Wait) :设置一个全局的等待时间。在这个时间内,如果元素没找到,WebDriver 会每隔一段时间重试查找,直到找到或超时。
    driver.implicitly_wait(10) # 设置隐式等待为10秒
    element = driver.find_element(By.ID, “dynamic-element”)
    
    它针对所有的 find_element 操作。缺点是它不关心元素是否“可点击”或“可见”,只关心是否存在在DOM中。对于需要等待元素具备某种状态(如可点击)的场景,可能不够。
  3. 显式等待 (Explicit Wait) 这是最佳实践! 它为某个特定元素和条件设置等待。你可以指定最长等待时间,以及检查的条件(如元素可见、可点击、存在等)。WebDriver 会轮询检查条件是否成立,成立则继续,超时则报错。
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC
    
    # 等待最多10秒,直到登录按钮变得可点击
    wait = WebDriverWait(driver, 10)
    login_button = wait.until(EC.element_to_be_clickable((By.ID, "loginBtn")))
    login_button.click()
    
    # 等待某个包含成功信息的元素出现
    success_msg = wait.until(EC.presence_of_element_located((By.CLASS_NAME, "alert-success")))
    print(success_msg.text)
    
    expected_conditions 模块提供了很多条件,如 visibility_of_element_located (元素可见)、 text_to_be_present_in_element (元素包含特定文本)等。

我的经验 在项目中混合使用隐式和显式等待 。设置一个较短的全局隐式等待(如5秒),作为兜底。在关键操作点(如点击按钮后页面跳转、等待弹窗出现)使用显式等待,并指定明确的条件。彻底避免使用 time.sleep

4. 实战进阶:处理复杂场景与封装技巧

掌握了基本操作,我们来看看实际项目中那些让人头疼的“拦路虎”。

4.1 处理弹窗、Alert 和 新窗口/标签页

JavaScript Alert/Confirm/Prompt

from selenium.webdriver.common.alert import Alert

# 触发一个alert后,切换到alert对象
alert = Alert(driver)
print(alert.text) # 获取alert文本
alert.accept() # 点击“确定”
# alert.dismiss() # 点击“取消”
# alert.send_keys(“输入文本”) # 针对prompt

新窗口/标签页

# 点击一个打开新标签页的链接前,记录当前窗口句柄
main_window = driver.current_window_handle

# 点击链接...
driver.find_element(By.LINK_TEXT, “新窗口”).click()

# 获取所有窗口句柄
all_windows = driver.window_handles
# 切换到新窗口
for window in all_windows:
    if window != main_window:
        driver.switch_to.window(window)
        break

# 在新窗口操作...
# 操作完毕后,切回主窗口
driver.switch_to.window(main_window)

iframe 嵌套页面 : 如果元素位于 <iframe> <frame> 标签内,你必须先切换到对应的 frame 中,才能定位其中的元素。

# 通过id或name切换
driver.switch_to.frame(“frame_name_or_id”)
# 或者通过定位到的frame元素切换
frame_element = driver.find_element(By.CSS_SELECTOR, “iframe.some-class”)
driver.switch_to.frame(frame_element)

# 在frame内操作元素...
# 操作完成后,切回主文档
driver.switch_to.default_content()

4.2 模拟高级用户交互:鼠标悬停、拖拽、键盘操作

需要导入 ActionChains 类来模拟复杂的鼠标和键盘操作。

from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys

# 鼠标悬停
menu = driver.find_element(By.ID, “dropdownMenu”)
ActionChains(driver).move_to_element(menu).perform()
# 此时,悬停触发的子菜单应该出现了,再定位子菜单项点击
sub_menu = driver.find_element(By.LINK_TEXT, “选项一”)
sub_menu.click()

# 拖拽元素
source = driver.find_element(By.ID, “draggable”)
target = driver.find_element(By.ID, “droppable”)
ActionChains(driver).drag_and_drop(source, target).perform()

# 组合按键(例如Ctrl+C复制)
ActionChains(driver).key_down(Keys.CONTROL).send_keys(‘c’).key_up(Keys.CONTROL).perform()

4.3 文件上传与下载

文件上传 :如果网页上传按钮是 <input type=“file”> ,那非常简单,直接 send_keys 文件路径即可。

upload_element = driver.find_element(By.XPATH, “//input[@type=‘file’]”)
upload_element.send_keys(“/Users/yourname/Desktop/test_image.jpg”) # 绝对路径

如果网页用了自定义的上传组件(通常是用 div 模拟的),可能需要用 ActionChains 模拟点击,或者借助 pyautogui 库模拟系统级键盘操作(不推荐,兼容性差),或者与开发沟通能否在测试环境提供原生 input。

文件下载 :需要设置浏览器选项,指定下载路径,并禁用下载弹窗。

from selenium import webdriver

options = webdriver.ChromeOptions()
prefs = {
    “download.default_directory”: r“C:\Downloads”, # 指定下载路径
    “download.prompt_for_download”: False, # 禁止下载弹窗
    “download.directory_upgrade”: True,
    “safebrowsing.enabled”: True
}
options.add_experimental_option(“prefs”, prefs)

driver = webdriver.Chrome(options=options)

之后,点击下载链接,文件就会自动保存到指定目录。

4.4 测试框架雏形:Page Object Model (POM)

当你的自动化脚本越来越多,你会发现大量重复的定位符和操作代码散落在各个测试用例里,难以维护。这时就需要 Page Object Model (页面对象模型) 。它的核心思想是 将页面封装成类,页面的元素定位和基本操作作为类的方法,测试用例只关心业务逻辑

举个例子,对于登录页面:

# pages/login_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

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

    # 定位器 (Locators)
    USERNAME_INPUT = (By.ID, “username”)
    PASSWORD_INPUT = (By.ID, “password”)
    LOGIN_BUTTON = (By.ID, “loginBtn”)
    ERROR_MSG = (By.CLASS_NAME, “alert-danger”)

    # 页面操作方法
    def enter_username(self, username):
        user_elem = self.wait.until(EC.visibility_of_element_located(self.USERNAME_INPUT))
        user_elem.clear()
        user_elem.send_keys(username)

    def enter_password(self, password):
        self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password)

    def click_login(self):
        self.driver.find_element(*self.LOGIN_BUTTON).click()

    def get_error_message(self):
        try:
            return self.driver.find_element(*self.ERROR_MSG).text
        except:
            return None

    # 业务场景组合方法
    def login(self, username, password):
        self.enter_username(username)
        self.enter_password(password)
        self.click_login()

然后在测试用例中,代码会变得非常清晰:

# tests/test_login.py
import pytest
from pages.login_page import LoginPage

def test_valid_login(driver): # 假设driver通过fixture注入
    login_page = LoginPage(driver)
    login_page.login(“admin”, “correct_password”)
    # 断言登录成功,例如跳转到首页
    assert “dashboard” in driver.current_url

def test_invalid_login(driver):
    login_page = LoginPage(driver)
    login_page.login(“wrong_user”, “wrong_pass”)
    error_text = login_page.get_error_message()
    assert error_text is not None
    assert “用户名或密码错误” in error_text

POM 极大地提高了代码的可读性、可维护性和复用性。元素定位符只在一个地方(Page类)维护,如果页面元素变了,你只需要修改这一个文件。

5. 常见问题排查与性能优化

即使按照最佳实践来写,脚本运行时还是会遇到各种稀奇古怪的问题。这里记录一些高频问题和解决思路。

5.1 元素定位失败:NoSuchElementException

这是最最常见的错误。原因和排查步骤:

  1. 等待不足 :元素还没加载出来就去定位。 解决方案 :使用显式等待 ( WebDriverWait + EC )。
  2. 定位器写错了 :ID/Class 名称有误,或者元素在 iframe/Shadow DOM 里。 排查 :用浏览器开发者工具仔细核对定位器,检查元素是否在 iframe 内。
  3. 页面结构动态变化 :元素的 ID 或 Class 是随机生成的。 解决方案 :使用相对稳定的属性来定位,比如 name ,或者使用包含部分文本的 XPath ( //button[contains(text(), ‘提交’)] ) 或 CSS 选择器 ( button[type*=‘submit’] )。
  4. 页面有多个匹配元素 :你的定位器找到了多个元素,但 find_element 只返回第一个,可能不是你想要的那个。 解决方案 :使用 find_elements 获取列表,或者优化定位器使其唯一。也可以使用 find_element 并指定索引(不推荐,容易失效)。

5.2 元素不可交互:ElementNotInteractableException

找到了元素,但点击或输入时失败。可能原因:

  1. 元素被遮挡 :被另一个元素(如弹窗、遮罩层)盖住了。 解决方案 :等待遮挡物消失,或者用 ActionChains 尝试点击。
  2. 元素不可见 :元素在屏幕外,或者 style=“display: none;” 解决方案 :使用 EC.visibility_of_element_located 等待其可见,或者用 JavaScript 滚动到元素所在位置 driver.execute_script(“arguments[0].scrollIntoView();”, element)
  3. 元素是 disabled 状态 :按钮有 disabled 属性。 解决方案 :等待其变为可用 ( EC.element_to_be_clickable ),或者检查前置操作是否未完成。

5.3 脚本运行不稳定(Flaky Tests)

有时成功有时失败,令人抓狂。除了上述等待问题,还有:

  1. 网络或环境不稳定 :这不是脚本问题。可以考虑在关键步骤加入重试机制,或者优化测试环境。
  2. 浏览器版本与驱动不匹配 严格检查并匹配版本!
  3. 使用了不稳定的定位器 :依赖于绝对路径的 XPath 或容易变化的 Class。 尽量使用有业务含义的、稳定的属性 ,如 data-testid (如果开发配合添加)。
  4. 异步操作未完成 :现代网页大量使用 Ajax 和前端框架,一个操作触发后,页面状态是逐步更新的。 不要只等待一个元素,要等待代表操作完成的状态出现 。例如,点击提交后,不要只等页面跳转,可以等待“提交成功”的提示框出现。

5.4 性能与稳定性优化建议

  1. 使用无头模式 (Headless) :在不需要观察浏览器界面的场景(如 CI/CD 流水线中),使用无头模式可以节省大量资源,运行更快。
    from selenium import webdriver
    options = webdriver.ChromeOptions()
    options.add_argument(“--headless”) # 开启无头模式
    options.add_argument(“--disable-gpu”) # 禁用GPU,某些系统需要
    options.add_argument(“--no-sandbox”) # Linux环境下可能需要
    driver = webdriver.Chrome(options=options)
    
  2. 合理设置等待时间 :隐式等待不要设得太长(建议5-10秒),显式等待根据操作复杂度设置(通常10-20秒)。过长的等待会拖慢整个套件的执行速度。
  3. 复用浏览器实例 :对于一组相关的测试用例,可以考虑复用同一个 driver 实例,而不是每个用例都开启关闭一次浏览器。这可以通过 pytest 的 fixture 设置 scope=“session” “module” 来实现。但要注意用例间的状态隔离,避免相互影响。
  4. 清理 Cookies 和缓存 :对于需要登录状态的测试,在用例开始前或结束后清理 Cookies,可以保证测试的独立性。
    driver.delete_all_cookies()
    driver.execute_script(“window.localStorage.clear();”) # 清理本地存储
    
  5. 截图和日志 :在关键步骤,特别是断言失败或发生异常时,自动截图并保存日志,对于后期排查问题有巨大帮助。
    try:
        # 某些操作
    except Exception as e:
        timestamp = time.strftime(“%Y%m%d_%H%M%S”)
        driver.save_screenshot(f“error_{timestamp}.png”)
        print(f“操作失败: {e}”)
        raise
    

自动化测试脚本的编写,三分在技术,七分在经验和耐心。每一个稳定运行的脚本背后,都是对业务页面交互逻辑的深刻理解和对各种边界情况的充分考虑。从最简单的登录操作开始,逐步扩展到处理复杂流程、封装页面对象、集成到持续集成平台,这条路没有捷径,但每一步都算数。当你看到原本需要手动执行一小时的回归测试,现在只需点一下按钮就能在十分钟内完成并生成报告时,那种成就感会让你觉得所有的折腾都是值得的。

更多推荐