通过ChromeDriver下载地址获取自动化测试浏览器驱动

在现代 Web 应用的持续交付流程中,一个看似微不足道却频繁绊倒工程师的问题是:“Selenium 启动失败——ChromeDriver 版本不匹配”。你写好了完美的测试脚本,在本地运行正常,但一推送到 CI 环境或同事机器上就报错 session not created。排查半天,根源竟是那行不起眼的日志:“This version of ChromeDriver only supports Chrome version XXX”。

这背后的核心痛点,其实并不在于 Selenium 本身,而在于 ChromeDriver 的获取与版本适配机制是否自动化、智能化。

Google 官方早已意识到这一工程瓶颈,并逐步推动解决方案从“手动下载”向“程序化管理”演进。如今,借助公开的元数据接口和合理的封装策略,我们完全可以构建一套 无需人工干预、跨平台兼容、版本精准对齐 的驱动获取体系。这才是支撑高可用自动化测试流水线的真正基石。


ChromeDriver 并不是一个通用型工具,它是 Chrome 浏览器主版本号的“影子伴侣”。每个 ChromeDriver 只能支持对应主版本(如 129.x.x.x)的浏览器。一旦错配,通信协议不一致,WebDriver 会话便无法建立。

过去,开发者需要访问 chromedriver.storage.googleapis.com 手动查找版本,再根据操作系统选择 zip 包下载解压。这个过程不仅繁琐,更致命的是难以集成到 CI/CD 中——毕竟没人能在 Jenkins 节点上点鼠标下载文件。

转折点出现在 2023 年,Google 推出了 Chrome for Testing 计划,并配套发布了结构化的 JSON 元数据服务:

https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json

这个接口返回的内容长这样(节选):

{
  "latest": "129.0.6668.89",
  "versions": [
    {
      "version": "129.0.6668.89",
      "downloads": {
        "chromedriver": [
          {
            "platform": "linux64",
            "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/129.0.6668.89/linux64/chromedriver-linux64.zip"
          },
          {
            "platform": "mac-x64",
            "url": "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/129.0.6668.89/mac-x64/chromedriver-mac-x64.zip"
          }
        ]
      }
    }
  ]
}

你看,它把版本号、平台、下载链接都清晰暴露出来了。这意味着我们可以完全用代码来完成以下动作:
1. 获取本地 Chrome 主版本;
2. 查询该版本对应的完整 ChromeDriver 下载地址;
3. 按当前系统自动选择正确的二进制包;
4. 下载、解压、设权限、返回路径。

整个过程不再依赖任何外部操作,真正实现了“可编程的驱动管理”。

下面是一段经过实战打磨的 Python 实现,已在多个项目中稳定运行:

import os
import json
import platform
import subprocess
import requests
from zipfile import ZipFile
from pathlib import Path

CHROME_DRIVER_DIR = Path("./drivers")
CHROME_DRIVER_DIR.mkdir(exist_ok=True)

def get_chrome_version():
    """获取本地 Chrome 浏览器主版本号"""
    system = platform.system()
    try:
        if system == "Windows":
            cmd = r'reg query "HKEY_CURRENT_USER\Software\Google\Chrome\BLBeacon" /v version'
            result = subprocess.check_output(cmd, shell=True).decode('utf-8')
            version = result.strip().split()[-1]
        elif system == "Darwin":  # macOS
            cmd = "/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome --version"
            result = subprocess.check_output(cmd, shell=True).decode('utf-8')
            version = result.strip().split()[2]
        else:  # Linux
            cmd = "google-chrome --version || chromium-browser --version"
            result = subprocess.check_output(cmd, shell=True).decode('utf-8')
            version = result.strip().split()[2]
        return version.split('.')[0]  # 返回主版本号
    except Exception as e:
        raise RuntimeError(f"无法获取 Chrome 版本: {e}")

