1. 问题现象与根源剖析
最近在升级到 Selenium 4.18.1 版本后,不少朋友在运行自动化脚本时,突然遇到了一个令人头疼的报错:
AttributeError: ‘WebDriver‘ object has no attribute ‘find_elements_by_css_selector‘
。这个错误直接导致脚本中断,尤其是那些依赖大量 CSS 选择器定位元素的测试用例。乍一看,错误信息很明确,是说
WebDriver
对象没有
find_elements_by_css_selector
这个属性了。很多人的第一反应是:“我的代码之前明明在 Selenium 3 或者更早的 Selenium 4 版本上跑得好好的,怎么一升级就挂了?” 这背后,其实是 Selenium 团队在推动代码库现代化和标准化过程中,做出的一次重大但必要的“断舍离”。
简单来说,从 Selenium 4 开始,特别是到了 4.18.1 这样的较新版本,原先那些我们熟悉的、以
find_element_by_*
和
find_elements_by_*
命名的便捷方法(例如
find_element_by_id
,
find_element_by_name
,
find_element_by_xpath
,
find_element_by_css_selector
等)已经被正式标记为
弃用(Deprecated)
,并在某些环境下可能已被移除。Selenium 官方推荐我们统一使用新的、更符合 Python 社区惯例的
find_element
和
find_elements
方法,配合
By
这个枚举类来指定定位策略。这个改动不是为了制造麻烦,而是为了让 API 更加清晰、一致,减少冗余,并更好地支持类型提示等现代开发特性。所以,当你看到这个
AttributeError
时,它本质上是一个“升级提醒”,告诉你需要将旧式的定位语法迁移到新式语法上。
2. 新旧定位方法对比与迁移指南
要解决这个问题,我们必须彻底理解新旧两套定位方法的区别,并掌握如何将旧代码无缝迁移到新标准。这不仅仅是简单的字符串替换,更是一种编码习惯的升级。
2.1 旧式方法(Deprecated) vs 新式方法(Recommended)
在 Selenium 3 及早期 Selenium 4 中,我们习惯于这样写:
# 旧式写法(已弃用)
driver.find_element_by_id(“username”)
driver.find_elements_by_class_name(“item”)
driver.find_element_by_css_selector(“#submitBtn”)
driver.find_element_by_xpath(“//button[@type=‘submit’]”)
每个定位方式都有一个独立的方法名。这种方式直观,但导致了 API 的膨胀,
WebDriver
和
WebElement
类下会有十几个功能类似的方法。
从 Selenium 4 开始,官方引入了统一的方法:
# 新式写法(推荐)
from selenium.webdriver.common.by import By
driver.find_element(By.ID, “username”)
driver.find_elements(By.CLASS_NAME, “item”)
driver.find_element(By.CSS_SELECTOR, “#submitBtn”)
driver.find_element(By.XPATH, “//button[@type=‘submit’]”)
核心变化在于:
-
方法统一
:只剩下
find_element和find_elements两个核心方法。 -
策略枚举
:通过
By类的静态属性(如By.ID,By.CSS_SELECTOR)来明确指定定位策略。 - 参数分离 :定位策略和定位器表达式作为两个独立的参数传入。
这种设计的好处非常明显:API 更简洁,减少了记忆负担;通过
By
枚举增强了代码的可读性和可维护性;也方便了 IDE 的代码补全和静态检查。
2.2 手把手迁移你的代码
迁移过程是机械性的,但需要细心。你可以手动修改,也可以借助编辑器的查找替换功能。
1. 单个元素查找的迁移:
将
find_element_by_*
替换为
find_element(By.*, ...)
。记得在文件顶部导入
By
。
2. 多个元素查找的迁移:
将
find_elements_by_*
替换为
find_elements(By.*, ...)
。
3. 迁移表示例:
| 旧方法 (弃用) | 新方法 (推荐) |
|---|---|
find_element_by_id(“elem”)
|
find_element(By.ID, “elem”)
|
find_element_by_name(“q”)
|
find_element(By.NAME, “q”)
|
find_element_by_xpath(“//div”)
|
find_element(By.XPATH, “//div”)
|
find_element_by_link_text(“Click”)
|
find_element(By.LINK_TEXT, “Click”)
|
find_element_by_partial_link_text(“Cli”)
|
find_element(By.PARTIAL_LINK_TEXT, “Cli”)
|
find_element_by_tag_name(“input”)
|
find_element(By.TAG_NAME, “input”)
|
find_element_by_class_name(“btn”)
|
find_element(By.CLASS_NAME, “btn”)
|
find_element_by_css_selector(“.primary”)
|
find_element(By.CSS_SELECTOR, “.primary”)
|
find_elements_by_class_name(“item”)
|
find_elements(By.CLASS_NAME, “item”)
|
find_elements_by_css_selector(“div”)
|
find_elements(By.CSS_SELECTOR, “div”)
|
注意 :迁移后,请务必在脚本文件的开头加上
from selenium.webdriver.common.by import By这一行导入语句,否则By会是未定义的。
2.3 使用兼容层进行临时过渡(不推荐长期使用)
如果你有一个庞大的旧代码库,一次性迁移所有文件有困难,或者你使用的某些第三方库内部还在调用旧方法,Selenium 提供了一个“兼容层”来临时缓解问题。你可以通过
webdriver.common.by
模块中的
_By
类来重新注册这些旧方法。
from selenium import webdriver
from selenium.webdriver.common.by import By
import selenium.webdriver.common.by
# 启用旧式方法的兼容模式
selenium.webdriver.common.by._is_legacy = True
# 然后照常初始化 driver 和使用旧方法(仅作演示,强烈建议迁移)
driver = webdriver.Chrome()
# 以下旧方法在设置后可能暂时可用,但未来版本会移除
# element = driver.find_element_by_id(“test”)
重要警告 :这只是一个 临时解决方案 ,目的是给你争取迁移代码的时间。这个兼容层在未来的 Selenium 版本中 一定会被移除 。依赖它等于埋下了一个定时炸弹。我的强烈建议是,不要依赖这个技巧,而是规划时间,尽快将代码库迁移到新的标准 API 上。
3. 升级后的最佳实践与代码优化
仅仅完成语法迁移是远远不够的。借此机会,我们可以重构和优化自己的自动化代码,使其更健壮、更易维护。以下是一些在 Selenium 4 时代值得采用的最佳实践。
3.1 利用相对定位器(Relative Locators)
Selenium 4 引入了一个非常实用的新特性:相对定位器(Friendly Locators, 后更名为 Relative Locators)。它允许你根据元素之间的相对位置关系来定位元素,比如“在某个元素上方”、“左侧”、“附近”等。这在定位那些缺乏稳定 ID 或 Class,但位置关系固定的元素时特别有用。
from selenium.webdriver.common.by import By
from selenium.webdriver.support.relative_locator import locate_with
# 假设有一个已知的“提交”按钮
submit_button = driver.find_element(By.ID, “submit”)
# 找到在提交按钮左边的“取消”按钮
cancel_button = driver.find_element(locate_with(By.TAG_NAME, “button”).to_left_of(submit_button))
# 找到在用户名输入框下方的错误提示信息
username_field = driver.find_element(By.NAME, “username”)
error_msg = driver.find_element(locate_with(By.CLASS_NAME, “error”).below(username_field))
相对定位器提供了
above()
,
below()
,
to_left_of()
,
to_right_of()
,
near()
等方法,极大地增强了定位的灵活性,让测试脚本更能适应 UI 的局部变化。
3.2 封装自定义查找方法(Page Object 模式增强)
在 Page Object 模式中,我们通常会在页面类里定义元素定位器。迁移到新 API 后,我们可以进一步封装,创建更简洁、容错性更高的查找方法。
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)使用元组存储 (By.策略, “表达式”)
USERNAME_INPUT = (By.ID, “username”)
PASSWORD_INPUT = (By.NAME, “password”)
SUBMIT_BUTTON = (By.CSS_SELECTOR, “button[type=‘submit’]”)
ERROR_MSG = (By.CLASS_NAME, “alert-error”)
# 封装的元素获取方法,自动加入显式等待
def get_username_field(self):
“”“返回用户名输入框元素,并确保其可点击。”“”
return self.wait.until(EC.element_to_be_clickable(self.USERNAME_INPUT))
def get_password_field(self):
return self.driver.find_element(*self.PASSWORD_INPUT) # 注意这里的 * 号解包
def click_submit(self):
self.wait.until(EC.element_to_be_clickable(self.SUBMIT_BUTTON)).click()
def get_error_message(self):
“”“获取错误信息,如果不存在则返回 None。”“”
elements = self.driver.find_elements(*self.ERROR_MSG)
return elements[0].text if elements else None
# 使用示例
page = LoginPage(driver)
page.get_username_field().send_keys(“myuser”)
page.get_password_field().send_keys(“mypass”)
page.click_submit()
error = page.get_error_message()
if error:
print(f“登录失败: {error}”)
这种封装将定位策略、等待逻辑和业务操作分离,使测试用例更加清晰,也大大提升了代码的复用性和可维护性。
*
操作符用于将
(By.XX, “value”)
这样的元组解包成两个独立的参数传递给
find_element
。
3.3 显式等待(WebDriverWait)的现代化使用
显式等待是处理动态加载元素的黄金标准。在新 API 下,与
WebDriverWait
和
expected_conditions
的结合更加自然。
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
# 等待一个元素出现并可见
element = WebDriverWait(driver, 10).until(
EC.visibility_of_element_located((By.ID, “dynamicContent”))
)
# 等待多个元素出现
all_items = WebDriverWait(driver, 10).until(
EC.presence_of_all_elements_located((By.CLASS_NAME, “list-item”))
)
# 等待元素包含特定文本
success_msg = WebDriverWait(driver, 10).until(
EC.text_to_be_present_in_element((By.ID, “status”), “操作成功”)
)
注意,这里传递给
EC.visibility_of_element_located
等条件的参数,也是一个
(By.XX, “value”)
格式的元组。这正好与我们 Page Object 模式中存储定位器的方式一致,配合得天衣无缝。
4. 常见问题排查与深度避坑指南
在迁移和升级过程中,你可能会遇到一些其他相关问题。这里我总结了一份常见问题排查清单和避坑经验,很多都是我在实际项目中踩过的“坑”。
4.1 问题排查清单
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
AttributeError: ‘WebDriver‘ object has no attribute ‘find_element_by_*‘
|
1. 使用了已弃用的旧方法。
2. Selenium 版本 >= 4.0,且兼容层未启用或已失效。 |
1.
首要方案
:将代码迁移至
find_element(By.*, …)
新语法。
2. 临时方案 :检查并设置
_is_legacy = True
(仅作过渡)。
|
NameError: name ‘By‘ is not defined
|
迁移代码时,忘记了导入
By
类。
|
在脚本文件顶部添加:
from selenium.webdriver.common.by import By
|
TypeError: find_element() takes 2 positional arguments but 3 were given
|
错误地使用了
find_element(By.ID(“myId”))
。
By.ID
是一个字符串常量,不是可调用函数。
|
正确写法:
find_element(By.ID, “myId”)
。确保
By.ID
和
“myId”
作为两个独立参数传递。
|
| 迁移后脚本运行变慢或元素找不到 |
1. 新/旧 API 混用导致逻辑混乱。
2. 页面加载或元素渲染速度问题,缺少等待。 |
1. 统一全部使用新 API。
2. 在关键操作后增加显式等待 (
WebDriverWait
),而非固定的
time.sleep
。
|
使用
find_elements
返回空列表
[]
| 定位器表达式写错了,或者元素确实不存在于当前页面。 |
1. 使用浏览器开发者工具(F12)的 Console 选项卡,输入
document.querySelectorAll(‘你的CSS选择器’)
或
$x(‘你的XPath’)
验证定位器是否正确。
2. 检查页面是否在 iframe 内,需要先
driver.switch_to.frame
。
3. 确认页面已完全加载。 |
相对定位器 (
locate_with
) 找不到元素
| 相对定位的参考元素位置或关系不准确。 |
1. 确保参考元素已正确找到且可见。
2. 相对定位的精度受页面布局影响,可能不如绝对定位稳定,建议作为辅助手段。 |
4.2 独家避坑技巧与心得
-
一步到位,拒绝妥协 :不要试图在项目中混合使用新旧两种 API。这会给后续维护带来巨大混乱。制定一个计划,一次性将整个项目或至少一个完整模块迁移到新 API。可以利用 IDE 的全局查找替换功能(使用正则表达式)来批量处理,效率极高。
-
定位器验证是第一步 :在将旧定位器迁移到新语法后,不要直接运行整个脚本。先写一个简单的调试脚本,只打开页面,然后用新语法尝试查找几个关键元素,打印其文本或属性,确保定位器本身在新的
find_element(By.XX, …)格式下依然有效。这能提前发现因手误导致的迁移错误。 -
拥抱显式等待,告别硬等待 :迁移代码是优化架构的好时机。检查你的脚本,把所有
time.sleep(5)这样的硬等待(也叫强制等待),尽可能地替换成WebDriverWait配合expected_conditions的显式等待。显式等待只在条件满足时立即返回,最大程度提升脚本执行速度。例如,等待按钮可点击:WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.ID, “btn”)))。 -
注意
find_element与find_elements的异常行为 :driver.find_element(By.XX, locator)如果找不到元素,会抛出NoSuchElementException。而driver.find_elements(By.XX, locator)在找不到元素时,会返回一个空列表[], 不会抛出异常 。这个区别非常重要。如果你需要判断元素是否存在,使用find_elements并检查列表长度是更安全的方式。 -
升级依赖链 :升级
selenium包时,注意与之配套的浏览器驱动(如chromedriver,geckodriver)的版本兼容性。通常建议也更新到较新版本。可以使用webdriver-manager这个第三方库来自动管理驱动下载和匹配,省去手动配置的麻烦。
# 使用 webdriver-manager 的示例
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service)
# 现在 driver 已经使用了正确版本的 ChromeDriver
- 回归测试是关键 :完成 API 迁移后,务必对你的自动化测试用例进行全面的回归测试。因为 API 变化是底层改动,虽然功能等价,但在某些边界情况或与特定等待条件结合时,行为可能有细微差别。确保所有核心业务流程的测试依然通过。
迁移到 Selenium 4 的新 API,初期会有一点学习成本和不适应,但长远来看,它带来的代码清晰度、一致性和可维护性的提升是巨大的。把这看作一次代码库的“健身”过程,优化后的脚本将更健壮,更能适应未来的变化。

310

被折叠的 条评论
为什么被折叠?



