ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Selenium 3.141.0 环境搭建与实战避坑指南

Selenium 3.141.0 环境搭建与实战避坑指南

简介:这是selenium 3.141.0的完整离线安装包,面向经常因网络不稳定导致在线安装中断、从Python官网获取源码又异常缓慢的Python开发者。资源采用zip压缩格式,解压后在任意目录打开命令行并切换至对应位置,执行python setup.py install即可完成本地部署,省去反复重试的麻烦。压缩包共104个文件,大小约905KB,其中86个py源码文件构成Selenium库核心,另有js、so、txt、pkg-info、cfg、rst等文件分别承担WebDriver辅助脚本、动态库、文档说明、安装元数据与配置指南等作用,目录结构清晰,便于按需查看。目前已有2130人学习下载,适合需要快速搭建自动化测试环境、批量操作网页或研究Selenium源码实现原理的初、中级Python开发者。资源包保留了完整的变更记录、许可文件及包描述,安装异常时可对照文件清单排查缺失项,也能帮助读者理解各模块之间的依赖关系。

1. 拿到 selenium-3.141.0 先别急着装:这个版本卡住了多少人的环境

如果你接手过三年前的项目,大概率见过requirements.txt里锁着selenium==3.141.0,或者在某个课程作业的压缩包里翻到这个 zip。别急着卸载换新版,Selenium 3.141.0 是 3.x 系列的最后一个正式版本,之后项目组直接跳到了 4.x,API 层面动了刀子。现在网上大量存量脚本、企业内网系统、老教材都跑在 3.141.0 上,你要是换成 4.x,find_element_by_id这类经典写法直接报错,黑匣子一开全是红。这篇笔记就把 3.141.0 的环境搭建、核心 API、定位策略和踩坑记录拆开讲清楚,适合三类人:接手老代码的维护者、照着旧教材做自动化测试的学生、以及暂时不想折腾新 API 只想让脚本跑起来的开发者。

2. 环境搭建:从 Python 版本到浏览器驱动的全套匹配方案

2.1 先确认 Python 版本,别让解释器拖后腿

Selenium 3.141.0 发布于 2018 年底,它的底层网络库和线程模型是基于当时的 Python 运行时写的。我见过有人在 Python 3.10 上硬装,结果import selenium没问题,一跑webdriver.Chrome()就抛异常,查半天发现是urllib3的兼容性翻车。最稳的组合是 Python 3.6 到 3.8,这个区间内 selenium 的依赖解析不会出幺蛾子。如果你机器上只有 3.9+,也别急着退版本,用虚拟环境隔离一下:

python -m venv selenium_venv source selenium_venv/bin/activate # Windows 下用 selenium_venv\Scripts\activate pip install selenium==3.141.0 python -c "import selenium; print(selenium.__version__)"

这里解释一下,虚拟环境不是为了赶时髦,而是防止你系统里其他项目把urllib3或certifi升级到不兼容版本。pip install selenium==3.141.0会拉取当时锁定的依赖版本,如果pip抱怨依赖冲突,常见做法是加--no-deps参数跳过依赖检查,但前提是你确认自己的环境里已有兼容的urllib3。打印版本号那步是必须的,很多人装完就信了,真跑起来才发现site-packages里是旧残留。

2.2 浏览器驱动:缺了它,脚本连浏览器都拉不起来

Selenium 3.x 的架构决定了它不能直接操作浏览器,必须通过浏览器厂商提供的驱动(Driver)来通信。很多初次接触的人以为装了 selenium 库就完事了,结果一执行webdriver.Chrome()就报WebDriverException: Message: 'chromedriver' executable needs to be in PATH——这是 3.x 时代最经典的翻车现场,没有之一。

不同浏览器对应不同驱动,它们的版本还必须和浏览器主版本号对齐。Chrome 用的是 chromedriver,Firefox 用 geckodriver,Edge 用 msedgedriver。下载驱动时记住一个原则:驱动的大版本号必须和浏览器一致,小版本可以略过。比如 Chrome 120 就找 chromedriver 120.x,找不到 120 就找最接近的 119 或 121,但别跨大版本。

浏览器驱动名称匹配规则
Chromechromedriver大版本号一致
Firefoxgeckodriver0.30+ 对应较新版本,通常兼容
Edgemsedgedriver与 Edge 版本完全一致

驱动下载后,Windows 上解压到任意目录,然后把目录加进PATH环境变量;macOS/Linux 下推荐放到/usr/local/bin并加执行权限。还有种更省事的做法,直接丢到 Python 解释器同目录下,selenium 会在启动时查找当前目录,这个技巧在实验室公用电脑上特别管用,不用动全局环境变量。

2.3 第一个能跑的脚本:从启动到退出的完整闭环

写第一个脚本时别贪多,就做三件事:打开一个页面、打印标题、退出。跑通了再往里面加东西。

from selenium import webdriver driver = webdriver.Chrome() # 启动 Chrome,此时会弹出浏览器窗口 driver.get("https://www.example.com") # 阻塞直到页面 load 事件完成 print(driver.title) # 拿到页面标题,验证页面真的打开了 driver.quit() # 关闭浏览器并清理进程

