做自动化测试这行的人,几乎都被 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 --version,Linux 下执行google-chrome --version或chromium-browser --version。这几个命令我在 CI 环境里天天用,很稳。
第三种,从正在运行的会话里读。如果你已经能启动浏览器,driver.capabilities['browserVersion']就能拿到版本。但这个思路有点鸡生蛋的味道——驱动没配对就启动不了,所以它更适合用来做版本校验而不是首次获取。
有一点必须强调:Windows 上 Chrome 可能同时存在 32 位和 64 位两个安装目录(Program Files和Program Files (x86)),你查哪个目录的版本,就要配对应平台的驱动。查错了目录,版本号可能都是对的,但架构不匹配,启动照样失败。
2.2 平台标识怎么选,选错了症状很隐蔽
Chrome for Testing 里每个版本下面有五个平台目录,含义如下:
| 平台标识 | 对应环境 | 常见误用 |
|---|---|---|
| win64 | Windows 64 位 Chrome | 最常见,绝大多数现代 Windows 都用这个 |
| win32 | Windows 32 位 Chrome | 老机器或某些嵌入式环境才用 |
| mac-x64 | Intel 芯片 Mac | M 系列芯片误用会导致无法执行 |
| mac-arm64 | Apple Silicon(M1/M2/M3) | 新 Mac 必选,选错直接拒绝运行 |
| linux64 | Linux 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.zip,Intel 下载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], text=True) elif system == "Darwin": cmd = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --version" out = subprocess.check_output(cmd, shell=True, text=True) else: out = subprocess.check_output("google-chrome --version", shell=True, text=True) 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, timeout=30).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(key=lambda x: [int(n) for n in x[0].split(".")]) return candidates[-1][1] def download_and_extract(url: str) -> Path: DRIVER_DIR.mkdir(exist_ok=True) zip_path = DRIVER_DIR / "chromedriver.zip" zip_path.write_bytes(requests.get(url, timeout=120).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(f"Chrome {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 Manager:4.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\selenium,Linux/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(service=service, options=options) driver.get("https://example.com") print(driver.title) driver.quit()Service 对象有三种传参方式,效果一样但适用场景不同。第一种就是上面的executable_path,字符串路径最直观。第二种是Service(service_args=[...]),用来给驱动进程本身传参数,比如--verbose打开详细日志,排查启动问题时非常有用。第三种是直接传一个已启动的驱动端口Service(port=9515),适合你自己手动把驱动当服务跑起来的场景。
一定要记住driver.quit(),它会关掉浏览器进程和驱动的连接。很多人只写driver.close(),那只关标签页,驱动进程还挂在后台,跑几十次之后机器上会堆一坨 chromedriver 僵尸进程,内存慢慢就被吃光了。在 CI 环境里我习惯用 try/finally 包裹,保证异常时也能清理。
4.2 Options 里那些真正有用的参数
网上关于 Chrome 启动参数的列表能列出上百个,但日常真正用得上的就那么十来个。我按使用频率从高到低说。
--headless=new是无头模式。注意 116 之后的 Chrome 用的是新版无头实现,写法是--headless=new,老的--headless在新版里行为有差异,某些渲染场景下表现不一样。无头模式下窗口尺寸默认是 800x600,很多响应式页面会渲染成手机版,定位元素时找不到,所以要配--window-size=1920,1080。
--no-sandbox和--disable-dev-shm-usage是容器环境的标配。前者绕过沙箱,后者把共享内存写到磁盘。不加这两个,在 Docker 里跑大概率报DevToolsActivePort file doesn't 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[type=submit]") 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 doesn't 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 或加入 PATH |
| DevToolsActivePort 不存在 | 容器沙箱限制/内存不足 | 加 --no-sandbox、--disable-dev-shm-usage |
| macOS 拒绝运行 | 文件带隔离属性 | xattr -d com.apple.quarantine |
| Permission denied | 缺少执行权限 | chmod +x |
| bad CPU type | 下载了错误架构的驱动 | M 系列芯片选 mac-arm64 |
| 元素定位报 stale | 缓存了 WebElement 对象 | 只存 By 元数据,现查现取 |
| 页面加载超时 | 默认加载策略等全部资源 | 改成 eager 加载策略 |
| 驱动文件凭空消失 | 被杀毒软件拦截 | 目录加白名单 |
| 脚本隔几天自动失效 | 浏览器后台自动升级 | 关自动更新或加版本校验 |
5.3 版本升级后的回归检查清单
浏览器一升级,我习惯按这个顺序过一遍,能挡住绝大多数后续问题:
第一步,重新获取 Chrome 完整版本号并和驱动版本比对,重点看主版本是否一致。第二步,跑一次chromedriver --version,确认驱动文件本身没被替换或者拦截。第三步,跑一个最小的打开页面并打印标题的脚本,把环境问题和业务代码问题切开。第四步,检查无头模式下的窗口尺寸,这一步经常出问题,新版 Chrome 渲染引擎小改一下,某些定位就会失效。第五步,跑完整回归,重点看那些依赖WebDriverWait超时时间的用例,浏览器性能变化会影响这些边界值。
这套流程走下来大概五分钟,比起线上任务半夜挂掉再爬起来排查,划算太多。我现在的做法是把前两步写进启动脚本,每次运行前自动校验,发现问题直接刷新驱动再启动,基本能做到无人值守。
最后分享一个我用了很久的习惯:把驱动的完整版本号和下载日期写进项目的 README,顺手记一行"当前 Chrome 主版本 XX"。团队里换人接手时,这一行字能省掉半天的摸索。踩过几次坑之后我越来越觉得,驱动这东西本身不复杂,复杂的只是信息不对称,把版本、平台、路径三件事一次性搞对,后面就都是顺水推舟的事了。