上周三凌晨,一个跑了两年的采集脚本突然挂了,控制台里红着一行session not created: This version of ChromeDriver only supports Chrome version 116。发截图过来的朋友一脸茫然:代码一个字没改,昨天还跑得好好的。这种场面我这两年遇到不下十次,原因几乎每次都一样——谷歌浏览器在后台悄悄完成了自动更新,而 chromedriver 还停在旧版本上,两边的主版本号对不上了。chromedriver和谷歌浏览器之间是一夫一妻制的绑定关系,浏览器一升级,驱动立刻作废。这篇就聊聊 2024 年怎么把旧版本的谷歌浏览器安装包和配套的 chromedriver 下载下来、装好、并且让它老老实实待在原地别乱动,顺便把版本对照、离线包选择、自动更新拦截、多开批量启动这些实操细节一次讲透。不管你是做自动化测试、爬虫、还是单纯要给老系统留一套能跑的浏览器环境,下面的内容都能直接抄作业。
1. 版本绑定规则:为什么换个浏览器脚本就废了
1.1 自动更新是这套组合最大的敌人
绝大多数人第一次遇到这个问题,都是在完全不知情的情况下踩的雷。Chrome 默认开启了后台静默更新,你在前台写代码、跑任务,它在后台把新版本下载好、重启浏览器进程、把旧的安装目录替换掉。整个过程没有任何提示,用户侧感知为零。等到你的 Selenium 脚本下一次调用webdriver.Chrome(),chromedriver 连上新版浏览器的 DevTools 端口,握手阶段发现主版本号不匹配,直接抛出异常退出。
我做过一个粗略统计:一个不加任何限制的自动化环境,平均存活周期大约是 6 到 8 周。Chrome 现在大约每 4 周发一个大版本,两个大版本之内必然崩一次。如果你的脚本是那种「部署完就不管、每天定时跑」的类型,那它崩掉的时候你大概率是在看别的项目,等想起来的时候已经积压了几天的任务。
所以处理这个问题的思路不是「崩了再修」,而是「装完之后立刻把更新通道掐死」。这一点我会在第 4 章展开,先把版本规则本身讲清楚。
1.2 四段式版本号里,chromedriver 只认第一段
Chrome 的版本号长这样:86.0.4240.198,四段分别是主版本号、次版本号、构建号、补丁号。chromedriver 的校验逻辑非常粗暴——它只看主版本号。也就是说86.0.4240.22的驱动,能带86.0.4240.198的浏览器,也能带86.0.4240.111、86.0.4240.75,甚至86.0.4123.5这种构建号对不上的版本。但换成87.x.x.x的浏览器,它就不认了。
这个规则在 115 版本之后发生过一次变化,后面会单独讲,但 114 及以前,这条「主版本对齐」的铁律是必须守住的第一原则。很多人踩坑踩在反方向上:以为必须下载和浏览器完全同号的驱动,然后在归档站里翻半天找不到86.0.4240.198这个精确版本,其实86.0.4240.22一样能用。
1.3 chromedriver=2.37.544315 这串数字到底是什么
热搜里出现过chromedriver=2.37.544315这个字符串,我猜是有人在配置项或者日志里看到它,想知道怎么对上。拆开看:2.37是驱动自身的版本标识,544315是构建号。它对应的浏览器主版本区间是64 到 66。
这是老式 chromedriver 命名法的典型例子。在 2.46 之前,驱动的版本号跟浏览器版本号完全不挂钩,是独立编号的一套体系。2.37 这个号听起来很小,但它管的浏览器版本是 64 到 66 —— 看起来也不新,确实是 2018 年前后的组合了。如果你的环境里还留着这套,多半是在跑某个只在 IE 时代末期页面上才正常的内部系统。
我这里给一张常用的旧版本对照表,基于官方 release notes 整理,具体到 build 号建议下载时再核对一次:
| chromedriver 版本 | 兼容的 Chrome 主版本 | 常见使用场景 |
|---|---|---|
| 2.37.544315 | 64 / 65 / 66 | 极老的内部系统自动化 |
| 2.40.x | 66 / 67 / 68 | 老版本内核的定制浏览器 |
| 2.42.x | 68 / 69 / 70 | 与 2.4x 系列混用 |
| 2.44.x | 69 / 70 / 71 | 过渡版本,兼容区间较宽 |
| 2.46.x | 71 / 72 / 73 | 2.x 体系的最后一个大版本 |
| 74.0.3729.x | 74 | 命名法切换后的第一个版本 |
| 80.0.3987.x | 80 | 老项目里出现频率很高 |
| 86.0.4240.x | 86 | 稳定版留存最多的一个版本 |
| 109.0.5414.x | 109 | Win7 能用的最后一个大版本 |
注意:这张表里的兼容区间来自官方 release notes,标注为「支持 A/B/C」的驱动表示这三个主版本都能带。但如果你手里是某个厂商二次封装的浏览器,主版本号可能被改过,这时候以实际握手结果为准。
1.4 什么情况下值得花时间折腾旧版本
不是所有场景都值得回退。我自己的判断标准是三条:第一,目标页面在旧内核上才正常渲染,换新版本会出现布局错位或者脚本报错;第二,操作系统本身装不了新版,比如 Win7 环境,Chrome 109 是天花板;第三,依赖的某些插件或者本地组件对新版不兼容。这三条命中任意一条,回退才有意义。
纯粹为了「跑个自动化」而去装旧版本,我不建议。旧版本意味着没有新的安全补丁,如果你要访问外网,风险是实打实的。回退应该是被动选择,不是主动优化手段。
2. 旧版 Chrome 安装包的获取渠道与版本辨认
2.1 官方网站只给你最新版,这是设计如此
打开官方下载页,拿到的永远是当前最新版本,不管是在线安装器还是 standalone 离线包。这是产品策略决定的,官方不提供历史版本的公开入口。所以想拿旧版本,只有两条路:走官方的测试版本分发通道,或者从可信的第三方归档站下载并自行校验。
在线安装器(那类只有几百 KB 到 1 MB 多的 exe)千万别用在离线环境,它在安装过程中要联网拉主程序,拉到的还是最新版。要找的是 standalone 离线包,体积在 50 MB 到 90 MB 之间,一个文件就是完整程序。
2.2 Chrome for Testing:113 及以上版本最干净的来源
从 113 版本开始,官方提供了一套专门给自动化场景用的分发体系,叫 Chrome for Testing。它同时提供浏览器本体和配套 chromedriver,版本号一一对应,直接打在同一个目录下,不需要你自己去猜兼容关系。
这套体系有两个 JSON 端点特别有用:
last-known-good-versions-with-downloads.json:拿到各平台当前推荐版本的下载地址known-good-versions-with-downloads.json:拿到所有历史版本的完整列表
地址都在googlechromelabs.github.io/chrome-for-testing/这个站点下。JSON 里的下载链接指向storage.googleapis.com/chrome-for-testing-public/这个存储桶,路径格式是固定的:
https://storage.googleapis.com/chrome-for-testing-public/<版本号>/<平台>/<文件名>平台标识有win32、win64、mac-x64、mac-arm64、linux64几种。文件名对应关系是chrome-win64.zip、chromedriver-win64.zip这种。用脚本把这个 JSON 读下来,筛出你想要的版本,直接拼 URL 下载,比在任何第三方站点里点来点去都可靠。
我一般的做法是写个十几行的 Python 脚本,把 JSON 拉下来,按版本号过滤,把 zip 包下载到本地归档目录,顺手算一遍 SHA256 记在文件名里。这个归档目录会跟着项目一起进版本控制(用 Git LFS 或者干脆放内网文件服务器),后面换机器、重装系统直接从归档拿,不用再去找。
2.3 113 以下只能靠归档站,怎么判断靠不靠谱
113 之前的版本,Chrome for Testing 不覆盖,只能从第三方归档站拿。这类站点不少,质量参差不齐,我踩过几次坑之后总结了几条筛选标准。
第一看文件体积。完整的 Windows 64 位离线安装包,50 MB 以上是底线,如果某个「安装包」只有十几 MB 甚至几 MB,那基本是在线安装器的壳子,装完还是最新版。第二看版本号密度。靠谱的归档站会保留几十个大版本、每个版本多个 build 号,页面上密密麻麻列一长串;只有一个最新版本的「归档站」直接关掉。第三看有没有提供校验值,以及校验值是不是能跟官方发布记录对上。
我给个折中方案:如果你能找到一台已经装过目标版本、或者曾经下载过该版本安装包的机器,直接从它的用户目录或者下载缓存里把安装包捞出来。Chrome 的更新包在本地是留存的,只是路径不显眼。这比在网上赌运气强得多。
2.4 Win7 的天花板是 109,位数别拿错
Win7 用户要特别记住一个数字:109。从 110 版本开始,Chrome 不再支持 Win7、Win8 和 Win8.1。市面上能拿到的「Win7 可用」的最后一个大版本就是 109.0.5414.x。想要更老的,比如 86 或者 80,也可以用,但没必要为了老而老,选 109 就能兼顾兼容性和相对较新的内核。
位数问题是另一个高频翻车点。32 位系统只能装 x86 版本,64 位系统两个都能装,但我建议 64 位系统也评估一下:某些老插件是 32 位写的,装 64 位 Chrome 之后它加载不了。这种情况就得退回 x86。判断方式很简单,在文件资源管理器里右键「计算机」看属性,看系统类型那一行。
还有一个容易混淆的点:Chromium 快照跟 Chrome 是两码事。Chromium 是上游开源项目,Chrome 是在它基础上加了闭源组件(编解码器、更新模块、部分 API)的商业版本。用 Chromium 跑自动化,你会发现某些视频站、某些依赖专有编解码器的页面行为完全不一样,然后开始怀疑自己的代码。别混用,老老实实用 Chrome。
2.5 下载之后先校验,别急着双击
拿到安装包之后,我建议至少做三件事:算一遍文件哈希、确认数字签名、在虚拟机里先装一遍。
哈希这一步不用多说,certutil -hashfile 文件名 SHA256一行命令搞定。数字签名是关键,Windows 上右键安装包看属性,数字签名那一栏如果是「无」或者签名者是陌生的个人名字,直接删掉重下。官方出品的安装包签名者是明确的公司主体,这个不会含糊。
虚拟机里先装一遍这一步,是为了验证安装包能正常走完流程、装完能起得来。归档站的文件在传输和存储过程中出问题的概率不算低,我遇到过下载完整包解压报错、安装到一半回滚的情况。花二十分钟在快照里验一遍,比装到生产机上出问题再回滚省事得多。
3. chromedriver 的下载渠道与版本匹配实操
3.1 索引页比猜路径效率高得多
老版 chromedriver 的官方存储桶地址是chromedriver.storage.googleapis.com。直接访问根目录会看到一个 XML 列表,信息量不大。真正好用的是一个带参数的索引页,加上?path=<版本号>就能列出该版本下所有平台的压缩包:
https://chromedriver.storage.googleapis.com/index.html?path=2.37/这个页面会列出chromedriver_win32.zip、chromedriver_mac64.zip、chromedriver_linux64.zip等文件。Windows 平台不管你是 32 位还是 64 位系统,用的都是chromedriver_win32.zip—— 这个名字有误导性,但确实是这样,别去找什么 win64 的包,不存在。
3.2 LATEST_RELEASE_XX 文件省去翻目录的功夫
如果你不想一个个版本翻目录,官方还提供了一个便捷入口:在存储桶根目录下,有一系列命名格式为LATEST_RELEASE_<主版本号>的纯文本文件。比如:
https://chromedriver.storage.googleapis.com/LATEST_RELEASE_86访问它会返回一个纯文本,内容是该主版本下推荐的完整驱动版本号,形如86.0.4240.22。拿到这个号之后,再拼下载路径就一气呵成了:
https://chromedriver.storage.googleapis.com/86.0.4240.22/chromedriver_win32.zip这套流程可以完全脚本化。我在项目里写过一个小工具,输入浏览器主版本号,自动解析出驱动版本、下载、解压、放到指定目录,整个流程大概三秒钟。团队里新来的同学配环境,跑一条命令就完事,不用再手工翻网页。
3.3 115 之后规则变了,别再按老办法找
从 115 版本开始,chromedriver 的发布方式有了明确变化:它不再作为独立项目维护版本号,而是直接跟随 Chrome 的版本号走,并且统一走 Chrome for Testing 那套分发体系。也就是说,Chrome 是121.0.6167.85,对应的 chromedriver 就是121.0.6167.85,一位不差。
这个变化带来两个影响。好消息是匹配关系变得极其简单,不用再查对照表;坏消息是老的chromedriver.storage.googleapis.com存储桶在 115 之后基本停更了,你如果在那个桶里翻 121 版本的驱动,是找不到的。得换到 Chrome for Testing 的存储桶去找。
另外从 Selenium 4.6 开始,Python 绑定的webdriver.Chrome()默认会走 Selenium Manager,也就是自动去检测本机浏览器版本、自动下载匹配驱动。这个功能对普通人很方便,但对「锁定旧版本」的场景是个坑:它会给你下最新驱动,直接把你精心维护的旧版本组合打散。所以旧版本环境下一定要显式指定驱动的可执行文件路径,把自动管理绕过去。
3.4 Linux 和 macOS 上的权限与存放位置
Windows 上把chromedriver.exe丢进 PATH 或者显式指定路径就完事,另外两个平台要多几步。
Linux 上解压出来的文件叫chromedriver,没有后缀。直接跑会报权限错误,得先加执行位:
chmod +x chromedriver然后要么放到/usr/local/bin/,要么在代码里指定绝对路径。我倾向于后者,因为多版本共存时放在统一目录里会互相覆盖。
macOS 上除了chmod +x,第一次运行还可能被系统的安全机制拦住,提示「无法验证开发者」。在「系统设置 → 隐私与安全性」里点一下「仍要打开」就行,或者用命令行去掉隔离属性:
xattr -d com.apple.quarantine ./chromedriverApple Silicon 机器还要注意架构问题。113 之后的 Chrome for Testing 提供了mac-arm64的包,但 113 之前的老驱动只有mac64(x86_64)版本,在 M 系列芯片上要靠系统自带的转译层运行。能跑,但启动会慢一点,而且偶尔会有奇怪的行为。如果长期在 M 系列机器上做旧版本自动化,我建议考虑用容器或者虚拟机跑 x86_64 环境。
4. 装完之后的收尾:掐断更新、隔离目录
4.1 Windows 上要处理两个服务和两个计划任务
装完旧版本 Chrome 之后,立刻处理更新通道,这是整个流程里最关键的一步。Windows 平台上的更新机制由几部分组成,只关掉其中一部分不够。
服务方面,在服务管理器里能找到名字里带更新的服务项,把它们设为「禁用」并停止。计划任务方面,任务计划程序库的根目录下会有两个名字里带 Google 更新相关的任务,同样禁用掉。只关服务不关计划任务,过一段时间计划任务会把服务重新拉起来,你会在某个早上发现浏览器又变新了。
更彻底的做法是改注册表的更新策略。在本地机器策略对应的路径下,把自动更新的默认行为改成「不自动更新」。这一层是最高优先级的配置,前面的服务和任务即使被重新启用,策略层也会拦住更新动作。
还有一个兜底手段是目录权限。把 Chrome 安装目录的写权限从普通用户手里收掉,更新程序就写不进去。但这个做法副作用比较明显,某些需要写入安装目录的操作会失败,而且更新程序失败之后可能在后台反复重试,占资源。我一般把它作为最后手段,前面三层都做完之后基本不需要再动它。
4.2 macOS 和 Linux 上的锁版本做法
macOS 上的更新由独立的更新代理组件负责,它藏在用户库目录下的一串路径里。可以直接用组件自带的命令行工具做清理,或者更简单一点,把整个更新组件目录的权限改成只读,让它没法自我更新。另外 macOS 上还要检查一下系统设置里的自动更新选项,把浏览器的自动更新开关关掉。
Linux 上如果是用包管理器装的,锁版本很直接。Debian 系用apt-mark hold把包钉住,Red Hat 系用版本锁插件。之后apt upgrade之类的批量升级操作就会跳过这个包。但要注意,手动dpkg -i安装 deb 包的时候,包管理器可能会自己把新版本拉进来,所以装完之后要立刻确认一次钉住状态。
如果是解压式的绿色安装(直接解压 tar 包到自定义目录),那就没有包管理器的问题,只要不去手动替换目录内容,它就一直是你装的那个版本。
4.3 目录隔离:浏览器、驱动、用户数据分开放
我强烈建议把三样东西分开放,而且目录名里带上版本号:
/opt/chrome-env/ ├── chrome-86.0.4240.198/ ├── chrome-109.0.5414.120/ ├── driver-86.0.4240.22/ ├── driver-109.0.5414.74/ └── profiles/ ├── task-a/ └── task-b/这样做的直接好处是切换成本极低。改一行配置里的路径就能在版本之间来回跳,不需要卸载重装。排错的时候也能快速排除「是不是我装错了版本」这个变量。
用户数据目录单独放还有个额外好处:登录状态、Cookie、本地存储都在里面。同一个 profile 目录重复使用,不需要每次登录;不同任务用不同 profile 目录,账号之间互不干扰。这一点在做多账号场景的时候是刚需,后面的多开部分会细讲。
5. 用 Python 把整套组合跑起来
5.1 显式指定浏览器路径和驱动路径
Selenium 4 的写法跟 3 有区别,特别是 Service 对象的引入。旧版本组合下,下面这段是我最常用的骨架:
from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options options = Options() # 关键:显式指向旧版本浏览器的可执行文件 options.binary_location = r"C:\chrome-env\chrome-86.0.4240.198\chrome.exe" # 关键:显式指定用户数据目录,避免和其他任务冲突 options.add_argument(r"--user-data-dir=C:\chrome-env\profiles\task-a") options.add_argument("--window-size=1440,900") options.add_argument("--disable-extensions") options.add_argument("--disable-popup-blocking") service = Service(executable_path=r"C:\chrome-env\driver-86.0.4240.22\chromedriver.exe") driver = webdriver.Chrome(service=service, options=options) try: driver.get("https://example.com") print(driver.title) finally: driver.quit()binary_location和executable_path这两个参数是整套配置的命门。前者告诉 Selenium 用哪个浏览器,后者告诉它用哪个驱动,两个都指定死了,系统里装了八百个版本的 Chrome 都不会互相干扰。
提示:如果你用的是 Selenium 4.6 以上版本,不加
executable_path时它会走自动管理,去网上拉最新驱动。锁定旧版本的环境下这是灾难,务必显式指定。
5.2 无头模式的分水岭在 109
做后台任务的时候,无头模式能省下不少资源。但无头模式的参数在版本之间有过一次变化:109 及以后可以用--headless=new,这个新模式走的是完整浏览器内核,渲染结果跟有界面模式基本一致;109 之前只能用--headless,那是老的实现,某些页面元素定位会出偏差。
# 109 及以上 options.add_argument("--headless=new") # 109 以下 options.add_argument("--headless")差别有多大?我实测过一个含 Canvas 图表的页面,老无头模式下图表区域截图是全白的,新无头模式正常。所以如果你的任务里涉及截图比对或者可视化元素定位,又只能用老版本,那建议干脆不开无头,用虚拟显示的方式跑。
无头模式下还有一组参数几乎必加。--disable-gpu在部分环境下不加会崩;--no-sandbox和--disable-dev-shm-usage主要是 Linux 容器环境的必备项,后者解决共享内存不足导致浏览器崩溃的问题。Windows 上这两个不加也能跑,但加了没坏处。
5.3 多开:把 profile 列表写成 txt,再转成 bat
热搜里有个词是「谷歌浏览器多开txt转bat」,这个做法很实用。核心思路是:每行写一个 profile 目录路径,然后用批处理脚本循环读取,每个路径起一个独立实例。
txt 文件长这样:
C:\chrome-env\profiles\acc-01 C:\chrome-env\profiles\acc-02 C:\chrome-env\profiles\acc-03批处理脚本:
@echo off set CHROME=C:\chrome-env\chrome-109.0.5414.120\chrome.exe for /f "usebackq delims=" %%p in ("profiles.txt") do ( start "" "%CHROME%" --user-data-dir="%%p" --no-first-run --no-default-browser-check )这里有三个细节容易翻车。第一,每个实例的user-data-dir必须完全不同,重复了会直接报「用户数据目录已被使用」然后退出。第二,同一个 profile 目录不能被两个进程同时打开,所以批量启动时如果某个目录已经被占用,那个实例会静默失败,最好在脚本里加个判断或者错开启动时间。第三,每个 profile 目录第一次启动都会走一遍初始化引导,加--no-first-run --no-default-browser-check能跳过,省时间。
如果要在自动化代码里管理多开,思路是一样的,只是把批处理换成一个路径列表的循环,每个任务分配一个独立的 driver 实例和独立的 profile 目录。注意内存占用,每个完整浏览器实例大概吃 150 MB 到 300 MB 内存,开十个就要预留两三个 G。
5.4 驱动日志开起来,出问题才有据可查
默认情况下 chromedriver 什么日志都不输出,一旦出问题你只能看到一个笼统的异常。把日志打开之后,连接过程、命令收发、浏览器启动参数全都能看到,排查效率天差地别。
service = Service( executable_path=r"C:\chrome-env\driver-86.0.4240.22\chromedriver.exe", log_output=r"C:\chrome-env\logs\chromedriver.log", service_args=["--verbose"] )日志文件里最该关注的是浏览器启动参数那一段,以及出现session not created之前最后几条记录。如果驱动尝试连了一个完全不同的端口,说明本机有另一个 chromedriver 实例还活着;如果浏览器启动参数里出现了你没设置的选项,说明有环境变量或者配置文件在捣乱。
6. 高频报错的完整排查链路
6.1 session not created 的三种不同成因
这条报错覆盖面很广,实际上至少有三种不同的原因,报错文案会给出线索。
第一种,文案里明确写着This version of ChromeDriver only supports Chrome version XX,这就是纯粹的版本不匹配,没有任何歧义。处理方式:看清它提示的版本号,去下对应的驱动,或者把浏览器降到驱动支持的版本。
第二种,文案是cannot find Chrome binary。这说明驱动根本没找到浏览器可执行文件。常见原因是binary_location没设置、路径写错、或者路径里有中文和空格导致解析失败。还有一种隐蔽情况是装了两个 Chrome,注册表里的安装路径指向了另一个,而你没显式覆盖。
第三种,文案是Chrome failed to start: crashed。这不是版本问题,是启动参数的问题。要么是user-data-dir指向的目录不可写,要么是某个参数组合互相冲突,要么是安全软件拦了。把参数简化到最基础(只留binary_location和user-data-dir)再试,能启动就说明是参数问题,然后一项项加回来定位。
我自己排查这三种情况的顺序是:先看报错文案的完整内容,不看第一行看后面几行;再跑一次最小化脚本,只开浏览器不开页面;最后看驱动日志。三步走下来基本都能定位到根因,很少需要靠猜。
6.2 DevToolsActivePort file doesn't exist 的几种解法
这个报错在 Linux 容器环境里特别常见。原因是 Chrome 启动时需要在一个临时目录里写一个端口信息文件,Selenium 去读这个文件来建立连接。如果这个文件没生成,或者生成的位置读不到,就报这个错。
最常见的成因是/dev/shm空间太小。容器默认给共享内存的空间往往只有 64 MB,Chrome 跑起来不够用。加--disable-dev-shm-usage让它改用/tmp就好了。第二个成因是权限问题,Chrome 在沙箱模式下需要内核支持,容器里往往没有,加--no-sandbox绕过。第三个成因是user-data-dir指向的目录不存在或者不可写,这个用ls -l一看就知道。
还有一个不太常见但确实存在的成因:系统时间不对。DevTools 的握手过程有时间戳校验,系统时间偏得太多会导致校验失败。容器里如果没同步时间,检查一下。
6.3 浏览器起来了却立刻退出
这种情况比直接崩溃更难查,因为表面上什么都没发生。表现是脚本执行到driver.get()就抛异常,驱动日志里能看到浏览器进程被拉起,然后马上收到退出信号。
我遇到过三次,成因各不相同。第一次是 Chrome 的「恢复上次会话」弹窗挡路,加了--no-first-run --no-restore-session-state解决。第二次是某个扩展在启动时崩溃,把整个浏览器带崩了,加--disable-extensions解决。第三次最隐蔽:profile 目录里残留了一个损坏的Preferences文件,把整个 profile 目录清空重建就好了。
排查这类问题时,先把 profile 目录换个全新的空目录试试。如果换空目录就正常,那问题一定在 profile 里面的某个状态文件上,不用再往别处找。
6.4 页面加载超时与 renderer 消息超时
Timed out receiving message from renderer和disconnected: unable to connect to renderer这两条,通常不是环境问题,是页面本身的问题。某个页面里的脚本卡住了渲染进程,或者页面持续加载长连接资源导致pageLoadStrategy一直等不到加载完成事件。
解法有三条。一是调整页面加载策略,把默认的normal改成eager或者none,不等资源加载完就返回控制权:
options.page_load_strategy = "eager"二是设置显式超时:
driver.set_page_load_timeout(30) driver.set_script_timeout(30)三是把脚本执行超时的参数传给驱动。给驱动加--script-timeout参数,或者在service_args里传--timeout。
不过要提醒一句:这些超时设置是「让脚本别卡死」的手段,不是「解决问题」的手段。如果某个页面在你的旧版本浏览器上稳定超时,而在新版上正常,那大概率是页面用了新版本的 API,这时候要么升级浏览器,要么放弃这个页面。硬扛没意义。
7. 让旧版本环境长期跑下去的几个经验
7.1 把安装包和驱动归档,别依赖外部链接
归档站会失效,存储桶会改路径,链接会 404。我吃过这个亏:某个跑了半年的项目需要重建环境,结果当初用的那个归档页关了,翻了两天才从另一个地方找到同版本的文件,还得担心来源是否可靠。
现在的做法是,每确定一套可用的版本组合,就把浏览器安装包和对应的驱动包一起打包,放进项目自己的归档目录。归档目录的命名带上完整版本号和校验值,比如chrome-86.0.4240.198-win64-<哈希前八位>.zip。同时在 README 里记清楚这套组合是在哪台机器、哪个系统版本上验证通过的。
这套归档的总体积不会太大。一套组合加上驱动大概 100 MB 出头,存三套也就 300 MB 多。放内网文件服务器或者对象存储里,成本可以忽略,但省下来的排查时间是很可观的。
7.2 多版本共存时的目录命名和切换方式
前面提过目录里带版本号的方案,这里补充几个实践细节。
命名格式建议统一成<组件>-<完整版本号>-<平台>,比如chrome-109.0.5414.120-win64。不要用chrome-old、chrome-backup这种模糊名字,三个月之后你自己都想不起来里面是什么。
切换版本不要在代码里硬编码路径散落各处。我的做法是在项目根目录放一个.env文件,把浏览器路径和驱动路径写成变量,代码里通过环境变量读取。切换版本时改.env一处即可。配合前面说的 bat 脚本或者 shell 脚本,还能做到「一条命令切到指定版本组合」。
另外,PATH 环境变量里不要放任何 chromedriver 的路径。一旦放了,某个不显式指定executable_path的旧脚本就会用到它,然后你就陷入「为什么这个脚本和那个脚本用的驱动不一样」的困惑里。所有路径都显式指定,是最不容易出错的策略。
7.3 用容器或者虚拟机把环境整个封起来
如果这套旧版本环境是要长期跑的关键任务,我建议再往前走一步,把它封进容器或者虚拟机。
容器方案适合 Linux 环境。基础镜像选一个跟你需要的浏览器版本匹配的系统版本,把 Chrome、chromedriver、以及运行依赖一起装进去,打成镜像。之后启动容器就是启动一整套环境,宿主机上装什么版本都影响不到它。缺点是镜像体积会大一些,几百 MB 到 1 GB 之间。
虚拟机方案适合 Windows 环境。装好系统、装好旧版 Chrome、关掉更新、配好驱动,然后打一个快照。以后环境出任何问题,回滚快照就行,三分钟恢复。虚拟机唯一的缺点是资源占用,但对 Windows 下的自动化任务来说,这个代价是值得的。
虚拟机还有一个额外好处:快照里天然固化了「更新已关闭」这个状态,不用担心某个操作把更新重新打开。只要不回滚之后又手动去开更新,这个环境可以稳定用很久。
7.4 定期做一次存活检查
环境封起来不等于一劳永逸。我建议加一个每日的存活检查任务:启动一次浏览器、打开一个本地静态页、截图、关掉。全程不联网,只验证「浏览器能起来、驱动能连上、页面能渲染」这三件事。
这个检查的好处是,环境出问题的时候你是第一时间知道的,而不是等到正式任务失败了才发现。而且检查脚本本身跑得很快,十秒左右,资源开销可以忽略。日志保留最近三十天,出问题的时候往前翻,能看清问题是哪天开始出现的。
我踩过最深的一个坑,就是某个环境在更新通道被关闭之前已经完成了一次部分更新,安装目录里的文件和版本号对不上了。表面上能启动,跑一段时间就崩。这种问题没有存活检查根本发现不了,等发现的时候已经是一周后了。加上每日检查之后,这类问题当天就能暴露出来,处理成本低很多。
最后分享一个我自己一直在用的小技巧:把「浏览器版本 + 驱动版本 + 首次验证日期 + 验证通过的操作系统」写成一行,存进项目根目录的一个纯文本文件里,命名就叫ENV-LOCK.txt。每个人接手这个项目,第一眼看这个文件,就知道环境应该是什么样子。这个习惯看起来有点土,但在多人协作和隔了半年再回来维护的场景里,它救过我很多次。