这段代码背后有三个值得注意的地方:get()方法不是等页面元素全部渲染完才返回,它等的是浏览器的加载状态变成 complete,所以如果页面里有异步请求,后续定位元素时必须配合显式等待,这个细节在 3.1 节展开;driver.quit()和driver.close()完全不同,close()只关当前标签页,quit()才是彻底关闭浏览器进程,脚本结束条件不写quit(),你会看到后台一堆残留的 chrome 进程,慢慢吃内存;driver.title是经典属性写法,在 3.x 版本里它不会自动等待,如果页面标题是动态渲染的,刚打开就取可能是空字符串。

3. 核心 API 实战:定位策略、显式等待与交互控制

3.1 八种定位方式,真正常用的就三种

Selenium 3.141.0 提供了八种元素定位方法,从find_element_by_id到find_element_by_xpath,新手容易陷入背 API 的误区。我拆了几十个跑在 3.141.0 上的老脚本,发现生产环境里超过八成用的是ID、XPATH和CSS_SELECTOR。ID定位最快,但现代前端框架里 ID 往往带随机后缀,一刷新就变;XPATH最灵活,能处理列表项、文本匹配、父子关系,代价是性能稍慢、可读性差;CSS_SELECTOR在速度和可读性之间取了平衡点,前端工程师写样式时留下的class和>from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC driver = webdriver.Chrome() wait = WebDriverWait(driver, timeout=10, poll_frequency=0.5) login_button = wait.until(EC.element_to_be_clickable((By.ID, "login"))) login_button.click()

WebDriverWait的构造参数有三个:driver是当前的浏览器实例;timeout是最大等待时间,单位秒,超过这个时间条件还不满足就抛TimeoutException;poll_frequency是轮询间隔,默认 0.5 秒,如果你要定位的元素出现在一个卡顿的页面里,可以把间隔调大到 1 秒,减少无效的 DOM 查找。element_to_be_clickable是EC模块里的一个条件类,它和presence_of_element_located的区别在于:后者只检查元素是否存在于 DOM 树中,哪怕被遮挡、被禁用也会返回;前者还要求元素可见且可用。实战中登录按钮常遇到这两种状态的差异,新手容易栽在这里。

3.3 交互操作:输入、点击和下拉框的坑

send_keys是模拟键盘输入的核心方法,但它在 3.141.0 上的一个玄学问题是中文输入丢字。尤其在 Linux 环境上,send_keys("用户名")偶尔会只输入一个"用"字,或者干脆漏掉后半截。常见做法是先element.clear()清空输入框,再逐字输入,或者用clipboard方案:把文本复制到剪贴板,然后send_keys(Keys.CONTROL + 'v')粘贴,中文稳定性更高。click()方法看着简单,坑在元素被遮住时会抛出ElementClickInterceptedException,这类问题我会在第四章集中展开。

下拉框处理是个独立的场景,原生<select>标签用 Selenium 自带的Select类最省事,但很多人不知道这个机制,偏要写find_element_by_xpath("//option[@value='2']").click(),结果发现点击选项后触发不了change事件。

from selenium.webdriver.support.ui import Select province_select = Select(driver.find_element_by_id("province")) province_select.select_by_value("110000") # 按 value 属性选 province_select.select_by_visible_text("北京市") # 按显示文本选 province_select.select_by_index(2) # 按可选列表的索引选,从 0 开始

Select类内部会自动处理展开和点击的逻辑,不需要手动去点<option>。三个select_by_*方法的区别很明确:select_by_value匹配的是option标签的value属性,最稳定;select_by_visible_text匹配用户看到的文本,适合没有 value 属性的老系统;select_by_index完全依赖选项顺序,只要前端加了选项位置就会错位,能不用就不用。另外,这个 Select 类只能处理标准的select标签,现在很多 React/Vue 项目用的是自定义下拉组件,DOM 结构根本不是<select>,这时候只能退回去写 XPATH 点击,没有捷径。

4. 避坑指南:五个高复现率的故障场景与解法

4.1 现象:send_keys输入中文掉字,只输入了第一个字

原因:Selenium 3.141.0 的输入事件处理在非拉丁字符集上有兼容问题,尤其在 Linux 的 X11 环境下,键盘事件的发送频率与输入法状态不稳定。解决:放弃直接输入,用剪贴板方案。

import subprocess from selenium.webdriver.common.keys import Keys def set_clipboard(text): process = subprocess.Popen(['xclip', '-selection', 'clipboard'], stdin=subprocess.PIPE) process.communicate(text.encode('utf-8')) set_clipboard("测试账号") input_el = driver.find_element_by_id("username") input_el.click() input_el.send_keys(Keys.CONTROL, 'v')

xclip需要单独安装,Windows 上可以用pyperclip库代替。这个方案的本质是绕过 selenium 的键盘事件通道,直接操作系统剪贴板,再通过快捷键触发粘贴。从那以后我每次处理包含非 ASCII 字符的输入都强制走剪贴板,后续没有复发过。

