
我最早接触 Selenium 是做爬虫的时候当时被各种反爬策略折磨得够呛。后来发现光靠 requests 只能拿到静态 HTML页面里一堆数据都是 JavaScript 动态渲染出来的这时候才真正开始系统性地用 Selenium 驱动真实浏览器去处理这些动态内容。到今天我已经用 Selenium 跑过电商网站的商品批量采集、自动登录、表单自动填写、定时巡检、UI 回归测试也经历了无数个版本的坑。这篇文章我打算把从零开始到实际能干活的全过程写下来包括环境搭建里最容易踩的版本匹配问题、定位元素的实战策略、遇到动态下拉框怎么处理、运行稳定性怎么调优以及一些你翻官方文档不太容易直接找到的高级技巧。内容会更偏向实战不是那种抄官方文档的翻译稿而是我在真实项目中验证过、踩过坑之后留下的一套可复用方案。如果你是刚接触 Python Selenium 的新手这篇文章能帮你少走至少两周弯路。如果你已经写过一些脚本但总觉得不稳定、难维护那本文后半部分的高级技巧和封装思路应该能给你一些启发。1. 环境搭建里的隐藏关卡版本匹配才是第一个真正的拦路虎很多人以为装 Selenium 是 pip install selenium 一条命令的事但真正上手之后你会发现80% 的环境问题根本不是出在 Python 包本身而是出在浏览器驱动和浏览器版本不匹配上。我第一次搭建环境的时候装好了 Selenium结果运行脚本直接报 SessionNotCreatedException浏览器页面刚闪一下就没了日志提示 chromedriver 版本和 Chrome 版本对不上。1.1 一套清爽的安装顺序先说结论一套干净且不容易出问题的安装顺序是这样的# 1. 安装 Python 环境建议 3.8 及以上版本 # 2. 建议先创建虚拟环境避免污染全局环境 python -m venv selenium_env source selenium_env/bin/activate # Windows 为 selenium_env\Scripts\activate # 3. 安装 Selenium 包 pip install selenium # 4. 安装浏览器驱动以 Chrome 为例 # 推荐使用 webdriver-manager 自动管理驱动版本 pip install webdriver-manager这里我特别推荐使用 webdriver-manager 这个工具它能自动检测你本机浏览器的版本然后自动下载相匹配的驱动省去手动对应版本号的痛苦。你只需要这样写from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager driver webdriver.Chrome( serviceService(ChromeDriverManager().install()) ) driver.get(https://www.baidu.com)这比手动下载 chromedriver 再配置路径要省心太多。但如果你的网络环境和系统策略不允许自动下载那就只能走手动路线先打开浏览器访问 chrome://version 查看版本号再去 ChromeDriver 镜像站下载对应版本的驱动。需要注意的细节是大版本号必须完全一致比如 Chrome 是 120.0.6099.109那驱动也要是 120 开头的版本小版本号可以不完全一样但最好选相近的。1.2 浏览器自动化初体验环境装好之后第一步建议先跑一个最小脚本验证链路是否通畅。我就是靠这样一个脚本确认驱动、浏览器、Selenium 三者已经完全打通from selenium import webdriver import time driver webdriver.Chrome() driver.get(https://www.baidu.com) print(页面标题:, driver.title) time.sleep(3) driver.quit()如果控制台能输出“页面标题: 百度一下你就知道”那说明环境已经通了。如果跑这一步就报错我建议按下面的表格依次排查报错信息关键词可能原因处理方式ModuleNotFoundError: No module named seleniumSelenium 未安装或装错环境确认是否在正确的虚拟环境中执行 pip installSessionNotCreatedException驱动版本和浏览器版本不匹配使用 webdriver-manager 或手动下载对应版本WebDriverException: unknown error: cannot find Chrome binary找不到浏览器可执行文件用 Options 指定 binary_location 指向 Chrome 可执行文件TimeoutException页面加载超时设置 page_load_strategy 或显示等待这一轮做完环境才算真正立住了。很多人环境搭好以后直接冲进定位元素阶段但如果前面版本不匹配的坑没排掉后面所有代码都会在最基础的启动环节失败排查成本反而更高。2. WebDriver 的运作逻辑搞懂协议才能写出不玄学的代码很多初学者觉得 Selenium 是个黑魔法调一下 API 就能操作浏览器。实际上 Selenium 之所以能控制浏览器靠的是一套 HTTP 协议接口也就是 WebDriver 协议。浏览器启动时chromedriver 会在本地监听一个端口Selenium 库通过发送 JSON 格式的 HTTP 请求给这个端口命令浏览器去执行点击、输入、跳转等操作然后把执行结果以 JSON 响应返回。理解这套机制有什么用它决定了你在写代码时候的很多直觉其实是错的。比如Selenium 定位元素的时候每次 find_element 都相当于一次 HTTP 往返请求。如果你在一个循环里频繁定位同一个元素那性能开销会非常大。我在做大批量数据采集时就曾经因为循环里每次都重新定位元素导致整个脚本运行耗时是优化后的 3 倍以上。另一个对理解协议有帮助的点是Selenium 不是实时操作浏览器而是发送指令给浏览器执行。所以当页面 JavaScript 动态修改了 DOM 之后你的 Selenium 操作何时生效、元素何时可以被定位到完全取决于浏览器的执行状态而不是你代码里的时序。这就是为什么需要显式等待而不是简单 time.sleep 的原因。举个例子很多新手写代码会是这样# 新手写法死等固定时间 driver.find_element(By.ID, username).send_keys(admin) time.sleep(5) driver.find_element(By.ID, login-btn).click()这种写法在网络慢或者页面加载快的时候都容易出问题固定 5 秒可能太长浪费效率也可能太短元素还没出来就报错。更合理的思路是通过 WebDriverWait 配合条件等待让程序问浏览器元素是否就绪就绪就继续没就绪就一直等到超时。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 推荐写法显式等待元素可点击 wait WebDriverWait(driver, 10) login_btn wait.until( EC.element_to_be_clickable((By.ID, login-btn)) ) login_btn.click()理解 WebDriver 协议的人还会明白一件事元素定位是一次性快照而不是实时引用。当你拿到一个 WebElement 对象后再去操作它Selenium 每次操作前还是会重新去浏览器里确认这个元素是否仍然存在于 DOM 中。如果页面结构已经被 JavaScript 重绘了那之前拿到的元素引用会变成 stale element reference 异常StaleElementReferenceException这一点在动态页面上极其常见后面我会专门讲怎么规避。3. 定位策略的实战选择从 id 到 XPath以及那个让无数人头疼的自定义下拉框Selenium 定位元素总共提供了八种策略ID、Name、Class Name、Tag Name、Link Text、Partial Link Text、XPath、CSS Selector。理论上一说大家就懂但实际写起来选哪个、什么时候用哪个里面是有讲究的。3.1 优先级怎么排我把定位策略按优先使用顺序排了一个序这是我在多个真实项目里验证过的心态和执行策略ID页面里唯一标识定位速度最快代码最可读能优先用就用Name、Class Name表单场景用得多但要注意同名和同 class 可能不止一个元素CSS Selector在复杂层级下比 XPath 更简洁性能也更好XPath功能最强大但尽量用相对路径和属性定位少用绝对路径Link Text / Partial Link Text专门定位 a 标签简单场景好用这里我想专门提醒一下 XPath 的绝对路径问题。很多新手会用浏览器的检查功能右键 Copy XPath得到一个类似 /html/body/div[1]/div[2]/div[3]/div[1]/form/input 的绝对路径。这种路径在页面改一个 div 层级就立刻失效毫无健壮性可言。正确做法是尽量用元素本身的属性特征来定位比如 //input[nameusername] 或者 //button[contains(text(),登录)]。另外XPath 里 contains() 和 text() 这两个函数我几乎每个项目都会用到。比如页面上有一个按钮文字是确认提交但前面还有空格或者动态拼接内容写成 //button[contains(text(),确认)] 就能模糊匹配到。3.2 自定义下拉框的定位一个反直觉的实战案例我在自动化采集一个跨境电商后台时遇到了一个典型的自定义下拉框不是原生 select 标签而是用 div ul li 组合模拟的。这种组件现在前后端框架里太常见了Element UI、Ant Design 这类组件库里的下拉框基本都是自定义渲染的。原生 select 可以用 Select 类来处理from selenium.webdriver.support.ui import Select select Select(driver.find_element(By.ID, category)) select.select_by_visible_text(电子类目)但自定义下拉框完全不吃这一套Select 类拿它没办法。我第一次遇到的时候按原生 select 的方式处理直接报 UnexpectedTagNameException后来才意识到这种组件的交互逻辑是先点击一个触发器通常是一个 div 或 input然后弹出一个隐藏的 ul 列表再点击 li 选项。正确做法分三步from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 第一步点击下拉框触发器展开列表 trigger driver.find_element(By.XPATH, //div[contains(class,el-select)]) trigger.click() # 第二步等待选项列表出现并可见 options WebDriverWait(driver, 10).until( EC.visibility_of_all_elements_located( (By.XPATH, //ul[contains(class,el-select-dropdown)]/li) ) ) # 第三步遍历选项找到目标文本并点击 for option in options: if option.text.strip() 电子类目: option.click() break这里有几个关键的坑值得展开说。第一个坑点击触发器之后下拉列表不会同步渲染出来如果立刻去定位 li 元素大概率找不到或者处于不可见状态所以必须用 visibility_of_all_elements_located 等待列表真正显示。第二个坑是 li 的 text 可能带前后空格直接判断 option.text 电子类目 会匹配失败所以要做 strip() 处理。第三个坑是这种自定义下拉框通常有动画过渡效果比如 CSS 的 opacity 从 0 变成 1即使元素被找到了也需要额外等一小段时间才能点击成功。稳妥的做法是在点击前再增加一次 element_to_be_clickable 判断。3.3 动态表单里最容易出现的 StaleElementReferenceException动态页面里最臭名昭著的一个异常就是 StaleElementReferenceException。它本质上是因为你持有的 WebElement 对象已经引用了过期的 DOM 节点而页面被 JavaScript 更新过后那个节点已经从 DOM 树中被移除或替换了。我遇到过最典型的一个场景一个表格的筛选功能每次点击搜索按钮后table 内部的 tr 元素全部被重新渲染。如果脚本里在点击搜索前就保存了一个 tr 的引用点击之后再拿它去取子元素浏览器就会告诉你这个引用已经失效了。解决思路也很简单把获取元素这个动作放到每次操作里重新执行不要存引用# 错误示范先拿引用后续操作还可能跨过页面刷新 rows_before driver.find_elements(By.XPATH, //table/tbody/tr) # 正确示范每次操作前重新获取 for i in range(1, 10): current_rows driver.find_elements(By.XPATH, //table/tbody/tr) target_row current_rows[i] target_row.find_element(By.XPATH, .//button[contains(text(),编辑)]).click() # 执行编辑操作... driver.back()另一个更彻底的方案是用重试装饰器封装操作如果遇到 StaleElementReferenceException 就重新定位再试一次。这个技巧在复杂的后台管理系统里非常好用后面封装部分我会给出完整代码。4. 元素枚举与定位元数据让脚本具备体检报告能力的进阶思路热词里有两条我特别想展开一个叫selenium 页面元素枚举一个叫仅存储定位元数据。这两个概念听起来有点抽象但实际上是工程化实践里的关键一环。4.1 页面元素枚举把整个页面的可交互元素摸一遍底元素枚举说白了就是把当前页面里所有能被用户交互的元素全部找出来包括按钮、输入框、链接、下拉框然后统一提取它们的定位元数据比如 id、name、class 等属性。做这件事的第一个价值在于新接触一个陌生系统时你想自动化它第一步应该先摸清页面结构而不是凭肉眼猜元素属性。我做过一个操作用一段脚本把页面所有 input、button、a、select 标签的信息打印出来立刻就对整个页面结构有了全面认知。from selenium import webdriver from selenium.webdriver.common.by import By driver webdriver.Chrome() driver.get(https://example.com/login) tags [input, button, select, a] for tag in tags: elements driver.find_elements(By.TAG_NAME, tag) print(f\n {tag} 标签共 {len(elements)} 个 ) for el in elements: # 取出元素的通用定位元数据 print({ tag: tag, id: el.get_attribute(id), name: el.get_attribute(name), class: el.get_attribute(class), text: el.text[:20] if el.text else , visible: el.is_displayed(), })这个输出的意义就好比给页面做了一次体检不用再靠肉眼和 F12 硬翻。而且打印出来的这些字段值本身就是你后续写 XPath 或 CSS 定位时最需要的原料。4.2 把定位元数据抽离出去和代码解耦仅存储定位元数据这个思路我是在维护一个用了三年的自动化项目时彻底想明白的。项目早期我把定位信息全部硬编码在脚本里比如 driver.find_element(By.ID, login-btn)。当时写起来很爽但后来页面一改版光是替换 ID 就找了几十个文件改到心态爆炸。后来我采用的方式是把所有定位元数据集中到一个 YAML 或者 JSON 字典里代码只负责读取。页面结构变了只改配置文件不动核心逻辑。# locators.yaml login_page: username_input: type: id value: username password_input: type: name value: password login_button: type: xpath value: //button[contains(text(),登录)] error_msg: type: css value: .login-error配合一个简单的读取封装import yaml from selenium.webdriver.common.by import By class LocatorManager: def __init__(self, filepathlocators.yaml): with open(filepath, r, encodingutf-8) as f: self.data yaml.safe_load(f) def get(self, page, element): item self.data[page][element] mapping { id: By.ID, name: By.NAME, class: By.CLASS_NAME, xpath: By.XPATH, css: By.CSS_SELECTOR, } return mapping[item[type]], item[value] # 使用示例 locators LocatorManager() by, loc locators.get(login_page, username_input) driver.find_element(by, loc).send_keys(admin)这层封装在项目复杂之后收益是巨大的。团队里其他同事接手的时候不需要读一行代码只需要打开配置文件就能知道所有页面元素定义在哪个位置以及对应的定位策略是什么。调试排查的效率大大提升。5. 从能跑到跑得稳等待机制、动作链和实用高级技巧基础定位和操作会了之后你很快会发现脚本能不能稳定跑下来往往不是靠定位写得多准而是靠应对页面动态变化和复杂交互的能力。这一节我分享几个被反复验证过的高级技巧。5.1 显式等待不是摆设它比 time.sleep 高明在哪儿显式等待的核心优势是让程序不断轮询页面状态直到某个条件满足或超时。对比 time.sleep它不会在元素已经可用的场景下白白浪费时间也不会在元素迟迟不出现的时候提前报错。Selenium 里最常用的一些 Expected Conditions 包括presence_of_element_locatedDOM 里存在即可不一定可见visibility_of_element_located存在且可见element_to_be_clickable存在、可见、且可点击text_to_be_present_in_element元素文本包含指定内容frame_to_be_available_and_switch_to_itiframe 可切换我在实际项目中基本只用三种场景的等待页面跳转完成后等元素出现、点击按钮后等新内容渲染出来、以及等待某个接口返回导致的高亮或提示文案出现。如果页面上同时有多个条件需要满足可以把条件组合起来wait WebDriverWait(driver, 10) # 等待两个条件同时满足 wait.until( lambda d: d.find_element(By.ID, loading).is_displayed() False and d.find_element(By.CLASS_NAME, result-list).is_displayed() )这种 lambda 写法比单纯堆积 expected_conditions 灵活得多尤其在处理自定义加载动画、模态框消失这类特定状态时特别好用。5.2 ActionChains处理悬停、拖拽、右键这类特殊操作ActionChains 是 Selenium 里处理复杂鼠标键盘交互的关键类。真实的 Web 系统里很多功能根本不是单纯的 click 和 send_keys 能搞定的。比如最常见的二级菜单鼠标必须悬停到一级菜单上才能展开二级菜单。from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By # 鼠标悬停触发下拉菜单 first_menu driver.find_element(By.XPATH, //li[contains(text(),用户管理)]) ActionChains(driver).move_to_element(first_menu).perform() # 拖拽操作 source driver.find_element(By.ID, drag-item) target driver.find_element(By.ID, drop-zone) ActionChains(driver).drag_and_drop(source, target).perform() # 右键菜单 ActionChains(driver).context_click(source).perform()拖拽操作在自动化测试里非常常见尤其是那种自定义排序列、看板拖拽任务卡片之类的功能。如果 ActionChains 的拖拽在某些场景下失效可以试试用点击加 move_by_offset 的方式模拟分步拖拽这也是很多项目里沉淀的土办法。5.3 execute_script 是你最趁手的后门Selenium 操作的是浏览器而浏览器最核心的能力其实是 JavaScript。很多页面操作用 Selenium 原生 API 做起来别扭但只要引入 execute_script就能直接突破。最常见的两个场景第一个是滚动页面到底部。Selenium 没有直接提供 scroll 方法用 ActionChains 发送 End 键有时候在特定页面无效最稳妥的是执行 JS# 滚到页面底部 driver.execute_script(window.scrollTo(0, document.body.scrollHeight)) # 让元素滚动到可见区域 element driver.find_element(By.ID, confirm-btn) driver.execute_script(arguments[0].scrollIntoView();, element)第二个是修改元素的属性比如有些日期输入框是 readonly 的直接 send_keys 会被浏览器拒绝。这时候先通过 JS 去掉 readonly 属性再输入就畅通无阻from selenium.webdriver.common.by import By date_input driver.find_element(By.ID, date-picker) driver.execute_script(arguments[0].removeAttribute(readonly), date_input) date_input.clear() date_input.send_keys(2024-06-01)还有一个很实用的场景是隐藏元素的强制点击。某些页面元素明明存在但因为布局原因被其他元素挡住点击会提示element click intercepted。这时候可以先用 JS 把元素滚动到视口中心或者直接强制触发 click 事件driver.execute_script(arguments[0].click();, element)这个方法是双刃剑它能绕过很多前端交互限制但也可能跳过了前端框架的某些事件绑定逻辑所以只建议在页面确实因为布局遮挡导致原生 click 失败的情况下使用。5.4 headless 模式与加载策略无头模式不是把所有东西都隐藏起来自动化脚本在部署到服务器或者定时任务里时通常不会再打开一个可视化浏览器窗口这时候就需要启用 headless无头模式。但 headless 模式最大的坑在于并不是所有浏览器行为都和带窗口时完全一致某些网站会检测 window.navigator 相关属性来识别是否为自动化环境。我实际使用中发现Chrome 的无头模式现在合并了老版本和新版本两种模式配置上有个细节from selenium.webdriver.chrome.options import Options options Options() options.add_argument(--headlessnew) # 新版无头模式更接近真实浏览器 options.add_argument(--window-size1920,1080) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) driver webdriver.Chrome(optionsoptions)这些参数分别解决什么问题window-size 是防止无头模式下默认窗口过小导致某些响应式布局的元素被隐藏disable-blink-features 和 excludeSwitches 则是减少被网站识别为自动化的概率。这组配置不是 100% 能对抗所有网站的风控但对于开源自动化项目来说已经能解决 80% 的场景。加载策略也是一个常被忽略的优化点。Selenium 默认页面加载策略是 normal也就是等所有资源都加载完成才执行下一步。如果页面里有大量图片、广告、统计脚本这个等待时间会非常长。我有一次采集一个图片站全部等资源加载导致单页耗时超过 20 秒后来把加载策略改成 eager 后整个任务跑完时间直接缩短了一半options.page_load_strategy eager # 或 options.page_load_strategy noneeager 表示 DOM 就绪后就认为页面加载完成不再等图片和样式表none 表示不等待页面加载适合已经不需要关心加载状态的场景。这两种方式在提速上效果显著代价是需要更可靠地使用显式等待来确认关键元素是否出现否则容易拿到半成品页面。5.5 拦截和伪造网络请求Selenium 4 开始支持完整的 CDPChrome DevTools Protocol能力可以做到监听网络请求、修改请求头、返回 mock 数据等操作。所以在项目里我能拿到很多以前 Selenium 做不到的信息。比如我想知道点击登录按钮后后端 API 返回了什么不用 F12 手动看直接在脚本里监听from selenium.webdriver.common.devtools.v85.network import RequestWillBeSent, ResponseReceived # 伪代码实际写法看 Selenium 版本 API driver.execute_cdp_cmd(Network.enable, {}) # 监听 response 事件回调虽然 Selenium 的 CDP 接口比起 Playwright 来说没那么优雅但对于老项目迁移成本低的优势是明显的。我做过最有价值的一件事就是通过 CDP 拿到页面上所有 XHR 请求的响应 JSON把 Selenium 定位不到的散落数据从接口里补齐大大减少了页面解析的工作量。6. 稳定性优化与测试运行错误处理、失败重试、并发与维护策略脚本在本地能跑和在生产环境能稳定跑完是两个完全不同的世界。我在开发阶段经常遇到三五分钟报一次错真正跑批量任务时问题更多几乎每周都有人在群里喊脚本又挂了。下面几个方向是稳定性的关键。6.1 给脚本加上健壮的错误处理最常见的错误处理需求是某一步操作失败不直接退出整个任务而是记录日志并跳转到下一条数据继续执行。为此我写了一个重试装饰器用在所有点击、输入等核心交互逻辑上from selenium.common.exceptions import StaleElementReferenceException, ElementNotInteractableException import time def retry_on_stale(max_retries3, interval0.5): def decorator(func): def wrapper(*args, **kwargs): last_exception None for _ in range(max_retries): try: return func(*args, **kwargs) except (StaleElementReferenceException, ElementNotInteractableException) as e: last_exception e time.sleep(interval) raise last_exception return wrapper return decorator retry_on_stale() def safe_click(element): element.click()这个装饰器的作用是当页面重新渲染导致之前的元素引用失效或者当前不可被点击时重新执行整个函数逻辑最多尝试三次。三次后仍然失败才向上抛异常交给更上层的业务处理逻辑。6.2 页面操作的对象化管理一个简单的 Page Object 实践在写大项目时不建议把操作逻辑直接散落在主流程里。虽然不要求你上完整的 Page Object ModelPOM但至少应该做到每个页面一个类页面的关键操作封装成方法。class LoginPage: def __init__(self, driver): self.driver driver self.username (By.ID, username) self.password (By.NAME, password) self.login_btn (By.XPATH, //button[contains(text(),登录)]) def login(self, username, password): self.driver.find_element(*self.username).send_keys(username) self.driver.find_element(*self.password).send_keys(password) self.driver.find_element(*self.login_btn).click()这样做的好处是页面元素定义和业务逻辑分离别人看你代码的时候一眼就知道登录这个操作涉及哪些元素。而且一旦页面改版只需要修改页面类里的元组定义值所有依赖它的测试和采集任务自动适配。6.3 分布式与并发的取舍很多场景下需要同时处理多个账号或多个页面这时候单线程 Selenium 就成了性能瓶颈。我试过几种并发方案。第一种是多进程因为 GIL 的存在多线程对 CPU 密集任务帮助不大而且 Selenium 的 driver 对象本身线程不安全不同线程之间不能共享同一个 driver 实例。from concurrent.futures import ProcessPoolExecutor def process_account(account): driver webdriver.Chrome() try: # 每个进程创建独立的浏览器实例 driver.get(https://example.com/login) driver.find_element(By.ID, username).send_keys(account) # ... finally: driver.quit() accounts [user1, user2, user3] with ProcessPoolExecutor(max_workers3) as executor: executor.map(process_account, accounts)第二种是基于 Docker 的 Selenium Grid浏览器在容器里运行宿主机只是发命令。这种架构更适合正式的测试团队扩展性和隔离性都更好但对个人项目来说部署成本偏高。从我个人实践看如果只是几十个账号的批量任务本地多进程配合合理的重试策略就已经够用。如果规模上千再去考虑 Selenium Grid 或者加一层任务队列会更合适。普通项目一上来就搭集群运维复杂度会反噬开发效率。6.4 日志与轨迹记录最后想强调一个很多教程不会讲、但实战中非常救命的东西日志与截图留痕。脚本在凌晨定时跑失败了如果只留下Error: timeout这几个字第二天你根本无从排查是哪个页面、哪一步、什么状态出了问题。我的做法是三步走第一步每次关键操作后打印带时间戳和页面标题的日志。import logging logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) def step_log(driver, message): logging.info(f{message} | 当前URL: {driver.current_url} | 标题: {driver.title})第二步发生异常时自动截图保存到本地。try: # 业务操作 pass except Exception as e: timestamp time.strftime(%Y%m%d_%H%M%S) driver.save_screenshot(ferror_{timestamp}.png) raise第三步页面关键状态记录到文件或者直接存到 Excel、数据库中。这样即使任务跑了一半崩了管理者也知道处理到哪一批了下次运行可以直接断点续跑。这些看起来不高级的小技巧恰恰是脚本从玩具工具变成生产工具最关键的转折点。我见过太多人写的自动化脚本逻辑没问题但一上线就跑飞根本原因不是定位写错了而是没有可靠的运行生命周期管理和异常现场保留。7. 最后的工程化建议维护比开发更考验功力写到这里我手上已经把这套从环境搭建到稳定性优化的完整路径整理出来了。但坦白说工具技巧只是表层真正能让你在这方面拉开差距的是维护一套自动化脚本的工程能力。我强烈建议你在项目初期就把定位元数据从代码中抽离出来尽早引入日志和截图机制而不是等功能写完了再补。因为等你代码量大了之后再去重构成本会成倍增加。页面一次改版、一个元素 ID 的变化就会让几十处代码瞬间失效那时候你就会理解为什么说定位元数据是最宝贵的资产。另外还想提一个容易被忽略的点Selenium 项目不是写完就结束了它和业务页面是强耦合的。前端每次升级框架引入新的组件库都会给 Selenium 定位增加新的挑战。我这篇文章里详细写的自定义下拉框就是典型例子即使你这次遇到了原生 select下一次可能就会遇到带搜索框的可远程搜索下拉框交互更复杂。多积累一些应对这类复杂组件的手感比背 API 有用得多。我个人的习惯是每接一个新项目先写一个元素枚举脚本把页面底数摸清再手动操作一遍业务流程每个步骤截图留档然后才开始写正式的自动化逻辑。这个流程看起来慢实际上后面调试省下的时间是前期的好几倍。这套方法论我已经用在小程序端自动化、Web 管理后台回归测试、电商采集等多个项目里结论都是稳定且可复制。如果你也准备做类似的事不妨顺着这个路线一步一个脚印地把它落地。