
做自动化测试这行的人几乎都被 ChromeDriver 的版本问题绊过一跤。前阵子帮同事看一个跑了两年的采集脚本报错只有一行SessionNotCreatedException: This version of ChromeDriver only supports Chrome version 114但本机 Chrome 早就自动升到 119 了。这种问题不难恶心在反复出现——浏览器后台偷偷升级驱动还停在原地脚本第二天早上就挂了。116 到 119 这几个版本又刚好踩在下载地址迁移的节点上很多老教程给的chromedriver.storage.googleapis.com已经不再更新照抄就是死路。这篇内容就是把这几个版本的驱动安装彻底讲透版本号到底要匹配到哪一位、新下载地址长什么样、Windows/macOS/Linux 三平台分别怎么放、Selenium 4.x 自带的自动管理机制什么时候能用什么时候要关掉以及装完之后跑起来会撞上哪些坑。不管你是刚学 selenium 安装的新手还是已经在维护一整套 selenium 自动化测试框架的老手照着走一遍都能落地。1. 版本号对不上是九成新手卡住的第一个坎1.1 Selenium、ChromeDriver、Chrome 三者到底是什么关系先把这三者的分工理清楚后面所有报错你都能自己推出来。Selenium 本身只是一套客户端库它不碰浏览器只负责用 W3C WebDriver 协议发 HTTP 请求ChromeDriver 是中间人接收这些请求翻译成 Chrome 能听懂的指令再把浏览器的返回结果打包回去Chrome 才是真正干活的。三者是一条链任何一环版本错位链条就断。这里有个容易误解的点Selenium 的版本和 ChromeDriver 的版本没有任何对应关系。Python 里pip install selenium装的是 4.15 还是 4.20都不影响你需要哪个驱动真正决定驱动版本的只有本机 Chrome 的主版本号。我见过有人为了配驱动去降级 selenium纯属白折腾。另一个误解是驱动装在项目里就行实际上 ChromeDriver 是独立可执行文件它的位置要么在系统 PATH 里要么你显式告诉 Selenium 它在哪Selenium 不会自动去项目目录翻。还有一层是协议兼容性。Selenium 4.x 用的是 W3C 标准协议ChromeDriver 从 75 版本之后就只认这套协议了。所以如果你手头还有 Selenium 3.x 的老项目配新版驱动时会出现命令发出去没反应的情况这时候要么升级 Selenium要么把驱动降到 74 及以下但后者基本不现实因为老驱动又不支持新 Chrome。结论很简单新 Chrome 配新驱动配 Selenium 4这条路是唯一顺畅的。1.2 116 版本是个分水岭下载地址换了地方这是本文最核心的一条信息。115 及之前ChromeDriver 一直挂在chromedriver.storage.googleapis.com这个老域名下目录结构几十年不变。但从 115 开始官方把它并入了 Chrome for Testing 项目新的分发地址变成storage.googleapis.com/chrome-for-testing-public索引页也从原来的老页面搬到了googlechromelabs.github.io/chrome-for-testing。这件事的直接影响是你在网上搜到的绝大多数中文教程、博客、甚至一些付费课程里的下载链接指向的都是老地址。老地址不是打不开而是它停在 114 版本不再更新。你点进去看列表最新一条就是 114.0.5735.90再往下就没有了。很多人下载下来发现版本不对又回去怀疑自己 Chrome 版本看错了其实问题出在链接本身就是死的。116、117、118、119 这几个版本全部只在 Chrome for Testing 下发布。它的目录命名规则是{完整版本号}/{平台}/chromedriver-{平台}.zip。注意这里是完整版本号比如119.0.6045.105而不是主版本119。这一点让手工拼链接变得很麻烦因为你不可能记住每个小版本号。所以正确的姿势不是拼 URL而是去读官方提供的 JSON 索引用代码匹配这个后面第 3 章会详细写。1.3 主版本、次版本、尾段到底要匹配到哪一位官方文档的说法是只保证主版本号匹配也就是 Chrome 119.x.x.x 配 ChromeDriver 119.x.x.x 就行。但实践里这个宽容度是有代价的。驱动和浏览器之间通过 DevTools Protocol 通信这个协议的接口在不同小版本之间会增删字段。如果驱动版本比浏览器版本新太多比如用 119.0.6045.200 的驱动去驱动 119.0.6045.105 的浏览器一般没事反过来用 119.0.6045.105 的驱动去驱动自动升级后的 119.0.6045.200 浏览器就可能撞上某个新接口找不到报一堆看不懂的 DevTools 错误。所以我的建议是能对齐就对齐至少对齐到完整版本号的前三段。实际操作中你不需要手动去找那个精确版本因为 Chrome for Testing 的 JSON 里带了完整的版本列表按前缀匹配取最新一个即可成本几乎为零。真正需要警惕的是浏览器自动更新——它会在后台静默升级而驱动不会跟着变。这就是为什么你的脚本可能周一好好的周三早上突然挂了。提示如果你在做长期运行的无人值守任务务必关掉 Chrome 的自动更新或者把驱动更新脚本挂到定时任务里让它在每次运行前先校验版本。这两种方案我都在用前者更省事后者更保险。2. 动手之前先把版本、平台、路径这三件事确认清楚2.1 三种方式拿到本机 Chrome 的精确版本号第一种图形界面。打开 Chrome地址栏输入chrome://version第一行 Google Chrome 后面那一长串就是完整版本比如119.0.6045.105 (Official Build) (64-bit)。这个方法最直观缺点是如果 Chrome 跑在服务器上没有图形界面你就用不了。第二种命令行查看可执行文件版本。Windows 下打开 PowerShell执行(Get-Item C:\Program Files\Google\Chrome\Application\chrome.exe).VersionInfo.ProductVersion直接输出完整版本号。macOS 下执行/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --versionLinux 下执行google-chrome --version或chromium-browser --version。这几个命令我在 CI 环境里天天用很稳。第三种从正在运行的会话里读。如果你已经能启动浏览器driver.capabilities[browserVersion]就能拿到版本。但这个思路有点鸡生蛋的味道——驱动没配对就启动不了所以它更适合用来做版本校验而不是首次获取。有一点必须强调Windows 上 Chrome 可能同时存在 32 位和 64 位两个安装目录Program Files和Program Files (x86)你查哪个目录的版本就要配对应平台的驱动。查错了目录版本号可能都是对的但架构不匹配启动照样失败。2.2 平台标识怎么选选错了症状很隐蔽Chrome for Testing 里每个版本下面有五个平台目录含义如下平台标识对应环境常见误用win64Windows 64 位 Chrome最常见绝大多数现代 Windows 都用这个win32Windows 32 位 Chrome老机器或某些嵌入式环境才用mac-x64Intel 芯片 MacM 系列芯片误用会导致无法执行mac-arm64Apple SiliconM1/M2/M3新 Mac 必选选错直接拒绝运行linux64Linux 64 位服务器、容器环境首选误选平台这件事的麻烦在于报错信息不直观。在 Apple Silicon 的 Mac 上下载了 mac-x64 的驱动执行时会报bad CPU type in executable提示还算清楚。但 Windows 上如果 64 位系统装了 win32 驱动有时能启动有时偶发崩溃最难排查。判断 Mac 芯片类型左上角苹果图标进关于本机看芯片一栏写的是 Apple M 系列还是 Intel。判断 Linux 架构uname -m输出x86_64就用 linux64输出aarch64的话目前官方没有对应的 ARM Linux 驱动需要走别的路径解决这点要有心理准备。2.3 驱动该放哪儿PATH、安装目录还是自定义目录三种放法各有场景我按推荐度排个序。自定义目录 Service 显式指定是我最推荐的。在项目根目录建一个drivers/文件夹把 chromedriver 扔进去代码里写Service(executable_path./drivers/chromedriver)。好处是版本跟着项目走不同项目可以用不同驱动版本互不干扰容器化部署时也方便打包。缺点是路径要对用相对路径时注意工作目录。放进系统 PATH适合本机长期只有一套环境的情况。Windows 把chromedriver.exe丢进C:\Windows\System32或者自己加一个 PATH 目录macOS/Linux 丢进/usr/local/bin/。放进去之后还要给执行权限chmod x /usr/local/bin/chromedriver少了这步会报 Permission denied。放进 Chrome 安装目录是老教程的常见做法Windows 下就是和chrome.exe放一起。这个方法能用但我不太推荐因为 Chrome 自动更新时有可能把目录结构动掉而且权限管理麻烦卸载重装 Chrome 时驱动也会一起没。注意macOS 从某个版本开始会对下载的可执行文件加隔离属性直接运行会弹无法验证开发者。解决方法是xattr -d com.apple.quarantine ./drivers/chromedriver一条命令去掉隔离标记。这一步在 macOS 上几乎必做别问为什么驱动明明下载对了却跑不起来。3. 116 到 119 的驱动下载与安装全流程3.1 用 JSON 接口查版本比翻网页快十倍Chrome for Testing 提供了几个 JSON 接口直接读它们比在网页上肉眼找版本可靠得多。常用的有三个第一个是last-known-good-versions.json返回当前 Stable、Beta、Dev、Canary 四个通道的最新版本及下载地址。如果你的目标就是配最新稳定版读这一个就够了。第二个是known-good-versions-with-downloads.json返回一批经过验证的版本及其下载链接数量在几百个量级。116 到 119 的绝大多数小版本都在里面。第三个是all-versions-with-downloads.json最全包含所有历史版本文件体积也最大。只有在需要精确定位某个老版本时才用它。需要说明的是这三个接口的地址都是公开的静态 JSON用requests.get直接取就行不需要任何认证。返回结构里versions数组每一项有version字段和downloads对象downloads.chromedriver是按平台分的数组每项含platform和url。写代码时我的习惯是先按主版本号过滤再按版本号字符串排序取最大的那个。这样不管官方什么时候新增小版本脚本都能自动拿到最新的匹配驱动不用改代码。3.2 手工下载安装的三平台操作实录如果只是临时用一次手工操作是最快的。以 119.0.6045.105 为例Windows访问索引页找到 119.0.6045.105下载chromedriver-win64.zip解压得到chromedriver.exe放进项目的drivers/目录。如果杀毒软件报毒并直接删文件需要把该目录加入白名单这是常见现象不是文件有问题。macOS先uname -m确认架构M 系列芯片下载chromedriver-mac-arm64.zipIntel 下载chromedriver-mac-x64.zip。解压后chmod x ./chromedriver xattr -d com.apple.quarantine ./chromedriver ./chromedriver --version最后一行能打印出版本号就说明驱动本身没问题了。Linux下载chromedriver-linux64.zip解压后同样给执行权限。如果是容器环境注意基础镜像里得装 Chrome 本体光有驱动没用。Alpine 镜像还需要额外装 glibc 兼容层用 Debian 系镜像会省心很多。不管哪个平台解压后都建议跑一次chromedriver --version自检。这一步能在 30 秒内确认驱动本身可用把驱动问题和代码问题彻底分开省掉大量猜谜时间。3.3 用一段 Python 脚本自动匹配并下载驱动长期维护的项目我强烈建议把这一步自动化。完整脚本大约五十行核心逻辑分四步读本机 Chrome 版本、拉 JSON 索引、匹配下载链接、解压到指定目录。import json import os import platform import re import subprocess import zipfile from pathlib import Path import requests INDEX_URL https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json DRIVER_DIR Path(__file__).parent / drivers def get_chrome_version() - str: system platform.system() if system Windows: cmd r(Get-Item C:\Program Files\Google\Chrome\Application\chrome.exe).VersionInfo.ProductVersion out subprocess.check_output([powershell, -Command, cmd], textTrue) elif system Darwin: cmd /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --version out subprocess.check_output(cmd, shellTrue, textTrue) else: out subprocess.check_output(google-chrome --version, shellTrue, textTrue) return re.search(r\d\.\d\.\d\.\d, out).group() def get_platform_key() - str: system platform.system() machine platform.machine().lower() if system Windows: return win64 if 64 in platform.architecture()[0] else win32 if system Darwin: return mac-arm64 if machine in (arm64, aarch64) else mac-x64 return linux64 def find_download_url(full_version: str, plat: str) - str: major full_version.split(.)[0] data requests.get(INDEX_URL, timeout30).json() candidates [] for item in data[versions]: if item[version].startswith(major .): for d in item[downloads].get(chromedriver, []): if d[platform] plat: candidates.append((item[version], d[url])) if not candidates: raise RuntimeError(f没有找到主版本 {major} 对应的驱动) candidates.sort(keylambda x: [int(n) for n in x[0].split(.)]) return candidates[-1][1] def download_and_extract(url: str) - Path: DRIVER_DIR.mkdir(exist_okTrue) zip_path DRIVER_DIR / chromedriver.zip zip_path.write_bytes(requests.get(url, timeout120).content) with zipfile.ZipFile(zip_path) as zf: zf.extractall(DRIVER_DIR) zip_path.unlink() for p in DRIVER_DIR.rglob(chromedriver*): if p.is_file() and p.suffix ! .zip: os.chmod(p, 0o755) return p raise RuntimeError(解压后没有找到驱动文件) if __name__ __main__: ver get_chrome_version() plat get_platform_key() url find_download_url(ver, plat) path download_and_extract(url) print(fChrome {ver} / 平台 {plat}) print(f驱动路径 {path}) subprocess.run([str(path), --version])这个脚本有几个我自己踩出来的细节。第一版本排序不能按字符串排119.0.6045.9会排在119.0.6045.105后面必须切成整数列表排序。第二解压出来的目录名带平台后缀比如chromedriver-win64/chromedriver.exe所以要用rglob递归找不能写死路径。第三macOS 和 Linux 上必须补chmod否则下载完第一次运行必挂。3.4 Selenium Manager4.6 之后的新选择什么时候该关掉它从 Selenium 4.6 开始官方内置了一个叫 Selenium Manager 的组件它会在你调用webdriver.Chrome()时自动检测浏览器版本、自动下载匹配驱动、自动缓存到本地。到 4.11 之后这个机制已经相当稳定很多简单场景下你什么都不用配pip install selenium加三行代码就能跑起来。但它不适合所有情况我在下面这几种场景会主动绕开它一是离线或受限网络环境。Selenium Manager 需要联网拉取索引和驱动包内网机器上必然超时而且它的超时提示不够明确容易误判成别的问题。二是需要锁定驱动版本。自动化测试讲究可复现同一个脚本在不同时间跑出不同结果是很头疼的。Manager 会取最新匹配版本浏览器一升级它就跟着换测试基线就漂了。三是多项目并行、驱动版本各异。Manager 的缓存目录是全局的多个项目共用可能互相干扰。要绕开它很简单只要显式传入Service(executable_path...)Manager 就不会介入。想彻底禁用可以设置环境变量SE_MANAGER_PATH或把--offline相关参数交给它不过绝大多数情况下显式指定路径就够了。提示Selenium Manager 的缓存位置在 Windows 是%USERPROFILE%\.cache\seleniumLinux/macOS 是~/.cache/selenium。遇到莫名其妙的驱动问题把这个目录整个删掉重来往往比逐个排查快。4. 驱动就绪之后代码层面还有这些细节要处理4.1 最小可运行代码与 Service 对象的三种写法先把最小的能跑通的代码贴出来这是所有后续配置的基线from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options options Options() service Service(executable_path./drivers/chromedriver) driver webdriver.Chrome(serviceservice, optionsoptions) driver.get(https://example.com) print(driver.title) driver.quit()Service 对象有三种传参方式效果一样但适用场景不同。第一种就是上面的executable_path字符串路径最直观。第二种是Service(service_args[...])用来给驱动进程本身传参数比如--verbose打开详细日志排查启动问题时非常有用。第三种是直接传一个已启动的驱动端口Service(port9515)适合你自己手动把驱动当服务跑起来的场景。一定要记住driver.quit()它会关掉浏览器进程和驱动的连接。很多人只写driver.close()那只关标签页驱动进程还挂在后台跑几十次之后机器上会堆一坨 chromedriver 僵尸进程内存慢慢就被吃光了。在 CI 环境里我习惯用 try/finally 包裹保证异常时也能清理。4.2 Options 里那些真正有用的参数网上关于 Chrome 启动参数的列表能列出上百个但日常真正用得上的就那么十来个。我按使用频率从高到低说。--headlessnew是无头模式。注意 116 之后的 Chrome 用的是新版无头实现写法是--headlessnew老的--headless在新版里行为有差异某些渲染场景下表现不一样。无头模式下窗口尺寸默认是 800x600很多响应式页面会渲染成手机版定位元素时找不到所以要配--window-size1920,1080。--no-sandbox和--disable-dev-shm-usage是容器环境的标配。前者绕过沙箱后者把共享内存写到磁盘。不加这两个在 Docker 里跑大概率报DevToolsActivePort file doesnt exist或者跑到一半浏览器崩掉。--disable-gpu在无头环境里建议加上尤其是有图形渲染需求时。现代 Chrome 其实已经不太需要它但加上无副作用能避免一些显卡驱动相关的诡异问题。下载路径配置要走 prefs这是实验性选项写法是options.add_experimental_option(prefs, {...})里面配download.default_directory指定目录、download.prompt_for_download设为 False 关闭弹窗。这个配置在采集类脚本里几乎必用默认下载目录是系统下载文件夹多任务并行时会互相覆盖文件名。--page-load-strategy用的是options.page_load_strategy eager这种属性写法不是 add_argument。默认的 normal 策略会等所有资源加载完遇到带大量统计脚本的页面能卡到超时切成 eager 只等 DOM 就绪速度能快好几倍。注意不要在代码里硬编码--user-agent去伪装什么现代反爬对 UA 的校验早就不是主战场了而且写死 UA 会让浏览器特征和 UA 不一致反而更容易被识别。真需要改用options.add_argument(--user-agent...)保持和实际环境一致。4.3 驱动就绪之后定位元数据的存放与元素枚举驱动跑起来只是第一步接下来一整天的活都在和页面元素打交道。这里分享一个我用了很多年的写法Page Object 里只存定位元数据不存 WebElement 对象。原因很直接。WebElement是页面状态的快照式引用页面一刷新或者 DOM 一重渲染之前拿到的对象就 stale 了再调用就报StaleElementReferenceException。很多新手被这个错折磨到怀疑人生根子就在于把元素对象当成员变量缓存了。正确做法是只存(By, value)这样的元数据from selenium.webdriver.common.by import By class LoginPage: username (By.CSS_SELECTOR, #username) password (By.CSS_SELECTOR, #password) submit (By.CSS_SELECTOR, button[typesubmit]) def __init__(self, driver): self.driver driver def login(self, user, pwd): self.driver.find_element(*self.username).send_keys(user) self.driver.find_element(*self.password).send_keys(pwd) self.driver.find_element(*self.submit).click()每次用的时候现查现取代价是一次网络往返换来的是永远不会 stale。这套写法在 selenium 自动化测试框架里是事实标准为什么大家这么做答案就在这。至于页面元素枚举也就是遍历一批同类元素find_elements是最直接的工具但要注意它返回的是列表找不到时返回空列表而不是抛异常所以判断页面是否有这个模块时用if driver.find_elements(...)比 try/except 更干净。反过来find_element找不到会抛NoSuchElementException写在循环里会让脚本直接中断这时候用WebDriverWait配合expected_conditions更合适。给个小技巧expected_conditions里有presence_of_element_located、visibility_of_element_located、element_to_be_clickable三个高频方法它们对元素的要求依次变严。presence 只要求 DOM 里有visibility 要求可见clickable 还要求能点。很多人用 presence 等到了元素却点不动就是因为元素虽然存在但被遮罩挡住了换成 clickable 立刻就好。5. 踩坑实录报错、排查思路与速查表5.1 六个高频报错逐个拆解SessionNotCreatedException: This version of ChromeDriver only supports Chrome version 118。最经典的一个版本不匹配。注意报错里会明确告诉你驱动支持的版本以及当前浏览器版本。处理方式就是按第 3 章的脚本重新下载对应驱动。有极少情况是驱动路径指向了旧文件检查executable_path是否指对了地方。chromedriver executable needs to be in PATH。Selenium 没找到驱动。如果你用了 Service 指定路径说明路径写错了检查相对路径的基准目录如果没用 Service说明 PATH 没配。这个报错在不同 selenium 版本里文案略有差异但意思一样。WebDriverException: unknown error: DevToolsActivePort file doesnt exist。容器环境的头号杀手。原因是 Chrome 在沙箱里启动失败加--no-sandbox和--disable-dev-shm-usage基本能解决九成。剩下的一成通常是容器内存太小Chrome 启动一半被 OOM 杀掉了加大内存或者限制--window-size能缓解。macOS 上cannot be opened because the developer cannot be verified。隔离属性没去掉xattr -d com.apple.quarantine一条命令解决。注意每次重新下载驱动都要执行一次因为隔离属性是跟随文件走的。Permission denied。Linux/macOS 上忘了chmod x。这个错太直白了但偏偏最容易忘因为从 zip 解压出来的文件默认没有执行权限。脚本跑一段时间后莫名卡死。八成是浏览器进程没退出累积占用资源。检查代码里driver.quit()有没有写到 finally 块里以及是否在异常分支提前 return 跳过了清理。我在 CI 里还会加一步超时强杀防止死锁。5.2 问题速查表现象或报错最可能的原因处理方式提示只支持 Chrome 某版本驱动与浏览器主版本不一致按主版本重新下载驱动找不到 chromedriver 可执行文件路径错误或 PATH 未配置检查 executable_path 或加入 PATHDevToolsActivePort 不存在容器沙箱限制/内存不足加 --no-sandbox、--disable-dev-shm-usagemacOS 拒绝运行文件带隔离属性xattr -d com.apple.quarantinePermission denied缺少执行权限chmod xbad CPU type下载了错误架构的驱动M 系列芯片选 mac-arm64元素定位报 stale缓存了 WebElement 对象只存 By 元数据现查现取页面加载超时默认加载策略等全部资源改成 eager 加载策略驱动文件凭空消失被杀毒软件拦截目录加白名单脚本隔几天自动失效浏览器后台自动升级关自动更新或加版本校验5.3 版本升级后的回归检查清单浏览器一升级我习惯按这个顺序过一遍能挡住绝大多数后续问题第一步重新获取 Chrome 完整版本号并和驱动版本比对重点看主版本是否一致。第二步跑一次chromedriver --version确认驱动文件本身没被替换或者拦截。第三步跑一个最小的打开页面并打印标题的脚本把环境问题和业务代码问题切开。第四步检查无头模式下的窗口尺寸这一步经常出问题新版 Chrome 渲染引擎小改一下某些定位就会失效。第五步跑完整回归重点看那些依赖WebDriverWait超时时间的用例浏览器性能变化会影响这些边界值。这套流程走下来大概五分钟比起线上任务半夜挂掉再爬起来排查划算太多。我现在的做法是把前两步写进启动脚本每次运行前自动校验发现问题直接刷新驱动再启动基本能做到无人值守。最后分享一个我用了很久的习惯把驱动的完整版本号和下载日期写进项目的 README顺手记一行当前 Chrome 主版本 XX。团队里换人接手时这一行字能省掉半天的摸索。踩过几次坑之后我越来越觉得驱动这东西本身不复杂复杂的只是信息不对称把版本、平台、路径三件事一次性搞对后面就都是顺水推舟的事了。