最近在帮团队搭一套 Web 自动化测试的底子,技术选型绕来绕去最后还是回到 Selenium 上。说实话,这玩意儿我前前后后用了四五年,从早期 Firefox 时代一路用到今天 Chrome 119+,每次有新人加入,第一周基本都在跟浏览器驱动、元素定位、等待时机这些东西死磕。标题里写的是“selenium使用(包括函数部分)”,听着挺朴素,但恰恰是这些最基础的东西,最值得沉下心来捋一遍。因为实际项目里跑得不稳、动不动就报 NoSuchElementException 的,九成问题都出在函数用得不透、时序没控制好。
这篇文章就当作一次完整的实操笔记,从环境准备讲到核心函数,再聊到工程化封装,最后把排错经验一并列出来。Selenium 本身能做什么?一句话总结:用代码驱动真实浏览器,模拟人的点击、输入、滚动、切页等操作,再对页面结果做校验。它适合谁?不管你是刚接触自动化测试的 QA,还是想写爬虫又不想被反爬绕晕的开发,甚至只是想把重复性的网页操作脚本化的运维同学,这篇文章都值得看完。内容偏实战,我自己踩过的坑都会标出来,能帮你少走不少弯路。
1. Selenium环境搭建与驱动匹配
1.1 安装selenium库的正确姿势
现在安装 Selenium 已经比前几年省心太多了,直接用 pip 装就行:
pip install selenium装的时候建议顺手把版本固定住。我个人习惯用pip install selenium==4.27.0这种写法,或者装完立刻执行pip freeze > requirements.txt导出依赖。原因很简单,Selenium 4.x 的 API 比 3.x 改动不小,尤其元素等待、下拉框处理这些地方,团队里版本不统一,代码互相跑不通,光填坑就能耗掉大半天。实测下来 Selenium 4.6 之后的版本自带驱动管理能力,不再需要手动去下载 chromedriver 落地到指定路径,这对新手来说真的是救命改进。
检查安装是否成功,在 Python 交互环境里跑一句:
from selenium import webdriver print(webdriver.__version__)能正常输出版本号就没问题。如果这里报 ModuleNotFoundError,先确认你是不是装进了当前激活的虚拟环境。很多新人折腾半天,最后发现 pip 和 python 根本不是同一套环境。
1.2 浏览器驱动下载与配置
Selenium 本身不包含浏览器,它只是通过 WebDriver 协议去指挥浏览器干活。所以你还得有一个驱动,相当于浏览器给外部程序留的“遥控接口”。
我用得最多的是 Chrome 配 chromedriver。从 4.6 版本开始,Selenium 提供了一个特别顺手的工具类SeleniumManager,代码里不再需要手动指定 executable_path:
from selenium import webdriver driver = webdriver.Chrome()只要 Chrome 浏览器本身是正常安装的,Selenium 会自动去匹配对应版本的驱动,下载后缓存在本地。这背后做的事情,说穿了就是去检查浏览器版本,然后找到匹配的驱动二进制。但有一点要提醒:如果公司内网环境访问不了外网,Selenium Manager 自动下载驱动会卡住,这时候只能手动到驱动仓库下载对应版本,放到系统 PATH 里。
版本匹配的原则记住一条:chromedriver 的大版本号和 Chrome 浏览器的大版本号必须一致。比如你本地 Chrome 是 120.0.6099,那驱动也必须找 120 开头的。版本不匹配时,启动浏览器会直接抛 SessionNotCreatedException,报错信息里会明明白白写着版本不兼容。
1.3 环境变量问题排查
标题的热搜词里有一串“无法将 xxx 项识别为 cmdlet、函数、脚本文件或可运行程序的名称”,这几乎是所有命令行工具的通用坑,Selenium 也不例外。最常见的是 pip 命令找不到:
pip : 无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称原因通常有两个:一是你的 Python 安装时没勾选“Add Python to PATH”;二是你当前激活的虚拟环境里压根没装 pip。前者去系统环境变量里把 Python 的 Scripts 目录加进去就行,后者直接在命令行里跑python -m pip install selenium,用模块方式调用 pip,可以绕开 PATH 问题。
chromedriver 如果提示“不是内部或外部命令”,直接把驱动所在目录加到 PATH,或者干脆把驱动文件放到 Python 安装目录的根下,一劳永逸。
2. 核心函数体系拆解:从定位到动作
2.1 最常用的定位函数:find_element 全家桶
Selenium 里所有操作的前置条件都是“找到元素”。定位函数用不好,后面全是空中楼阁。Selenium 4 提供了两种写法,一种是老牌的:
driver.find_element_by_id("username")另一种是 Selenium 4 推荐的统一入口:
from selenium.webdriver.common.by import By driver.find_element(By.ID, "username")我个人只用第二种。第一种在老版本被标记为废弃后,虽然短期内还能用,但每次跑都会刷 deprecation 警告,日志干净度很影响排查效率。统一入口的好处还在于,你只需要记一个函数名,配合不同定位策略切来切去:
| 定位策略 | By 参数 | 适用场景 |
|---|---|---|
| ID | By.ID | 元素有唯一 id,优先级最高 |
| 类名 | By.CLASS_NAME | 有明确 class,且不与其他元素重复 |
| 名称 | By.NAME | 表单控件常用 name 属性 |
| XPath | By.XPATH | 结构复杂时兜底方案,灵活但慢 |
| CSS 选择器 | By.CSS_SELECTOR | 比 XPath 快,写法简洁 |
| 链接文本 | By.LINK_TEXT | 精确定位超链接 |
| 部分链接文本 | By.PARTIAL_LINK_TEXT | 只记得链接部分文字时用 |
实际项目里的选型优先级,我一般按 ID > CSS_SELECTOR > XPath 来。ID 是最稳定的锚点,但很多前端框架生成的 ID 里带动态随机串,每次刷新都变。遇到这种,CSS 选择器拿属性值定位反而更稳:
# 定位一个>element = driver.find_element(By.ID, "username") element.clear() # 清空输入框 element.send_keys("admin") # 输入内容 element.click() # 点击 print(element.text) # 获取可见文本 print(element.get_attribute("class")) # 获取任意属性这些函数看起来简单,细节里全是坑。先说 clear,很多人在输入前不清空,结果文本框里残留上次的值,拼出来的字符串就错了。尤其浏览器会自动填充账号密码时,send_keys 之前不 clear 一下,最终值会是“自动填充的值+你输入的值”。
再说 click,直觉上就是“点一下”,但实际执行时会要求这个元素是可见的、且宽高都不为 0。如果一个元素在页面底部被折叠起来了,直接 click 会报 ElementNotInteractableException。这时候系统性地用 scroll_into_view 先滚过去:
from selenium.webdriver.common.action_chains import ActionChains ActionChains(driver).scroll_to_element(element).perform() element.click()send_keys 还有一批隐藏功能,比如操作键盘按键。上传文件时也能用 send_keys 直接传文件路径,但 Chrome 出于安全限制,只能操作 input[type=file] 元素,不能直接发路径到任意输入框:
# 回车键 element.send_keys(Keys.ENTER) # 组合键 Ctrl+A element.send_keys(Keys.CONTROL, "a") # 上传文件 driver.find_element(By.CSS_SELECTOR, "input[type='file']").send_keys("/path/to/file.pdf")这里有个很大的易错点:文件上传对话框是操作系统原生窗口,Selenium 默认没有能力操作它。但如果你是真实的上传场景,直接把文件路径通过 send_keys 喂给 input[file] 就能绕过弹窗,这是做自动化时最常用的招。
2.3 浏览器控制函数:get、back、forward、refresh 与窗口管理
和元素操作并列的,是一批驱动对象级函数,用于控制浏览器整体行为:
driver.get("https://example.com") # 打开页面 driver.back() # 后退 driver.forward() # 前进 driver.refresh() # 刷新 driver.title # 获取页面标题 driver.current_url # 获取当前URL driver.maximize_window() # 最大化窗口 driver.set_window_size(1920, 1080) # 设置窗口尺寸 driver.get_screenshot_as_file("shot.png") # 截图特别提一下 get_screenshot_as_file,这个函数在排查问题时候就是救命稻草。我做的项目里,凡是跟第三方支付页面对接,回调不稳定的时候,全靠失败那一刻的截图还原现场。更讲究一点,可以把截图命名为“时间戳+用例名”存到指定目录,万一 CI 失败,直接看图片定位问题,比看一堆堆的异常栈快得多。
窗口管理也值得单独说说。很多业务点完按钮会新开 Tab,Selenium 默认还停留在旧页面上,这时候你必须手动切窗口:
# 获取当前所有窗口句柄 handles = driver.window_handles # 切到最后一个新窗口 driver.switch_to.window(handles[-1])有同事问过,为什么写了 click 之后明明页面已经跳了,find_element 还是找不到?八成就是没切窗口,或者没做等待。窗口切换和元素可见性是两个独立的维度,都要处理。
3. 进阶函数:从硬等待到智能等待
3.1 等待函数与时机的博弈
刚接触 Selenium 的朋友最常见的错误就是“洒水式”的硬等待:
import time time.sleep(5)不是说不能用,而是不能到处用。硬等待会让整个用例的执行时间被拉长好几倍,100 个用例每个多 sleep 3 秒,就是 300 秒的损耗。更麻烦的是,网络状况好的时候用不着等那么久,网络状况差的时候 5 秒又不够,用例开始变得“时而绿、时而红”。
正确的思路是显式等待。Selenium 提供了一对黄金搭档:WebDriverWait 和 expected_conditions(通常别名成 EC):
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10, poll_frequency=0.5) login_btn = wait.until( EC.element_to_be_clickable((By.ID, "login-btn")) ) login_btn.click()这段代码的意思是:最多等 10 秒,每 0.5 秒去检查一次按钮是否可点击,一旦可点击立刻返回元素,超过 10 秒就抛 TimeoutException。相比 sleep,它把“等待”逻辑从固定时长变成了“轮询直到条件满足”,这才是自动化的正确姿势。
EC 模块里常用的条件函数我列几个:
| EC 函数 | 作用 |
|---|---|
| EC.presence_of_element_located | 元素出现在 DOM 中,但不一定可见 |
| EC.visibility_of_element_located | 元素可见 |
| EC.element_to_be_clickable | 元素可见且可点击 |
| EC.text_to_be_present_in_element | 元素文本包含指定内容 |
| EC.title_contains | 页面标题包含关键字 |
| EC.alert_is_present | 弹窗出现 |
我自己总结了一个经验规则:元素定位用 duration 多等一点没事,操作类的等待(点击、输入后触发跳转)重点在“可交互”这一层,而不是“出现”这一层。DOM 里出现不代表能点击,可能还在加载样式、事件还没绑定完。所以在实际项目里 EC.element_to_be_clickable 是我用得最多的条件,比单独 presence 稳定不少。
3.2 下拉框处理的Select函数
热搜词里专门有 select 函数,这是 Selenium 里一个特别容易踩坑的点。很多人拿到下拉框,下意识想点击,结果发现原生<select>元素点了没反应,因为它的展开选项根本不是普通的 DOM 元素,是浏览器原生绘制的东西。
Selenium 为原生 select 提供了专门封装:
from selenium.webdriver.support.ui import Select select = Select(driver.find_element(By.ID, "city")) # 按可见文本选择 select.select_by_visible_text("北京") # 按 value 属性选择 select.select_by_value("110000") # 按索引选择,从0开始 select.select_by_index(1)三个函数的区别很简单:页面给用户看的是“北京”,HTML 里 value 可能是“110000”,索引就是第几个 option。实际项目里优先用 select_by_visible_text,因为它在页面上的最终呈现结果最直观,别人 review 代码的时候一眼能懂。
但要注意,Select 类只对原生的<select>标签有效。现在很多前端框架(比如 Vue + Element UI、React + Ant Design)自作主张做了自定义下拉框,本质是 input + 下拉列表的组合,这时候你用 Select 函数会直接报 UnexpectedTagNameException。处理这种自定义下拉,思路就要变:先点击触发下拉列表展开,再根据选项文本定位弹层中的具体项:
driver.find_element(By.CSS_SELECTOR, ".ant-select-selector").click() wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".ant-select-dropdown"))) driver.find_element(By.XPATH, "//div[contains(@class,'ant-select-item') and text()='上海']").click()这里有个细节:自定义下拉的选项可能很多,但只有当前渲染出来的部分能定位。如果列表很长,选项是滚动懒加载的,还得先在弹层里执行滚动,再继续找后面的选项。
3.3 鼠标键盘操作的ActionChains函数
有些操作已经超出了简单的 click 和 send_keys,比如鼠标右键、双击、悬停、拖拽、按住不放。Selenium 的 ActionChains 就是把一连串动作组织成链条,最后统一 perform:
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys actions = ActionChains(driver) # 悬停到菜单项上 hover = driver.find_element(By.ID, "nav-products") actions.move_to_element(hover).perform() # 双击 actions.double_click(element).perform() # 右键 actions.context_click(element).perform() # 拖拽 actions.drag_and_drop(source, target).perform()ActionChains 最典型的业务场景是电商后台的拖拽排序:把某个商品从左边的可选列表拖到右边的已选列表。以前有人用模拟键盘 Tab+方向键去操作,稳定性极差。换成 drag_and_drop 之后,整个用例的执行时间从 20 秒降到了 8 秒。
一个细节之坑:perform 之前 ActionChains 维护的动作链表是累加的。如果你在一个 actions 对象上先 move_to_element 又 double_click,这两个动作会被一起执行。所以每次动作串联完必须执行 perform 结束链条,否则下次写动作会把上次的残留一并带上。习惯上我每次都会新建一个 ActionChains 对象,不复用。
3.4 页面JavaScript注入:execute_script
再核心的自动化也少不了执行 JavaScript 的场景。页面上的滚动、属性修改、隐藏元素操作,很多时候 Selenium 原生函数做不到,但一段 JS 搞定:
# 滚动到页面底部 driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") # 给元素加一个红色边框,方便肉眼核对 driver.execute_script("arguments[0].style.border='2px solid red'", element) # 去掉 readonly 属性,让原本禁止填写的日期框可输入 driver.execute_script("arguments[0].removeAttribute('readonly')", element)arguments[0] 是 Selenium 注入 JS 时的参数占位符,后面的 Python 参数会按顺序映射到 arguments[0]、arguments[1]... 这个方法在绕过只读控件时几乎是唯一解。我以前遇到过业务里明明有“申请日期”字段,但因为权限控制被设成 readonly,手动测试能通过,自动化却一直输不进去。开发说“你们测试不能动这个字段”,我说“行,那就验证一下只读逻辑”,然后改用 JS 去调用 removeAttribute,顺利解决了。
execute_script 的返回值也很有用。如果 JS 最后 return 了一个值,Python 这边能直接拿到:
# 获取页面可见高度 viewport_height = driver.execute_script("return window.innerHeight;") # 判断页面是否滚动到底部 is_bottom = driver.execute_script("return window.scrollY + window.innerHeight >= document.body.scrollHeight;")这给了我一个思路:不用总依赖 Selenium 的 API 去拿页面状态,有时候直接用 JS 问浏览器“你现在的滚动位置是多少”比任何封装都准确。
4. 工程化封装中的函数设计
4.1 用 Page Object 模式封装页面函数
学会 Selenium 函数只是第一层。真正考验功力的是把散落各处的函数组织成一套可维护的工程结构。Page Object(页面对象模式)是目前业界最经典的方案,核心思想:每个页面封装成一个类,页面上要操作的元素定位和动作统统收敛到类方法里,测试用例只跟“页面对象”交互,不直接碰 driver。
举个例子,登录页可以设计成:
class LoginPage: def __init__(self, driver): self.driver = driver self.username_input = (By.ID, "username") self.password_input = (By.ID, "password") self.login_button = (By.ID, "login-btn") def input_username(self, username): self.driver.find_element(*self.username_input).send_keys(username) def input_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()看着好像只是把定位符集中存放了,但实际好处太多了。第一,前端改版把登录按钮的 id 换了,你只需要改这一处,所有用例集体生效。第二,测试用例读起来是业务语言而不是 Selenium 语法:
login_page = LoginPage(driver) login_page.input_username("tester01") login_page.input_password("password123") login_page.click_login()这比我最早写的“find_element_by_id 一万行”清晰太多了。代码 review 的时候,别人不需要知道 Selenium 有哪些函数,只需要看方法名就知道当前用例在做什么。
我还会在基类里封装一些公共操作,比如统一等待、统一截图:
class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) def find(self, locator): return self.wait.until(EC.visibility_of_element_located(locator)) def click(self, locator): self.find(locator).click()这样所有页面类继承 BasePage,就自动获得“可见即所得”的查找能力,不用每个方法都重复写 wait.until。
4.2 通用函数的三个高阶技巧:重试、截图、日志
工程化到后半程,你会发现自己重复写“失败重试”“失败截图”“记录日志”这三件事。把它们抽成通用函数,整个框架的稳定性会明显上升。
失败重试的通用逻辑可以封装成装饰器,比如点击确认弹窗后页面可能短暂闪烁,第一次点击没生效,重试一次就好了:
import functools def retry_on_failure(max_retries=3): def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries): try: return func(*args, **kwargs) except Exception: if attempt == max_retries - 1: raise time.sleep(1) return wrapper return decorator需要注意,重试不是万能药。如果元素找不到是因为前端压根改版了,重试只会浪费 3 秒再报错。所以我的做法是:只在“偶发性、时序性”问题上用重试,定位符变更、断言失败这类确定性错误直接抛出来让人去查。
失败截图我喜欢做成 pytest 的钩子。每一条用例失败的时候自动截图,文件名带用例名和时间戳,写进 allure 报告。这样失败之后根本不用翻日志,直接打开报告看图就知道浏览器当时的状态。
5. 常见问题与排查技巧实录
5.1 元素找不到?先分三路排查
NoSuchElementException 是 Selenium 里出现频率最高的报错,没有之一。接手过太多人问“为什么我明明复制了 XPath 还是找不到”,这里把排查思路整理成一张速查表:
| 类别 | 现象 | 排查方向 |
|---|---|---|
| 时序问题 | 报错时间很快,页面其实还没加载完 | 加显式等待 |
| 窗口问题 | 元素在 iframe 或新窗口里 | 先 switch_to 再定位 |
| 定位符问题 | 元素确实在 DOM,但定位表达式写错了 | 用浏览器 console 验证 XPath |
时序问题是最常见的。页面用了异步加载,元素在 DOM 里“迟到”了,你写代码时看到的页面是加载完的状态,但脚本执行到它的那一瞬间还没有。处理方式参考 3.1 的显式等待。
窗口和 iframe 的问题常常被忽略。很多管理系统后台、第三方登录组件、广告弹层都是 iframe 内嵌的,你在父页面 find_element 自然找不到。先切进去:
driver.switch_to.frame("frame_name") # 按 name 切 driver.switch_to.frame(0) # 按索引切 driver.switch_to.frame(frame_element) # 或直接传我们找到的元素切换完之后别忘了,用完再切回默认内容:
driver.switch_to.default_content()这个坑我踩过不止一次:切进 iframe 操作完成之后,没有切回主文档,结果下一步找主页面元素一直报错,排查半天才发现自己在 iframe 的上下文里出不来。
5.2 元素被遮挡与点击偏移
还有一个高频问题:元素在页面上的位置被其他元素盖住了,比如弹窗遮罩、banner 悬浮层、Cookie 协议条。Selenium 定位到了元素,但点击时浏览器提示“元素不可交互”。
最直接的解决办法是手动把遮挡层干掉,或者把元素滚动到可见区域:
element = driver.find_element(By.ID, "submit") driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", element) element.click()如果页面挂了遮挡层,可以用 JS 把遮挡元素隐藏:
driver.execute_script("document.querySelector('.modal-mask').style.display='none';")注意:这种绕过手段只适合测试环境。如果线上页面被拦截,说明业务有合规要求,必须先确认能不能跳过,别为了一时方便把功能测出一个假通过。
拦截点想到这我还想分享一个更隐蔽的:固定头部导航栏横向占满整个页面,页面上方的按钮虽然出现在视口里,但被导航栏盖住了实际可点击区域。这时 scrollIntoView 加上 block: center 之后依然点不中,因为点击坐标落在遮罩层上。我的处理办法是滚动多一点:
driver.execute_script("window.scrollBy(0, -120);")手动把页面再往上挪 120 像素,让目标元素避开导航栏遮挡区域。
5.3 弹窗处理与多窗口切换
弹窗分成两类:浏览器原生 alert,和页面自定义弹层。两者处理逻辑完全不同。
原生 alert 是浏览器级别的,没法用 find_element 定位,必须用 switch_to:
# 等待弹窗出现 wait.until(EC.alert_is_present()) alert = driver.switch_to.alert print(alert.text) # 获取弹窗文本 alert.accept() # 点击确定 # alert.dismiss() # 点击取消自定义弹层本质是一个 div,左上角那个关闭按钮就是普通元素。一定要注意弹层出现有没有过渡动画。有些团队给弹层加了 0.3s 的 fade 过渡,脚本执行太快,在动画期间点关闭按钮会 miss,就等着报错吧。这种只能等着,最稳妥的方式还是 EC.element_to_be_clickable 等它完全出现。
多窗口切换在 2.3 里写过基础用法,这里补一个封装好的切换函数:
def switch_to_new_window(driver, old_handles): """切到新打开的窗口,old_handles 是切换前的窗口句柄列表""" wait = WebDriverWait(driver, 10) wait.until(lambda d: len(d.window_handles) > len(old_handles)) new_handles = driver.window_handles for handle in new_handles: if handle not in old_handles: driver.switch_to.window(handle) break这里面最核心的一步是用 lambda 条件等待新窗口生成。如果窗口都没打开你就急着切,拿到的是一个不存在的句柄,后面全崩。
5.4 浏览器驱动闪退与浏览器崩溃
最后聊聊一个玄学问题:脚本跑到一半,浏览器窗口突然全关了,报错信息还指向 chromedriver。这种情况在 Linux 服务器上跑无人值守的任务时尤其常见。
第一,确认系统内存是否吃紧。浏览器是内存大户,一个 Chrome 实例轻松吃掉 500MB 以上,服务器上同时跑多个用例时 OOM 是常事。排查方法很简单,盯一下系统监控里的内存曲线,或者执行 free -h 看剩余内存。
第二,检查浏览器版本是否被自动更新了。Chrome 默认开启自动更新,今天跑得好好的,明天驱动对不上,直接崩。生产环境建议固定浏览器版本,或者写脚本定期检查版本一致性。
第三,无头模式(headless)下更容易暴露资源问题。用--headless=new跑无头模式时,页面加载但肉眼不可见,很多人以为更省资源,其实不然。我给团队的建议是:调试时一定用有头模式,能看见浏览器行为;只在 CI 上跑批量回归时切换到无头模式,且加上窗口尺寸设置:
from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--headless=new") options.add_argument("--window-size=1920,1080") options.add_argument("--disable-gpu") driver = webdriver.Chrome(options=options)记得设置 window-size,否则无头模式下页面视口默认可能只有 800x600,很多响应式布局的页面在这个尺寸下的 DOM 结构和 1920 宽下完全不同,直接导致元素定位失败。
6. 我个人在实际项目里的体会
Selenium 这套工具链说简单也简单,无非就是定位元素、执行操作、验证结果;说复杂也复杂,真实业务里的动态加载、跨域跳转、弹窗嵌套、浏览器兼容,每一样都够折腾一阵。但在我看来,掌握 Selenium 的核心不在于记住多少个 API 函数,而在于建立起“情况驱动选型”的判断力:什么时候用普通定位、什么时候必须显式等待、什么时候切 iframe、什么时候用 JS 兜底。
我见过太多人把 Selenium 用成了“录制回放”工具,脚本拿到手能跑就万事大吉,等前端一改版就全部报废。真正健壮的自动化测试,靠的是把函数用得恰到好处,把等待、重试、截图这些防御性措施做足。前面分享的每一条经验,都是我实打实踩过坑换来的结论。如果你照着这篇里的思路走一遍,再去看你们项目的自动化脚本,多半能发现不少可以优化的地方。
最后再分享一个习惯:每次在 Selenium 里遇到新问题,我都会先写一个最小复现脚本,让问题在最短代码路径里暴露出来。这个习惯帮我最快找到问题的根源,也让我能快速验证某个函数在当前浏览器版本下的真实行为。Selenium 版本迭代快、浏览器版本差异大,靠记忆推断远不如亲手验证靠谱。希望这篇文章能帮你少走几步弯路,有不确定的地方,动手跑一跑就知道了。