简介:这份整合包面向使用 Python Selenium 做浏览器自动化的开发者,内置 Chrome 105.0.5195.102 稳定版及对应 chromedriver.exe、chrome_proxy.exe,重点解决浏览器与驱动版本不匹配、项目打包后路径失效等问题。压缩包共 103 个文件,约 152.05MB,主要由 58 个 pak 资源文件、12 个 dll 动态库、8 个 exe 可执行文件及若干 png、json 配置组成,完整保留浏览器运行所需组件。已有 732 人学习下载,可直接将解压目录放入项目,通过相对路径指定 chrome.exe 与 chromedriver.exe,便于开发调试和交付打包,减少环境配置与驱动查找时间,适合需要固定版本、快速搭建自动化环境的初中级开发者参考使用。
1. 版本对不上的那半小时:我更愿意用整合包起一套 Selenium 环境
把一个在本机跑得好好的 python 自动化脚本拷到另一台机器,第一次执行就抛SessionNotCreatedException,提示驱动只支持某个旧的主版本,而本机 chrome 早就自己更新过了。这种场面在 selenium 项目里出现的频率,比代码本身写错高得多。chrome 的后台更新是静默的,chromedriver 不会跟着动,两者主版本号一旦错开,浏览器连启动的机会都没有,报错信息还往往指向别处,白白耗掉半小时。
整合包真正值钱的地方不是省一次下载,而是把"浏览器"和"驱动"这两样必须咬合的东西锁在同一个版本上,装的时候对一次,后面几个月都不用再想它。适合三类人:刚接触 web 自动化、还在跟环境较劲的新手;要给离线内网机器部署脚本的运维;以及手上同时挂着好几套采集任务、最怕半夜浏览器偷偷升级的老手。
2. 环境清单与版本咬合:Chrome 主版本号和驱动大版本怎么对
动手之前先把清单列清楚,比装到一半再回头查版本要省事得多。这套环境的依赖关系其实只有一条主线:chrome 的主版本号决定 chromedriver 的主版本号,chromedriver 的版本决定 selenium 能不能正常握手,python 版本决定 selenium 能不能装得上。这条链上任何一环错位,最后表现出来的都是同一句含糊的报错。
所以这一章不急着敲命令,先把"装哪个版本、为什么是这个版本"讲透,后面几章才有落点。
2.1 先把 Python 和 selenium 的基线定下来
我一般不会在系统自带的 python 上直接pip install。一是容易把系统工具链搞乱,二是同一台机器上经常要跑两套不同 selenium 版本的脚本,混在一起排查起来非常难受。虚拟环境是成本最低的隔离手段,建一次十几秒,后面省下的是几个小时。
| 组件 | 建议版本 | 选择理由 |
|---|---|---|
| Python | 3.9 ~ 3.12 | 3.12 需要 selenium 4.15 以上才稳,低于这个版本会遇到兼容问题 |
| selenium | 4.15 及以上 | 4.6 起内置 Selenium Manager,4.15 之后对 Chrome 新版本适配更完整 |
| Chrome | 与整合包内驱动同主版本 | 装完立即关闭自动更新,避免半夜跨版本 |
| chromedriver | 与 Chrome 主版本号一致 | 后三段版本号越接近越稳,第一段必须相同 |
# 建一个专用虚拟环境,目录名带项目含义,方便以后一眼认出来 python -m venv venv-selenium source venv-selenium/bin/activate # Windows 换成 venv-selenium\Scripts\activate # 顺手把 pip 升一下,老版本 pip 解析依赖时容易挑到过时的 wheel python -m pip install --upgrade pip # 装 selenium,这里给一个下限而不是钉死小版本 python -m pip install "selenium>=4.15" # 立刻验证装没装上、装的是哪个版本 python -c "import selenium; print(selenium.__version__)"最后那行print是必做的,不是为了看热闹。很多人以为装完了,结果activate没生效,包装进了系统 python,脚本一跑还是报No module named selenium。打印出来的版本号能一次性排掉这个怀疑。
这里要专门说一下 Selenium Manager。selenium 4.6 之后自带了这个组件,理论上你只要写webdriver.Chrome(),它会自己去探测浏览器版本、下载对应驱动、缓存到~/.cache/selenium(Windows 在%USERPROFILE%\.cache\selenium)。听着很省事,但它有两个前提:能联网,且能访问官方分发地址。离线内网、公司网络受限、构建机禁外访问的场景下,它会卡在下载那一步然后抛一个跟版本无关的超时错误,非常误导人。这也是整合包一直有市场的根本原因——把驱动提前放在本地,让 Selenium Manager 无事可做。
如果确实需要在有网环境里关掉自动下载、强制走本地驱动,可以设置SE_OFFLINE=true和SE_CACHE_PATH这两个环境变量,让 Selenium Manager 只查本地缓存。常见做法是在 CI 的构建脚本里统一注入,避免每台机器行为不一致。
2.2 读出本机 Chrome 的主版本号
版本号必须从浏览器自己嘴里问出来,不能靠"我记得装的是 120 几"。三个平台各有各的读法,写进部署检查脚本里都只要一行。
# macOS:直接调 Chrome 二进制打印版本 /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version # Linux:包管理器装的通常是这个名字,个别发行版是 chromium-browser google-chrome --version # Windows cmd:从注册表读,比看安装目录文件名靠谱 reg query "HKCU\Software\Google\Chrome\BLBeacon" /v version除此之外还有一个更直观的办法:打开浏览器访问chrome://version,第一行就是完整版本号。差别在于命令行能写进脚本自动比对,chrome://version只能人工看,适合临时确认。
如果你的部署脚本本身是 python 写的,可以用下面这段在 Windows 上直接读注册表,不启动浏览器也能拿到版本,方便做"版本是否匹配"的前置断言:
# Windows 环境下读取 Chrome 版本的检查片段,适合放进部署前的自检脚本 import winreg # BLBeacon 这个键记录的就是当前安装的 Chrome 版本,比解析安装目录名稳定 key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Google\Chrome\BLBeacon") version, _ = winreg.QueryValueEx(key, "version") print(f"本机 Chrome 版本: {version}") # 形如 121.0.6167.185 print(f"需要匹配的主版本: {version.split('.')[0]}")version.split('.')[0]拿到的就是主版本号,后面挑驱动全看这个数字。注意这个键在便携版或者绿色版 Chrome 上可能不存在,那种情况下只能走--version命令行。
版本对应规则在 Chrome 115 是个分水岭,这里必须说清楚,否则很容易照着老教程操作半天对不上。115 之前,chromedriver 和 Chrome 的版本号是完全一致的,四段数字一模一样,差一位都可能出问题。115 之后,官方统一走 Chrome for Testing 渠道发布,驱动版本跟浏览器版本同步发布,但实际使用时只要主版本号一致就能工作,后三段不必强求完全相同。所以你会看到 121.0.6167.85 的浏览器配 121.0.6167.184 的驱动照样能跑,这不是运气,是规则变了。
提示:判断标准就一条——第一段数字相同即可,后三段尽量接近。如果第一段不同,别试了,一定报错。
想找某个具体版本对应的驱动,官方提供的 known-good-versions 版本清单接口可以按主版本号筛出可用的组合,比在下载页里一个个翻快得多。整合包的价值就在于这一步别人已经替你做完了。
2.3 整合包解开之后应该长什么样
拿到手的是一个压缩包,解压之后目录结构应该能一眼看懂哪个文件给哪个平台用。我一般按下面这个结构整理,后面无论是自己用还是交给同事,都不用再解释:
chrome-chromedriver-bundle/ ├── chrome/ # 浏览器安装包或便携版目录 ├── chromedriver/ │ ├── win64/chromedriver.exe # Windows 64 位 │ ├── mac-arm64/chromedriver # Apple Silicon │ ├── mac-x64/chromedriver # Intel Mac │ └── linux64/chromedriver # Linux 64 位 └── README.txt # 版本号、适用平台、校验值对应关系看这张表更直接:
| 文件 | 目标平台 | 使用方式 |
|---|---|---|
win64/chromedriver.exe | Windows 10/11 64 位 | 复制到工具目录并加入 PATH |
mac-arm64/chromedriver | M 系列芯片的 macOS | 放到/usr/local/bin并加执行权限 |
mac-x64/chromedriver | Intel 芯片的 macOS | 同上,注意别拿错架构 |
linux64/chromedriver | 主流 Linux 发行版 | 放到/usr/local/bin或/usr/bin |
拿到文件先别急着配环境,第一步永远是验证这个二进制本身能不能跑:
# 能打印出版本号,说明文件完整、有执行权限、架构也没拿错 chromedriver --version # 期望输出类似:ChromeDriver 121.0.6167.184 (xxx-refs/branch-heads/...)如果这行命令报Permission denied,是权限问题;报cannot execute binary file,多半是架构拿错了,比如在 M 系列 Mac 上放了 x64 版本,或者在 32 位系统上放了 64 位驱动。这两种情况的处理方式完全不同,先分清再动手。
3. 安装落位:把 chromedriver 放进系统能找到的地方
版本对上了,接下来是"放哪儿"和"怎么让它被找到"。这两件事看起来简单,实际是新手翻车最集中的地方,因为报错信息通常不会告诉你"我没找到驱动",而是抛一堆看着像代码问题的异常。
这一章给三种方案,从"改系统环境变量"到"代码里钉死路径",按你的实际场景选一种就行,不要三种混着上,混着上就是后面排查地狱的起点。
3.1 解压与落位:三个平台的目录选择
选目录就两条原则:路径里不要有中文和空格,不要放在需要管理员权限才能写的目录里。C:\Program Files\看着正规,但驱动运行时可能被权限策略拦下,出问题很难定位;C:\Users\某某\桌面\新建文件夹\这种带中文和空格的路径,则是另一类经典事故源。
Windows 上我固定用C:\tools\chromedriver\,路径短、无空格、普通用户可写。macOS 和 Linux 统一放/usr/local/bin/,这个目录本来就在默认 PATH 里,省一步配置。
# macOS / Linux:复制到标准位置,并补上执行权限 sudo cp chromedriver /usr/local/bin/chromedriver sudo chmod +x /usr/local/bin/chromedriver # macOS 从网络下载的二进制会带隔离属性,不去掉会提示"无法验证开发者" sudo xattr -d com.apple.quarantine /usr/local/bin/chromedriver 2>/dev/null || truechmod +x这步在 Linux 上尤其不能漏,很多压缩包解开之后权限位是丢的,文件明明在,就是不给执行。xattr那一行只对 macOS 有意义,结尾的|| true是为了在属性不存在时不中断脚本,让整套部署命令能一路跑下去。
Windows 上直接用资源管理器复制就行,命令行版本是这样:
# 建目录并复制驱动,注意目标文件名保持 chromedriver.exe,不用改名 New-Item -ItemType Directory -Force -Path "C:\tools\chromedriver" | Out-Null Copy-Item ".\chromedriver\win64\chromedriver.exe" -Destination "C:\tools\chromedriver\chromedriver.exe"New-Item带-Force是为了目录已存在时不报错,脚本可以重复执行。这一点在批量部署时很重要,幂等的安装脚本才不会第二次跑就挂。
3.2 环境变量配置:用户级还是系统级
PATH 我优先改用户级,不动系统级。系统级 PATH 是所有账户共享的,一旦写错分号或塞进一个不存在的目录,影响的是整台机器上所有依赖 PATH 的程序,包括一些你根本想不到的系统工具。用户级出错最多影响自己,删除也方便。
Windows 用 PowerShell 改用户级 PATH:
# 先读出当前用户级 PATH,再追加,千万不要直接覆盖写死 $oldPath = [Environment]::GetEnvironmentVariable("Path", "User") $newPath = $oldPath + ";C:\tools\chromedriver" # 第三个参数 "User" 是关键,写成 "Machine" 就变成改系统级了 [Environment]::SetEnvironmentVariable("Path", $newPath, "User")改完之后有一个必须知道的细节:已经打开的终端、编辑器、IDE 都不会自动刷新环境变量,它们继承的是进程启动那一刻的快照。所以配完 PATH 之后 PyCharm、VS Code 全都要完全退出再重开,只关窗口不算。这一条不知道的话,会陷入"命令行里明明能跑,代码里就是找不到"的死循环。
macOS 和 Linux 就是往 shell 配置文件里加一行。现在的 macOS 默认是 zsh,写~/.zshrc;Linux 桌面和服务器大多是 bash,写~/.bashrc;如果脚本是用 systemd 或者 cron 跑的,那两个文件都不会被读取,得在服务配置里单独声明环境变量。
# 追加到当前 shell 的配置文件,注意是追加 >> 不是覆盖 > echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.zshrc # 让当前会话立即生效,不重开终端 source ~/.zshrc放在$PATH前面是为了让自定义目录优先命中,避免系统里如果已经有个旧版本驱动时抢在前面。这个顺序问题在第 5 章会展开,它是最隐蔽的一类坑。
3.3 不改 PATH 的写法:在代码里钉死驱动路径
有些场景不适合改系统环境变量:临时帮别人调试、在容器里跑一次性任务、同一台机器要测试多个驱动版本。这时候在代码里显式指定路径更干净。
from selenium import webdriver from selenium.webdriver.chrome.service import Service # Selenium 4 的写法:路径通过 Service 对象传入 # 不要再写 webdriver.Chrome(executable_path=...),这个参数在 4.x 已废弃 service = Service(executable_path=r"C:\tools\chromedriver\chromedriver.exe") driver = webdriver.Chrome(service=service) driver.get("https://example.com") print(driver.title) driver.quit()这个 API 变化值得单独提醒。Selenium 3 时代webdriver.Chrome(executable_path=...)是标准写法,网上大量教程还是这个版本;升到 4.x 之后,路径改由Service对象承载,旧写法虽然部分版本还能跑,但会抛弃用警告,再往后就直接报TypeError。看到Service写法不用觉得陌生,那才是当前版本的正确姿势。
路径字符串前面的r也别省,Windows 路径里的反斜杠在普通字符串里是转义符,\t、\n会被解释成制表符和换行,路径就莫名其妙变了样。用正斜杠C:/tools/chromedriver/chromedriver.exe也能跑,效果一样。
3.4 验证 PATH 是否真的生效
配置完不验证,等于没配。三条命令按顺序执行,每一条都有明确的判断标准:
# 1. 看系统能不能定位到驱动文件 which chromedriver # macOS / Linux where chromedriver # Windows # 2. 看定位到的那个文件能不能执行、是不是你要的版本 chromedriver --version # 3. 看 Python 启动的子进程继承到的 PATH 对不对 python -c "import os; print(os.environ.get('PATH'))"第一条能看到路径,说明 PATH 生效;第二条打印的版本号和你放进目录的文件一致,说明没被旧文件抢走;第三条的输出里能搜到你配置的那个目录,说明 Python 进程的环境是对的。三步都过,驱动这一层就算彻底通了。
注意:三条里任何一条不符合预期,先别急着改代码,问题一定在环境这一层。代码是无辜的。
4. 启动配置:ChromeOptions 与 Service 里值得改的那几个参数
驱动放对了位置,只是让它"能被找到"。真正决定脚本稳不稳的,是启动时传进去的那些参数。同一份代码,参数配得好能在容器里跑一整夜,配得不好十分钟崩一次,而且每次崩的原因看着都不一样。
这一章挑的是实际项目里改动频率最高的几项,不是参数大全。没列出来的项,大概率你也不需要。
4.1 Service 对象:路径、日志和超时
Service除了传路径,还有两个我基本每次都会开的选项:日志和详细输出。
import os from selenium import webdriver from selenium.webdriver.chrome.service import Service service = Service( # 路径从环境变量读,代码不写死,本机和容器用同一份脚本 executable_path=os.environ.get("CHROMEDRIVER_PATH", "/usr/local/bin/chromedriver"), # 驱动自己的日志写进文件,出问题时这是唯一能看到底层原因的地方 log_file="chromedriver.log", # 打开详细日志,首次部署阶段建议开着,稳定运行后可以关掉减少 IO service_args=["--verbose"], ) driver = webdriver.Chrome(service=service) # 页面加载上限,比在代码里到处 sleep 靠谱得多 driver.set_page_load_timeout(30) # 元素查找的兜底等待,只对 find_element 生效,不要和显式等待混用 driver.implicitly_wait(10)log_file是这套环境里的黑匣子。Selenium 抛出来的异常信息普遍很浅,session not created、unknown error这类文案根本看不出根因,但驱动日志里会写清楚它找的是哪个浏览器二进制、握手失败在哪一步、Chrome 进程的退出码是多少。第一次部署时把--verbose打开,稳定之后再关,这个习惯能省下大量瞎猜的时间。
set_page_load_timeout和implicitly_wait是两回事,经常被混为一谈。前者管的是"页面加载多久算超时",后者管的是"找一个元素最多等多久"。两个都设上,脚本才不会被一个卡住的页面拖死。
4.2 ChromeOptions:值得改的开关和不该乱加的开关
参数不是越多越好,每加一个都要知道它解决什么问题。下面这几个是我在绝大多数场景下都会保留的:
| 参数 | 作用 | 什么时候必须加 |
|---|---|---|
--window-size=1920,1080 | 固定窗口尺寸 | 需要截图或依赖布局的页面 |
--user-data-dir=<独立目录> | 使用独立配置目录 | 本机开着 Chrome 时要并行跑脚本 |
--lang=zh-CN | 指定界面语言 | 页面会根据语言返回不同内容 |
--disable-gpu | 关闭 GPU 加速 | 无显卡的服务器、容器环境 |
--no-sandbox | 关闭沙箱 | root 用户运行、容器内,本机不要开 |
--disable-dev-shm-usage | 改用临时目录 | Linux 容器,/dev/shm偏小时 |
写成代码是这样:
from selenium.webdriver.chrome.options import Options options = Options() options.add_argument("--window-size=1920,1080") options.add_argument("--lang=zh-CN") # 独立配置目录,避免和本机正在运行的 Chrome 抢同一个 profile 锁 options.add_argument("--user-data-dir=/tmp/chrome-profile-auto") # 去掉"Chrome 正受到自动测试软件的控制"提示条,纯观感,不影响功能 options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False)--user-data-dir这个参数单独说一下。不指定的话,驱动会去用默认的用户配置目录,而你本机开着 Chrome 的时候,那个目录是被锁住的,结果就是启动直接失败,报一个跟"用户数据目录不可用"相关的错误。给它指一个临时目录,问题当场消失,代价是每次都是全新的浏览器状态、没有登录态。需要复用登录态的场景,就得反过来指定一个专用目录并提前登录一次,这是另一个话题了。
--no-sandbox属于典型的"能不用就不用"。它确实能解决容器里的一批启动失败,但代价是关掉了浏览器的进程隔离。本机开发环境完全没必要开,只有 root 用户或者受限容器里才作为兜底手段加上。
4.3 无头模式与容器环境
无头模式是自动化脚本跑在服务器上的标配,但写法在 Chrome 109 之后变过一次,老写法虽然还能用,行为已经有差异。
options = Options() # Chrome 109 之后用 new,老的 --headless 走的是另一套实现,行为不一致 options.add_argument("--headless=new") options.add_argument("--window-size=1920,1080") # 无头模式下 UA 里会带 HeadlessChrome 字样,部分站点会据此返回不同页面 options.add_argument( "--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36" ) # 容器里通常还需要这两个,具体原因见下一章的排查部分 options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage")UA 这一项很容易被忽略,直到某天发现同样的代码在无头模式下抓到的内容和有头模式完全不一样。原因就是站点做了 UA 判断,看到HeadlessChrome就返回了精简版页面。加上显式 UA 之后两边一致,问题不再复现。
容器环境还有一条:镜像里必须装齐 Chrome 运行需要的系统库,只把二进制拷进去是不够的。缺库的表现是 Chrome 进程起来就退出,驱动报Chrome failed to start,日志里能看到缺少.so文件的提示。常见做法是直接用官方提供的带完整依赖的基础镜像,或者按照发行版把libnss3、libatk、libgbm这一组依赖一次装全。
5. 避坑排查:五个高频翻车现场的现象、原因和解决
前面几章是按"顺利路径"写的,实际部署很少一路顺风。这一章把踩过的坑按现象归类,每条都给出原因和具体动作,遇到问题时可以直接对号入座,不用从头再推一遍。
顺序大致按出现频率排,前面三条覆盖了绝大部分情况。
5.1 session not created:版本号没咬合
现象:脚本一跑就抛SessionNotCreatedException,消息里写着This version of ChromeDriver only supports Chrome version 120,而当前会话的版本是 121。
原因:Chrome 在后台自动更新跨了主版本,驱动还停在旧版。还有一种变体是驱动其实已经换了新的,但 PATH 里排在前面的旧目录先被命中,代码实际调用的是老文件。
解决:先开chrome://version确认当前主版本,再核对驱动版本。两者第一段必须相同。确认无误后,把 Chrome 的自动更新关掉——Windows 上可以通过计划任务和更新服务两个入口处理,macOS 上关掉自动检查更新,Linux 上把包管理器里的 chrome 包标记为保持当前版本。关更新这一步很多人嫌麻烦跳过,结果是每隔几周就要重新排查一次同样的问题。
5.2 驱动被杀软或权限拦下:进程起不来
现象:Windows 上抛PermissionError: [WinError 5] Access is denied;macOS 上提示cannot be opened because the developer cannot be verified;Linux 上直接Permission denied。
原因:三种表现,三种成因。Windows 多是安全软件把新出现的可执行文件当可疑程序拦了,或者驱动放在了需要提权的目录里;macOS 是下载文件自带的隔离属性没去掉;Linux 纯粹是权限位丢了。
解决:Linux 上chmod +x;macOS 上执行xattr -d com.apple.quarantine;Windows 上把驱动目录加进安全软件的白名单,同时确认目录不在Program Files这类受保护路径下。三 platform 的处理方式不一样,先看报错文案属于哪一类再动手,别一上来就重装。
5.3 PATH 里有两个 chromedriver:改了没生效
现象:终端里执行chromedriver --version显示的是新版本,但 python 脚本跑起来还是报旧版本的错。
原因:两个可能。一是 IDE 或编辑器在改 PATH 之前就启动了,进程环境还是旧的快照;二是系统里真的存在多个驱动文件,PATH 顺序决定了命中哪一个,而脚本启动的环境可能和终端不完全一致。
解决:先彻底退出 IDE 再重开,包括托盘里的后台进程。还不行就用which -a chromedriver(macOS/Linux)或where chromedriver(Windows)把全部命中路径列出来,把多余的旧文件删掉或改名。最省事的终极方案是在代码里用Service(executable_path=...)写死路径,绕开 PATH 查找,从此不再有歧义。
5.4 中文路径与空格:解压到桌面就报错
现象:WebDriverException: unknown error: cannot find Chrome binary,或者驱动启动后立刻退出,日志里没有任何有用信息。
原因:路径里带中文或空格。Windows 的C:\Users\张三\桌面\这种路径,在参数传递过程中可能被截断或转义错误;另外便携版 Chrome 和驱动不在同一个目录、又没有显式指定浏览器位置时,也会报找不到二进制。
解决:把驱动和浏览器都挪到纯英文、无空格的短路径下,比如C:\tools\。如果浏览器必须放在别处,用options.binary_location = r"C:\path\to\chrome.exe"显式告诉驱动去哪里找。这一条没有技术含量,但确实是最容易在"看起来没问题"的地方翻车。
5.5 无头模式容器里崩溃:/dev/shm 太小
现象:容器里跑脚本,报unknown error: DevToolsActivePort file doesn't exist,或者 Chrome 进程无声无息地消失。
原因:容器默认的/dev/shm只有 64MB,Chrome 渲染页面时需要的共享内存远超这个数,直接被系统杀掉。用 root 用户跑也是同类问题的常见诱因。
解决:加--disable-dev-shm-usage让 Chrome 改用/tmp,加--no-sandbox处理 root 用户场景,再加--headless=new保证无头模式走的是新实现。如果是在 docker 里,另一个更彻底的办法是启动容器时给--shm-size=1g,把共享内存调大,比在代码里加参数更干净。
提示:
DevToolsActivePort file doesn't exist这个报错覆盖的场景很广,权限、共享内存、用户数据目录锁、Chrome 崩溃都会报它。排查顺序建议是:先看驱动 verbose 日志,再确认 Chrome 能不能手动起来,最后才怀疑参数。
6. 装完怎么验证:三段式自检把驱动跑通
装完就交付是给自己埋雷。我固定用一套三段式自检,从底层往上层走,每段有明确的通过标准,任何一段不过就停在那里,不往上试。
第一段,驱动自身可用。chromedriver --version能打印出版本号,且主版本号和浏览器一致。这一步排掉文件损坏、架构拿错、权限缺失。
第二段,Selenium 能找到驱动并起浏览器。这段最容易被环境变量问题挡住,所以用一个不依赖外网的脚本,只起浏览器拿标题,跑通了说明 PATH 或显式路径这条路是通的。
# 第二段自检:只验证驱动与浏览器的握手,不访问任何外部站点 from selenium import webdriver from selenium.webdriver.chrome.service import Service service = Service( executable_path="/usr/local/bin/chromedriver", # Windows 换成对应路径 log_file="selftest.log", service_args=["--verbose"], ) options = webdriver.ChromeOptions() options.add_argument("--headless=new") # 服务器上没有显示器,必须无头 options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") driver = webdriver.Chrome(service=service, options=options) print("浏览器版本:", driver.capabilities["browserVersion"]) print("驱动版本:", driver.capabilities["chrome"]["chromedriverVersion"].split(" ")[0]) driver.quit()这段代码之所以不访问任何网址,是为了把"网络问题"和"驱动问题"彻底分开。如果访问外网失败,你没法判断是驱动挂了还是站点不通。driver.capabilities里拿到的两个版本号可以直接对照,是不是同一主版本一眼就看出来了,比翻日志快。
第三段,页面真的能操作。用data:开头的内联 HTML,完全不依赖网络,把"取元素"这个动作跑一遍:
from selenium.webdriver.common.by import By # data: URL 不走网络,纯本地渲染,用来验证元素定位链路是否正常 driver.get("data:text/html,<h1 id='t'>driver ok</h1>") text = driver.find_element(By.ID, "t").text print("页面取到的内容:", text) assert text == "driver ok", "元素定位失败,检查页面渲染或等待策略" driver.quit()assert这行别省。没有断言的脚本跑完只是不报错,不代表结果是对的,尤其在 CI 里,静默失败比直接报错更难查。
三段都过之后,我会再补一张对照表贴在部署文档里,方便下次直接对:
| 自检项 | 命令或动作 | 期望结果 | 不通过先看哪里 |
|---|---|---|---|
| 驱动可执行 | chromedriver --version | 打印版本号,主版本与浏览器一致 | 权限、架构、路径 |
| 浏览器可启动 | 第二段脚本 | 打印两个版本号且主版本相同 | PATH、binary_location |
| 元素可定位 | 第三段脚本 | 输出driver ok | 渲染等待、定位表达式 |
| 进程干净退出 | 观察是否有残留 Chrome 进程 | 无残留 | 是否漏了driver.quit() |
最后一行经常被忽略。脚本异常退出时如果没有driver.quit(),Chrome 进程会留在后台,跑几次之后机器上堆着几十个僵尸进程,内存被吃光,下一次启动失败的原因看起来跟驱动毫无关系。用try/finally包住,或者在测试框架的 teardown 里统一清理,比事后kill省心得多。
从那以后我每次交付这套环境,不管是本机还是容器,都强制把这三段自检走一遍,哪怕这次只是换了个驱动小版本。看起来多花三分钟,实际省下的是半夜被叫起来排查"昨天还好好的"的时间。希望帮到你。
本文还有配套的精品资源,点击获取