从“写脚本点到手软”到“让浏览器自己干活”,Selenium 始终是 Python 自动化绕不开的那扇门。这篇文章我会从 Python 环境搭建、Selenium 安装和浏览器驱动匹配这些最基础但也最容易翻车的环节开始,讲到定位策略、下拉框处理、等待机制和多标签页管理这些实战里高频使用的进阶技巧。目标是让刚接触浏览器自动化的新手能照着一步步跑通,也让写过一段时间脚本的人能避开我之前踩过的那些坑。
1. 环境搭建:从零到能跑的第一步
环境搭建本身没什么高深的东西,但恰恰是这一步卡住了最多人。我见过不少朋友卡在selenium.common.exceptions.WebDriverException: Message: 'chromedriver' executable needs to be in PATH这种报错上好几天。说白了,Selenium 的原理是:它不是直接在浏览器内部跑脚本,而是通过一个“驱动程序”作为中介,用代码告诉驱动,驱动再驱动浏览器去执行操作。所以环境搭建的核心就三件事:装 Python、装 Selenium 库、装与浏览器版本匹配的驱动。
1.1 Python 装不装得好,直接决定后面顺不顺
Python 的安装本身不难,但“装好了”和“用起来了”是两回事。先说一个最常见的问题:很多人从官网下载安装包,安装时忘了勾选“Add Python to PATH”,结果在命令行里输入python就提示python was not found。这句话我帮别人排查过无数次,原因基本都是这个。如果已经装错了,最简单的办法是卸载重装,勾选上 PATH 选项;不想重装的话,也可以手动把 Python 的安装目录和Scripts目录加到系统环境变量里。
另外一个容易忽略的点是Python 版本的选择。Selenium 4.x 对 Python 版本要求其实很宽松,3.7 以上都能跑。但如果你打算后续做一些复杂的数据分析或数据处理(毕竟自动化采集完数据总得处理),我建议直接装 3.9 到 3.12 之间的版本。太老的版本库兼容性会逐渐变差,太新的版本反而是一些第三方库还没跟上。别装最新尝鲜版,稳定优先。
装完验证有没有成功,打开命令行,输入:
python --version如果显示Python 3.x.x,那就没问题了。接下来建议顺手把 pip 源换成国内镜像。这一步不是必需的,但实测下来速度差距非常明显。我之前用默认源装一个稍大点的库,经常卡在Requirement already satisfied之前那种漫长的等待里,换镜像后基本是秒下。配置方式:
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple注意:如果在公司内网或者某些受限网络环境下,镜像源可能连不上,那就老老实实用默认源,只是多点耐心等一等。
如果你用的是 Linux 系统,安装思路类似,只是发行版不同命令不一样。Ubuntu/Debian 系可以直接sudo apt install python3 python3-pip,CentOS/RHEL 系用sudo yum install python3。不过 Linux 上有个比较好的实践是装python3-venv,每个项目单独建一个虚拟环境,避免不同项目依赖冲突。Windows 上也可以用python -m venv venv建虚拟环境,我后面讲封装的时候会说到为什么要这么做。
1.2 Selenium 安装与浏览器驱动匹配
Python 环境就绪后,安装 Selenium 库就一行命令:
pip install selenium想指定版本的话就pip install selenium==4.x.x。这里多提一句:Selenium 4 和 3 的 API 差别不小,网上很多老教程用的还是 3 的写法,比如find_element_by_id,在 4.x 里直接跑会报错。建议你用 4.x,然后记住新写法:
driver.find_element(By.ID, "username")而不是:
driver.find_element_by_id("username")这两种写法在 Selenium 3 和 4 之间的差异,是新手最容易踩的坑之一。下面表格里列一下常用的对应关系:
| Selenium 3 旧写法 | Selenium 4 新写法 |
|---|---|
find_element_by_id("xxx") | find_element(By.ID, "xxx") |
find_element_by_name("xxx") | find_element(By.NAME, "xxx") |
find_element_by_class_name("xxx") | find_element(By.CLASS_NAME, "xxx") |
find_element_by_xpath("xxx") | find_element(By.XPATH, "xxx") |
find_element_by_css_selector("xxx") | find_element(By.CSS_SELECTOR, "xxx") |
find_element_by_link_text("xxx") | find_element(By.LINK_TEXT, "xxx") |
然后是驱动。Chrome 就用 ChromeDriver,Firefox 就用 GeckoDriver,Edge 用 EdgeDriver。最坑的一点是驱动版本必须和浏览器版本匹配,版本差太远就会启动时报错。
Chrome 查看版本号的方法:地址栏输入chrome://version/,或者点右上角三个点 → 帮助 → 关于 Chrome。看到版本号后,去 ChromeDriver 的下载页找到对应版本下载。Windows 下载下来是一个 exe 文件,有两种用法:
- 把 exe 放到 Python 安装目录或项目目录下,代码里指定路径:
from selenium import webdriver from selenium.webdriver.chrome.service import Service service = Service(r"C:\path\to\chromedriver.exe") driver = webdriver.Chrome(service=service)- 把 exe 所在目录加到系统 PATH,然后直接用:
from selenium import webdriver driver = webdriver.Chrome()Linux 和 Mac 上思路一样,区别是把 exe 换成可执行文件,并可能需要chmod +x chromedriver给执行权限。如果你用的是 Selenium 4.6 以上的版本,其实还有更省心的方案:Selenium Manager 会自动帮你搞定驱动匹配。也就是说,只要网络能访问,你直接webdriver.Chrome()它就自动下载正确的驱动了。不过自动下载依赖网络环境,国内有时候会失败,所以我一般还是建议手动下载驱动放到固定位置,尤其是公司内网环境,一劳永逸。
1.3 验证环境的经典探针脚本
环境装完,跑一个最简单的脚本验证整条链路是否通畅。打开编辑器,新建一个test_selenium.py:
from selenium import webdriver driver = webdriver.Chrome() driver.get("https://www.baidu.com") print(driver.title) driver.quit()如果运行后,浏览器自动打开并打印出页面标题,说明整条链路是通的。如果报错,优先检查 Chrome 和 ChromeDriver 版本号是否匹配,这是出现频率最高的错误来源。
注意:
driver.quit()一定要写,它会关闭浏览器进程并释放资源。很多人调试时习惯直接关浏览器窗口,没执行quit(),结果下次运行时出现多个残留的 chromedriver 进程,占内存不说,后续还可能引发端口冲突。
2. 定位策略:别再只会 find_element_by_id
页面元素的定位是整个自动化脚本里占比最重的工作。很多人写脚本时最顺手的定位方式是复制 XPath,但复制出来的 XPath 往往又长又脆,页面稍微改一下结构脚本就崩了。这节我分三个层次讲:常用的八种定位方式、一个从项目里总结出来的页面元素枚举与定位元数据存储思路、以及定位不到元素时的高频原因。
2.1 八种定位方式,按优先级排个序
Selenium 提供的定位方式,最常用的有八种。按照我的使用习惯,优先级从高到低排:
- ID:唯一的,速度最快,优先用。没有 ID 再看下面的。
- Name:表单元素常用,稳定性次之。
- Class Name:适合定位样式特征明显的元素,但要注意一个页面可能有多个同 class 元素。
- CSS Selector:
#id、.class、[attribute="value"],功能强大,比 XPath 少一层遍历,速度稍快。 - XPath:功能最全,可以用文本、包含关系、父子关系定位,但慎用绝对路径(
/html/body/div[1]/div[2]/...),要在相对路径上下功夫。 - Link Text / Partial Link Text:只适用于
<a>链接元素,按链接文字定位。 - Tag Name:标签名定位,命中范围太大,一般作为辅助手段,配合其他条件使用。
实际操作的时候,我的判断逻辑是:ID 不存在就找 name,再没有就用 CSS Selector 或相对 XPath。拿一个登录框举例:
<input type="text" id="username_input" name="username" class="input-large" placeholder="请输入用户名">CSS Selector 可以这么写:
#username_input input[name="username"] .input-large input[placeholder="请输入用户名"]XPath 可以这么写:
//input[@id="username_input"] //input[@name="username"] //input[contains(@placeholder, "用户名")]这里我特别想说一个点:XPath 不是越短越好,而是越稳健越好。比如//*[@id="main"]/div[2]/div[1]/div[2]/form/div[1]/input这种复制来的绝对路径,一旦页面前面加了一个广告推荐模块,后面的层级全部错乱,直接定位失败。更好的做法是找一个能代表这个元素身份的锚点,比如:
//form[@id="loginForm"]//input[@type="password"]这种写法定位的顺序是先找到一个有明确 ID 的 form,再在这个 form 内部找输入框,层级变化时很大概率还能扛住。
2.2 页面元素枚举与定位元数据存储的实践思路
这里我要展开讲一个进阶点:高质量自动化项目里,其实不太建议把定位表达式散写在业务代码里。一旦页面改版,你要一个个去代码里搜 XPath,那痛苦比写脚本本身还大。一个可参考的做法是:枚举出所有需要操作的元素,只把定位元数据集中存储,让代码与定位解耦。
什么叫只用定位元数据?意思是不存整个 WebElement 对象,而是存元素的 FindElement 条件本身。WebElement 是运行时的东西,页面一刷新就失效了,写死在代码里很脆弱;而元数据只是“怎么找到它”的描述,是不变的,页面变了只改描述即可。
实战里我见过两种典型的组织方式:
一种是用字典集中管理。比如创建一个locators.py:
from selenium.webdriver.common.by import By LOGIN_PAGE = { "username_input": (By.ID, "username_input"), "password_input": (By.NAME, "password"), "login_button": (By.CSS_SELECTOR, "button[type='submit']"), "error_msg": (By.XPATH, "//div[contains(@class, 'error')]"), } SEARCH_PAGE = { "search_input": (By.CSS_SELECTOR, "#searchInput"), "search_button": (By.CSS_SELECTOR, "#searchButton"), }用的时候:
from locators import LOGIN_PAGE username_input = driver.find_element(*LOGIN_PAGE["username_input"]) username_input.send_keys("admin")有同学可能会问,Selenium 4 里不是有ByChained或者其他 Page Object 模式吗?确实有。上面这种字典方式是轻量版,适合中小项目;项目大了以后可以考虑 Page Object Model(POM)——每个页面一个类,把页面元素和业务动作封装在一起。但不管是字典还是 POM,核心思想一致:把定位信息从业务逻辑里剥离出来,变成一套可维护的元数据。
枚举元素的另一种常见手法是对一组相似元素统一拿取,例如页面里有多条新闻列表项:
items = driver.find_elements(By.CSS_SELECTOR, "ul.news-list li") for idx, item in enumerate(items): title = item.find_element(By.CSS_SELECTOR, "h3.title") date = item.find_element(By.CSS_SELECTOR, "span.date") print(idx, title.text, date.text)用find_elements拿到的是一个列表,可以对每个 item 再继续用相对定位找子元素。这在列表页、表格页、轮播图这种重复结构下非常常用,比写一整条超长 XPath 去定位第几个元素要强得多。
2.3 定位不到元素?十有八九是这四个原因
我在实战中经常遇到定位不到元素的场景,总结下来基本是这四类:
元素在 iframe 里。网页上有嵌入的窗口、广告位、第三方支付组件等,很多都在 iframe 里。直接用主文档的 driver 去 find,一定会失败。解决办法是先用
switch_to.frame()切换进去,操作完再switch_to.default_content()切出来。元素还没加载出来。页面结构到了,但数据还在异步加载,你定位的那个按钮还没渲染出来。这个不细说,后面等待机制那节会专门讲。
元素属性动态变化。有些元素的 ID 或 class 含时间戳或随机数,每次刷新都变,一次性的定位条件自然失效。这时候就要靠相对 XPath 找不变的特征,比如用
contains(@class, "btn")这种模糊匹配。元素不是在你想的那个窗口/标签页里。点击一个链接打开了新标签页,你以为当前页面还是原来的,实际上已经切换了。多标签页的情况后面也会专门讲。
遇到定位不到,先别急着改表达式,应该先打开浏览器的开发者工具,手动 Ctrl+F 搜索你写的 XPath 或 CSS 表达式,看浏览器自己能不能找到。手动都搜不到,代码里肯定找不到;手动能搜到,那再走代码层面的排查。
3. 下拉框处理:从原生 select 到 div+ul+li 定制组件
下拉框是自动化脚本里的一类特殊交互。它的难点不在“点击”本身,而在结构判断:你以为它是一个元素,实际上它可能是一组元素的组合。这里我重点讲两类:原生<select>下拉框,和用<div><ul><li>拼出来的非原生下拉框。
3.1 原生 select 下拉框的标准处理
原生下拉框长这样:
<select id="city" name="city"> <option value="beijing">北京</option> <option value="shanghai">上海</option> <option value="guangzhou">广州</option> </select>处理原生下拉框,Selenium 封装了一个专门的类Select:
from selenium.webdriver.support.ui import Select select = Select(driver.find_element(By.ID, "city")) select.select_by_value("shanghai") # 通过 value 属性选 select.select_by_index(1) # 通过索引选,从 0 开始 select.select_by_visible_text("广州") # 通过可见文本选三个方法选哪个?我的建议是优先select_by_value,因为 value 通常不容易变,而且最接近接口层语义。索引是你实在没办法时的兜底方案,因为 options 的顺序可能会被产品调整。visible_text适合页面文本明确、value 属性缺失的情况。
另外还有两个使用频率稍低但是好用的方法:
select.deselect_all():取消所有选中项,仅多选下拉框可用。select.options:获取所有 option 元素的列表,可以用来枚举选项或校验选项个数。
如果你想遍历下拉框里的所有选项,可以这么写:
all_options = select.options for opt in all_options: print(opt.text, opt.get_attribute("value"))注意:如果页面报错说
Select only works on <select> elements,说明这个下拉框不是原生 select,而是后面的 div+ul+li 结构,那就用下面的方法处理。
3.2 非原生下拉框(div+ul+li 组合)的定位与操作
实战里遇到最多的坑是这种下拉框:
<div class="custom-select" id="provinceSelect"> <div class="select-trigger">请选择省份</div> <ul class="select-options" style="display: none;"> <li>trigger = driver.find_element(By.CSS_SELECTOR, "#provinceSelect .select-trigger") trigger.click()点击之后,原本display: none;的 ul 会显示出来。注意这里有个隐性条件——某些下拉框展开是有动画的,或者下拉选项是异步加载的,所以点击后最好等一下再定位选项。
第二步:等待下拉选项可见,然后点击目标项。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait = WebDriverWait(driver, 10) target_option = wait.until( EC.element_to_be_clickable((By.XPATH, "//li[contains(text(), '广东')]")) ) target_option.click()这里用contains(text(), '广东')做模糊匹配,可以兼容文本里有多余空格或换行的情况。如果 li 的>//li[@data-value='guangdong']
第三步:验证选择结果。
选项点完之后,通常会体现在 trigger 文本的变化上:
trigger_text = driver.find_element(By.CSS_SELECTOR, "#provinceSelect .select-trigger").text assert trigger_text == "广东"处理这种定制组件,最核心的是搞明白它展开前的隐藏条件是什么。有的下拉框是display: none,有的是visibility: hidden,有的是通过 class 控制显隐。你在写定位表达式时,要先在浏览器开发者工具里确认展开前后的 DOM 变化,然后选择合适的等待条件。
我之前遇到过一个奇葩下拉框,点击触发器后整个列表是以弹层形式挂在 body 底部的,跟原来的 div 结构毫无关系。这意味着你不能在原来的父级 div 下面找选项,要去 body 底部找。这类结构用 XPath 全局定位反而更方便:
//div[contains(@class, 'dropdown-menu')]//li[contains(text(), '目标项')]所以千万不要想当然地认为选项肯定写在触发器旁边,实际结构要在开发者工具里把 DOM 展开看清楚。
3.3 弹窗、悬浮与 iframe:穿插出现的三类坑
下拉框很多时候不是单独出现的,它经常和弹窗、悬浮层、iframe 搅在一起。我遇到过一种页面:点一个按钮先弹出模态框,模态框里嵌套了一个 iframe,iframe 里又是一堆 div+ul+li 的下拉框。这种套娃结构,新手很容易层层卡住。
逐个分析:
弹窗出现时,主窗口会有一个半透明遮罩层。如果弹窗上的元素定位不到,先检查是不是遮罩层挡住了点击。Selenium 里
element.click()要求元素可见且可交互,被遮罩覆盖时通常会抛ElementClickInterceptedException。解决办法有两个:一是等弹窗完全出现后操作,二是必要时用execute_script强制点击。悬浮层(hover 才出现的菜单)用的是动作链。比如鼠标悬停到“用户中心”才会显示“个人资料”链接:
from selenium.webdriver.common.action_chains import ActionChains user_center = driver.find_element(By.LINK_TEXT, "用户中心") ActionChains(driver).move_to_element(user_center).perform() driver.find_element(By.LINK_TEXT, "个人资料").click()悬浮菜单的处理关键是move_to_element之后要给菜单显示留一点时间,不要立刻去定位菜单里的元素。可以time.sleep(0.5),也可以用 WebDriverWait 去等到目标元素可点击。
- iframe 的切换逻辑前面提过,再补充一个细节:iframe 有嵌套时,要一层一层切进去,切回来也要一层一层回退或者直接
switch_to.default_content()。Selenium 4 里还支持用 WebElement 直接切换:
iframe_el = driver.find_element(By.CSS_SELECTOR, "iframe[name='modal-iframe']") driver.switch_to.frame(iframe_el) # 操作 iframe 内部元素 driver.switch_to.default_content()提示:如果你用
switch_to.frame()时传的是字符串,Selenium 会先去匹配id或name属性。如果 iframe 既没有 id 也没有 name,就得通过 XPath 或 CSS 找到 iframe 元素再传入。
4. 高级技巧:让自动化脚本从“能跑”到“靠谱”
环境搞定、定位熟练之后,脚本能跑起来了,但距离“稳定可靠”还有一大步。这节讲的几个技巧,是我在维护长周期自动化脚本时最受益的部分:等待机制、多标签页管理、失败现场保存、以及如何把脚本封装成可以维护的方案。
4.1 等待机制:sleep 不是不行,但别用成习惯
很多初学者的脚本里到处是time.sleep(3),这种做法最大的问题是:环境快的时候白等,环境慢的时候又不够等。等待的本质是解决渲染和异步请求的时序问题,所以正确的做法是“条件等待”,而不是“死等”。
WebDriverWait 是条件等待的主力。它配合 expected_conditions(简称 EC)使用,能够轮询判断某个条件是否满足。常用的几个条件:
element_to_be_clickable:元素可见且可点击,用于按钮、链接、菜单项。visibility_of_element_located:元素可见,用于判断异步内容是否渲染完成。presence_of_element_located:元素在 DOM 树里存在,不一定可见。text_to_be_present_in_element:元素文本包含指定内容,常用来等待状态变化。
一个典型的用法:
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, "loginButton"))) login_btn.click()这里有几个细节值得注意:
- WebDriverWait 的默认轮询频率是 0.5 秒,如果你需要更快或者更慢,可以显式传入
poll_frequency。 - 不要两个等待条件套着用,容易把逻辑搞复杂。比如先 wait 元素存在,再 wait 元素可见,其实一个
visibility_of_element_located就涵盖了存在+可见两层。 - WebDriverWait 捕获超时异常时,建议在
except TimeoutException里截图保存当前页面状态,这能大幅提升排查效率。
那time.sleep就完全不能用吗?也不是。有些场景没有原生的等待条件,比如等待一段动画播完、等待某个 JS 计算完成,这时候适当用time.sleep(1)配合条件等待兜底,是合理的。但原则是:优先条件等待,sleep 只是补充。
4.2 多标签页管理、截图与失败现场保存
多标签页是自动化里容易懵的一个点。点击一个target="_blank"的链接,浏览器会新开标签页,但 Selenium 的 driver 并不会自动切过去。你要手动切换:
# 点击前先记录当前窗口句柄 main_window = driver.current_window_handle # 点击链接,新标签页打开 driver.find_element(By.LINK_TEXT, "打开新页面").click() # 等待新窗口出现 WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2)) # 切换到新窗口 new_window = [handle for handle in driver.window_handles if handle != main_window][0] driver.switch_to.window(new_window) # 操作完,回到主窗口 driver.switch_to.window(main_window)这里最容易犯的错是:只判断了len(driver.window_handles) == 2就切换,但页面可能开了不止一个标签页。所以切换时最好用列表推导式找出“不是当前句柄的那个”,而不是写死window_handles[-1]。
失败现场保存在长期无人值守运行时特别重要。最简单的做法是在except里保存截图和页面源码:
import time from selenium.common.exceptions import TimeoutException try: wait.until(EC.element_to_be_clickable((By.ID, "submitBtn"))) except TimeoutException: timestamp = time.strftime("%Y%m%d_%H%M%S") driver.save_screenshot(f"error_{timestamp}.png") with open(f"page_{timestamp}.html", "w", encoding="utf-8") as f: f.write(driver.page_source) raise截图能直接看到当时页面长什么样,页面源码可以让你事后在本地打开复现,排查是定位条件问题还是页面本身没有加载出来。这一步投入成本极低,但回报极高。
4.3 把脚本封装成可维护的方案
我见过太多人把几百行代码写在一个文件里,函数不拆,参数写死,定位表达式满天飞。一开始跑得很爽,维护起来只想跑路。这里分享一个轻量但实用的封装思路:
把浏览器初始化逻辑单独封装。因为 Selenium 启动浏览器的参数比较多,而且你可能需要在不同环境切换无头模式、user-agent、代理(指的是网络代理,这里提醒一下:脚本所在环境如果有代理设置,要注意它可能影响连接,但这里不展开)。封装函数的核心是让所有参数集中在函数签名里,调用方不需要关心细节:
def create_driver(headless=False, window_size=(1920, 1080)): options = webdriver.ChromeOptions() if headless: options.add_argument("--headless=new") options.add_argument(f"--window-size={window_size[0]},{window_size[1]}") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") return webdriver.Chrome(options=options)再封装一层通用的页面操作工具。比如点击、输入、等待元素统一走工具函数,业务层不直接接触 Selenium 的 API。这样后续换元素定位方式、调整等待策略,只改工具层就能全局生效。
def wait_and_click(driver, locator, timeout=10): wait = WebDriverWait(driver, timeout) element = wait.until(EC.element_to_be_clickable(locator)) element.click() def wait_and_input(driver, locator, text, timeout=10): wait = WebDriverWait(driver, timeout) element = wait.until(EC.visibility_of_element_located(locator)) element.clear() element.send_keys(text)然后是任务流程层。把业务流程分成若干个大步骤,每个大步骤对应一个函数,函数内部再调用工具层。这样你在看代码时,第一眼就能明白整个自动化在干什么,而不是陷在细节里。
这种分层的结构,说白了就是让“做什么”和“怎么做”分开。定位表达式、等待参数属于“怎么做”,业务逻辑属于“做什么”。这两者混在一起时,页面一改版,你连业务逻辑的代码也要跟着动;分开以后,页面改版只动元数据即可。
5. 常见问题与排查技巧实录
最后这部分是实打实的经验库。我把这些年里遇到频率最高的、以及帮别人排查最多的问题整理成一张速查表,附上我自己的排查思路。遇到问题先对号入座,能省很多时间。
5.1 安装与环境问题速查
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
'chromedriver' executable needs to be in PATH | 驱动不在环境变量里 | 把 chromedriver 所在目录加入 PATH,或代码里指定路径 |
SessionNotCreatedException: This version of ChromeDriver only supports Chrome version xxx | 驱动版本与浏览器版本不匹配 | 下载与浏览器版本匹配的驱动 |
ModuleNotFoundError: No module named 'selenium' | Selenium 库未安装或装错 Python 环境 | pip install selenium,检查当前环境是哪个 Python |
python was not found; run without arguments to install | 安装 Python 时未勾选 Add to PATH | 重装勾选 PATH,或手动修改环境变量 |
ConnectionRefusedError或驱动启动后闪退 | 端口被占用或残留的 chromedriver 进程 | 任务管理器杀掉所有 chromedriver 进程,再重试 |
AttributeError: 'WebDriver' object has no attribute 'find_element_by_id' | 使用了 Selenium 4 的旧 API | 改用find_element(By.ID, ...)写法 |
| 无头模式下截图一片空白 | 没设置 window-size 或 GPU 相关参数 | 加--window-size,并加--disable-gpu |
5.2 我在实战中踩过的坑
挑几个印象最深的讲。第一个是登录态过期导致的后半段脚本全挂。之前跑一个数据采集脚本,前半段一切正常,跑到中间突然开始大量报元素找不到。我截图看了半天才发现是登录态过期,页面跳到了登录页。从那以后,我在所有长链路脚本里都会在关键节点做一次“当前页面是否正常”的断言,不等到最后才发现崩了。
第二个是关于click()无效却又不报错的情况。有一个按钮,无论怎么 click 都没有反应,也不报错。排查了很久,发现是页面上浮着一个透明的广告层,正好覆盖了按钮。Selenium 的 click 会直接在那个坐标点点击,结果点到的是透明层。解决方案是用ActionChains移动鼠标到元素再点击:
ActionChains(driver).move_to_element(button).click().perform()或者干脆用 JS 直接触发点击:
driver.execute_script("arguments[0].click();", button)这两个方法的核心区别是:ActionChains走的是真实用户的操作路径,会触发元素上的事件监听;JS 点击是绕过可见性检查直接触发事件,适合某些被遮挡或者readonly属性的元素。但注意execute_script的方式可能不会触发一些浏览器原生的行为,能不用尽量不用。
第三个坑是input 框值没有清空导致文本拼接。在输入框里重新输入内容时,原值还在,send_keys会直接把新内容追加在旧内容后面。所以在输入前一定要element.clear()。Selenium 的 clear 在原生输入框上没问题,但如果碰到 React 组件那种自定义输入框,clear 后还要再填一次空字符串触发事件才能确保数据真正清空。
第四个是下拉框选项点击变成了文字复制。这听起来很玄学,但确实发生过。原因是一个自定义下拉框在点击时触发了浏览器的文本选中行为,把文字拖拽复制了。解决办法是在点击前先element.click()一次让选项聚焦,再ActionChains的方式点击,或者直接发送 Enter 键选中。
第五个是关于等待时间设置过短导致偶发失败。新手容易把 WebDriverWait 的超时时间设成 3 秒或 5 秒,内网环境慢的时候根本不够用。我的建议是:宁可设 15 秒,让它大部分时间 1 秒内就返回,也不要在超时代码里反复重试。等待本身不会浪费太多时间,因为它是在条件满足后立即返回的,超时时间只是兜底的极限值。
!最后再送一个我自己一直在用的小技巧:写自动化脚本时,尽量在每一步关键操作后把当前的关键信息打印出来,比如成功点到了什么、当前页面标题是什么、当前 URL 是什么。跑批量的任务时,这些日志能带你精准定位到失败的那一步,比事后翻截图高效得多。长期维护脚本,日志就是你的第二双眼睛。
我在实际使用中还有一个习惯容易被人忽略——尽量让脚本在真实环境跑通一遍之后,再考虑加无头模式。无头模式只是少了可视化窗口,网络加载行为、元素渲染时序和真实浏览器仍有细微差别。先把业务逻辑跑稳,最后再切到无头模式做回归,这样会省下大量排查时间。答应我,不要第一次写完脚本就直接挂在无人值守的环境里,至少留一次人工盯跑的机会。