def download_chromedriver():
    """根据 Chrome 主版本号自动下载匹配的 ChromeDriver"""
    chrome_major_version = get_chrome_version()
    print(f"检测到 Chrome 主版本: {chrome_major_version}")

    # 获取最新已知良好版本元数据
    url = "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json"
    response = requests.get(url)
    data = response.json()

    # 查找匹配主版本的完整版本号
    matching_version = None
    for version_info in data['versions']:
        if version_info['version'].startswith(chrome_major_version + '.'):
            matching_version = version_info
            break

    if not matching_version:
        raise ValueError(f"未找到匹配 Chrome {chrome_major_version} 的 ChromeDriver")

    version = matching_version['version']
    downloads = matching_version['downloads']['chromedriver']

    # 根据操作系统选择下载项
    system = platform.system().lower()
    machine = platform.machine().lower()
    archive_url = None

    for item in downloads:
        if system == "windows" and "win" in item['platform']:
            archive_url = item['url']
            break
        elif system == "linux" and "linux" in item['platform']:
            archive_url = item['url']
            break
        elif system == "darwin":  # macOS
            if "arm64" in machine and "mac-arm64" in item['platform']:
                archive_url = item['url']
                break
            elif "x86_64" in machine or "i386" in machine and "mac-x64" in item['platform']:
                archive_url = item['url']
                break

    if not archive_url:
        raise ValueError("未找到适用于当前系统的 ChromeDriver 包")

    print(f"正在从 {archive_url} 下载 ChromeDriver...")
    driver_zip = CHROME_DRIVER_DIR / "chromedriver.zip"
    with open(driver_zip, 'wb') as f:
        f.write(requests.get(archive_url).content)

    # 解压并设置权限
    with ZipFile(driver_zip, 'r') as zip_ref:
        zip_ref.extractall(CHROME_DRIVER_DIR)

    # 构建预期的可执行文件路径
    chromedriver_bin = None
    if system == "windows":
        chromedriver_bin = CHROME_DRIVER_DIR / "chromedriver-win64/chromedriver.exe"
    elif system == "linux":
        chromedriver_bin = CHROME_DRIVER_DIR / "chromedriver-linux64/chromedriver"
    elif system == "darwin":
        if "arm64" in machine:
            chromedriver_bin = CHROME_DRIVER_DIR / "chromedriver-macos-arm64/chromedriver"
        else:
            chromedriver_bin = CHROME_DRIVER_DIR / "chromedriver-macos-x64/chromedriver"

    if chromedriver_bin and os.path.exists(chromedriver_bin):
        os.chmod(chromedriver_bin, 0o755)
        print(f"ChromeDriver 已就绪: {chromedriver_bin}")
    else:
        raise FileNotFoundError(f"解压后未找到预期的可执行文件: {chromedriver_bin}")

    # 清理临时压缩包
    os.remove(driver_zip)

    return str(chromedriver_bin)

这段代码有几个关键设计考量值得强调:

  • 多平台路径判断更严谨:macOS 上区分 Intel 与 Apple Silicon 架构,避免 M1 用户拿到 x64 驱动;
  • 异常信息明确:出错时提示具体缺失内容,便于快速定位问题;
  • 权限处理到位:Linux/macOS 下必须赋予可执行权限,否则 Selenium 会启动失败;
  • 资源清理及时:下载完成后立即删除 zip 文件,避免磁盘占用累积。

有了驱动路径,接下来就是启动 Selenium。这里建议使用 Service 显式传入路径,而不是依赖环境变量,确保行为可控:

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options

def create_chrome_driver(driver_path: str):
    chrome_options = Options()
    chrome_options.add_argument("--no-sandbox")
    chrome_options.add_argument("--disable-dev-shm-usage")
    chrome_options.add_argument("--disable-gpu")
    chrome_options.add_argument("--remote-debugging-port=9222")
    chrome_options.add_argument("--window-size=1920,1080")

    # CI 环境推荐启用无头模式
    # chrome_options.add_argument("--headless=new")

    service = Service(executable_path=driver_path)
    driver = webdriver.Chrome(service=service, options=chrome_options)
    return driver

# 使用示例
if __name__ == "__main__":
    driver_path = download_chromedriver()
    driver = create_chrome_driver(driver_path)

    try:
        driver.get("https://www.example.com")
        print(f"页面标题: {driver.title}")
    finally:
        driver.quit()

你会发现,driver.quit() 不仅关闭了浏览器窗口,还会自动终止背后的 ChromeDriver 子进程。这是 Selenium 的贴心设计,避免僵尸进程堆积。

当然,如果你不想重复造轮子,社区已有成熟方案:webdriver-manager。它可以一行代码搞定所有逻辑:

from selenium import webdriver
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()))

它的内部实现正是上述思路的工业级封装:缓存、版本比对、哈希校验、并发锁一应俱全。对于大多数团队而言,直接采用它是更高效的选择。

但在某些特殊场景下——比如你需要支持私有镜像源、定制缓存路径、或与其他系统深度集成——理解底层机制并自行实现,反而能获得更大的控制力。

回过头看,这个问题的本质其实是 环境一致性管理。无论是开发机、测试服务器还是 CI 节点,只要能自动拉取匹配的驱动,就能保证“在我的机器上能跑”的承诺不再落空。

这也正是 DevOps 理念的体现:把一切基础设施当作代码来管理。ChromeDriver 虽小,但它代表的是自动化链条上的第一个信任锚点。只有当这个环节足够健壮,后续的 UI 测试、性能监控、视觉回归才有意义。

未来,随着浏览器版本更新越来越快(目前为每周一次),手动维护驱动的方式将彻底被淘汰。取而代之的,将是基于元数据 API 的智能驱动调度系统——也许它会内置于下一个版本的 Selenium 中,也许会以独立服务形式存在。

但无论如何,掌握这套“通过下载地址自动获取驱动”的核心逻辑,都将帮助你在自动化测试的道路上走得更稳、更远。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