4.2 现象:click()报WebDriverException: Element is not clickable at point

原因:目标元素被浮层、固定导航栏或加载动画遮挡,selenium 模拟的是真实用户点击,但浏览器的点击命中检测发现那个坐标为的顶层元素不是你的目标。解决:先滚动到元素可见,再用element_to_be_clickable显式等待,最后兜底用 JavaScript 点击。

from selenium.webdriver.common.action_chains import ActionChains ele = driver.find_element_by_xpath("//button[@id='submit']") driver.execute_script("arguments[0].scrollIntoView();", ele) try: ele.click() except WebDriverException: driver.execute_script("arguments[0].click();", ele)

scrollIntoView把元素滚动到视口内,这一步能解决大部分遮挡问题。JavaScript 点击是最后的后悔药,它会绕过可见性检测直接触发点击事件,适合那种元素被opacity: 0隐藏但实际可用的场景。

4.3 现象:SessionNotCreatedException: This version of ChromeDriver only supports Chrome version X

原因:浏览器自动更新了主版本,但 chromedriver 还停留在旧版本。这是本地开发环境最常见的高频事故,Chrome 默认开启自动更新,你睡一觉起来脚本就全挂。解决:锁定浏览器版本,关闭自动更新;如果锁定不了,就写个启动前检查函数,比对driver.version和浏览器版本,不一致时自动报错并提示去下载对应驱动。

from selenium import webdriver driver = webdriver.Chrome() actual_version = driver.capabilities['browserVersion'] driver_version = driver.capabilities['chrome']['chromedriverVersion'].split()[0] print(f"浏览器版本: {actual_version}, 驱动版本: {driver_version}") if actual_version.split('.')[0] != driver_version.split('.')[0]: raise SystemExit("驱动大版本与浏览器不匹配,请重新下载驱动")

把这段检查写在你所有爬虫或测试脚本的入口处,能省下一大堆定位问题的排查时间。

4.4 现象:显式等待一直超时,元素明明已经出现在页面上

原因:最常见的是元素在 iframe 里,selenium 默认只操作顶层文档,find_element找不到 iframe 内部的东西只会干等。解决:先切换到 iframe 再定位,操作完记得切回默认内容。

driver.switch_to.frame("main-frame") # 传入 iframe 的 id 或 name # 此时可以正常 find_element 定位 iframe 内的控件 driver.switch_to.default_content() # 切回顶层文档

4.5 现象:脚本跑到一半浏览器窗口被系统强制关闭

原因:对于超长任务,浏览器进程因内存占用过高被操作系统杀掉。解决:在任务循环里定期重启浏览器,而不是一直复用同一个实例,这是爬虫项目的通用做法。

5. 进阶技巧:无头模式与页面对象模式的双保险

如果你要把这段 3.141.0 的脚本部署到服务器上,没有显示器环境,就必须启用无头模式(headless)。3.141.0 支持--headless参数,但要注意,它和 4.x 的--headless=new参数在行为上有差异——旧版 headless 模式不渲染部分 CSS 和 JS,截图结果和新版不一样。如果只是想跑数据交互,差别不大;但如果你依赖截图做页面回归对比,建议在本地 4.x 上先跑一遍基准图,再切到服务器用 3.141.0 执行。

from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--headless") options.add_argument("--no-sandbox") options.add_argument("--disable-gpu") options.add_argument("--window-size=1920,1080") driver = webdriver.Chrome(chrome_options=options) driver.get("https://www.example.com") print(driver.title) driver.quit()

四个参数缺一不可:--no-sandbox是因为容器环境里用户权限受限,去掉会直接启动失败;--disable-gpu是为了避免某些 Linux 图形栈的空指针崩溃;--window-size非常关键,headless 模式默认窗口是 800x600,很多响应式页面的元素在窄屏下会被隐藏,定位不到。chrome_options这个参数名在 3.141.0 里已经废弃,官方推荐用options,但老代码里两种写法都能跑,不用急着改。

页面对象模式(Page Object Pattern)是让脚本从"能用"走到"好维护"的关键一步。思路不复杂:把一个页面的所有定位器和操作方法封装成一个类,测试用例只关心业务流程,不关心 XPath 细节。考虑到 3.141.0 的老项目多半是快速搭的,直接上框架成本高,我建议只做一步小的重构——把复用超过两次的定位逻辑抽成函数,比如把wait.until(EC.element_to_be_clickable(...))封装成def click_btn(driver, locator)。这样即便后面前端改版,你只需改一处就行,而不是整个脚本里搜索。

最后说个真实教训:某次凌晨三点跑批量任务,脚本在第二个循环就崩了,查日志发现是网络抖动导致页面加载了一半,显式等待超时。那以后我所有循环任务都在开头加了一个健康检查——先driver.execute_script("return document.readyState")确认页面真的 ready,再往下走。希望这篇围绕 3.141.0 的拆解能帮你少踩几个坑,让老项目在自动化这条路上继续发光发热。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进