1. 为什么今天还要读 Selenium 3 的源码?——一个被低估的“过时”技术栈
很多人看到标题里的“Selenium 3”,第一反应是:都 2024 年了,Selenium 4 都发布三年多了,WebDriver W3C 标准也全面落地,还翻老黄历看 Selenium 3?是不是在浪费时间?我试过直接跳进 Selenium 4 的源码,结果卡在RemoteConnection和W3CHandshake两层抽象之间整整两天——不是不会用,而是根本不知道它为什么非得绕这么大一圈。后来我才明白:Selenium 3 是整个 WebDriver 协议演进的“分水岭式”版本,它第一次把“浏览器驱动通信”从硬编码的 JSON Wire Protocol(JWP)协议,拆解成可插拔的CommandExecutor+Request+Response三层模型。这个设计直接影响了后续所有主流自动化框架的架构逻辑。你今天在 Playwright 里看到的Channel、在 Cypress 里看到的Protocol层,甚至 Appium 的BaseDriver抽象,都能在 Selenium 3 的remote/目录下找到原型。这不是怀旧,是溯源。本文不讲怎么写driver.find_element(By.ID, "login"),而是带你站在selenium/webdriver/remote/这个目录下,看清整个通信链路的第一块砖是怎么垒起来的。如果你正在调试“为什么明明元素存在却报NoSuchElementException”,或者纳闷“为什么driver.get()要发两次 HTTP 请求”,又或者想搞懂DesiredCapabilities到底在哪个环节被转换成真正的启动参数——那这篇就是为你写的。它适合两类人:一类是写了三年自动化脚本、但对底层机制始终隔着一层纸的中级工程师;另一类是刚接触 Web 自动化、不想一上来就被封装好的WebDriverWait和expected_conditions带偏方向的初学者。我们不假设你熟悉 HTTP 协议细节,但会带你亲手抓包看POST /session请求体里capabilities字段的真实结构;我们不预设你了解 Python 的abc.ABC,但会解释为什么WebDriver类本身不继承RemoteWebDriver,而是一个工厂函数返回的实例。一切从源码开始,从部署环境开始,从最基础的from selenium import webdriver开始。
2. Selenium 的本质不是“浏览器控制库”,而是一套标准化的远程过程调用(RPC)协议实现
2.1 从“点开浏览器”到“发起一次 HTTP POST 请求”的完整映射
很多人误以为 Selenium 就是“让 Python 控制 Chrome”,这其实是个巨大的认知偏差。Selenium 3 的核心设计哲学是:WebDriver 是一个客户端,它不直接操作浏览器,而是通过标准 HTTP 接口,向一个独立运行的“浏览器驱动服务”发起远程调用。这个服务,就是 ChromeDriver、GeckoDriver 或 EdgeDriver。它们不是 Python 库的一部分,而是用 C++ 编写的独立可执行文件,监听本地某个端口(默认http://127.0.0.1:9515),接收符合 WebDriver 协议的 JSON 请求,并返回结构化的 JSON 响应。Selenium Python 绑定,本质上就是一个精心构造的 HTTP 客户端 SDK。
我们来看一个最简单的例子:driver = webdriver.Chrome()。这行代码背后发生了什么?它绝不是“启动 Chrome 进程”,而是:
- 启动一个独立的
chromedriver进程(如果未指定executable_path,则从PATH查找); chromedriver自动绑定到一个随机空闲端口(如9515),并启动一个内嵌的 HTTP 服务器;- Python 端的
Chrome类(继承自ChromiumDriver)初始化一个RemoteConnection实例,其remote_server_addr默认为"http://127.0.0.1:9515"; - 调用
start_session()方法,向http://127.0.0.1:9515/session发送一个POST请求,请求体是包含capabilities的 JSON; chromedriver收到后,解析 JSON,根据capabilities中的browserName、version、platform等字段,真正去fork()一个 Chrome 浏览器进程;- Chrome 启动成功后,
chromedriver返回一个包含sessionId的 JSON 响应,Python 端将其解析并保存为self.session_id。
提示:你可以用
ps aux | grep chromedriver看到它确实是一个独立进程;用curl -X POST http://127.0.0.1:9515/session -H "Content-Type: application/json" -d '{"desiredCapabilities": {"browserName": "chrome"}}'手动模拟这个过程,你会得到和 Python 一样的响应。这证明了 Selenium 的“客户端-服务端”分离本质。
2.2 JSON Wire Protocol(JWP)与 W3C WebDriver 协议的分水岭意义
Selenium 3 是 JWP 协议的最后一个主要支持版本。JWP 是 Selenium 团队早期自己定义的一套 RESTful API 规范,它的 URL 路径和请求体结构都带有明显的“Selenium 风格”。例如,创建会话的 endpoint 是/session,而获取元素的 endpoint 是/session/{session id}/element。它的请求体中,能力(capabilities)字段叫desiredCapabilities,这是一个典型的驼峰命名。
而 W3C WebDriver 协议(Selenium 4 及以后的默认协议)则是一个由 W3C 标准化组织制定的、更通用、更严格的规范。它的 endpoint 更加语义化,如/session保持不变,但获取元素变成了/session/{session id}/element(路径相同,但内部结构不同)。最关键的是,它的能力字段统一为capabilities(小写 c),并且要求必须是一个对象,而不是 JWP 中允许的任意 JSON 结构。
Selenium 3 的源码之所以值得深挖,是因为它内部实现了JWP 协议的完整客户端逻辑,并且其CommandExecutor抽象层已经为协议切换埋下了伏笔。你可以在selenium/webdriver/remote/remote_connection.py中找到RemoteConnection类,它的_commands字典里,清晰地定义了每一条命令对应的 URL 模板和 HTTP 方法:
_commands = { Command.STATUS: ('GET', '/status'), Command.NEW_SESSION: ('POST', '/session'), Command.GET_ALL_SESSIONS: ('GET', '/sessions'), Command.QUIT: ('DELETE', '/session/$sessionId'), Command.GET_CURRENT_WINDOW_HANDLE: ('GET', '/session/$sessionId/window_handle'), # ... 其他上百条命令 }这个_commands字典,就是整个 JWP 协议的“字典”。每一个Command.XXX常量,都对应着一个固定的字符串(如'newSession'),而这个字符串又作为 key,映射到一个(method, url)元组。当你调用driver.get("https://example.com")时,源码最终会走到RemoteWebDriver.get()方法,该方法内部会调用self.execute(Command.GET, {'url': url})。execute()方法再根据Command.GET在_commands中查到('POST', '/session/$sessionId/url'),然后拼接 URL、构造请求体、发送 HTTP 请求。
注意:
Command.GET这个常量名容易引起误解,它并不是 HTTP 的 GET 方法,而是 WebDriver 协议中“导航到 URL”这个操作的代号。真正的 HTTP 方法是POST,因为/session/$sessionId/url这个 endpoint 在 JWP 中定义为 POST。这种“协议命令名”与“HTTP 方法名”的分离,正是 Selenium 3 架构的精妙之处——它把协议语义和网络传输解耦了。
2.3 “WebDriver” 不是一个类,而是一个由工厂函数返回的、遵循特定接口的实例
这是 Selenium 3 源码中最反直觉,也最体现其设计思想的一点。在selenium/webdriver/__init__.py文件的末尾,你找不到class WebDriver:的定义。取而代之的是一长串的from .remote.webdriver import WebDriver as RemoteWebDriver,以及最关键的几行:
def __getattr__(name): if name in ['Chrome', 'Firefox', 'Safari', 'Opera', 'Edge', 'Ie']: from . import webdriver return getattr(webdriver, name) raise AttributeError(f"module 'selenium.webdriver' has no attribute '{name}'")这意味着,当你写下from selenium import webdriver,然后driver = webdriver.Chrome(),你调用的Chrome类,实际上来自selenium/webdriver/chrome/webdriver.py。而这个Chrome类,其父类是ChromiumDriver,再往上是RemoteWebDriver。RemoteWebDriver才是那个真正实现了所有find_element、get、quit等方法的基类。
但RemoteWebDriver本身并不直接处理“如何启动 Chrome”。它的__init__方法只做三件事:
- 初始化一个
RemoteConnection实例(负责 HTTP 通信); - 初始化一个
CommandExecutor实例(负责命令路由); - 调用
self.start_session(desired_capabilities, browser_profile),发起创建会话的请求。
真正的“浏览器特异性逻辑”,比如如何设置 Chrome 的启动参数(--headless,--disable-gpu)、如何查找chromedriver的路径、如何处理 Chrome 的options对象,全部封装在Chrome类自己的__init__方法里。它会先解析你传入的options,生成一个desired_capabilities字典,然后把这个字典连同RemoteConnection一起,交给RemoteWebDriver的__init__。
所以,WebDriver是一个“契约”,一个接口(Interface)。RemoteWebDriver是这个接口的一个通用实现,而Chrome、Firefox等则是针对不同浏览器的“适配器”。这种“面向接口编程”的思想,使得 Selenium 能够轻松支持新的浏览器驱动,只要它们遵循相同的 HTTP 协议即可。这也是为什么 Appium 能够复用大量 Selenium 的 Python 代码——因为它只是换了一个RemoteConnection的地址(指向http://127.0.0.1:4723/wd/hub),其余的命令执行逻辑完全一样。
3. 环境部署:不是简单pip install,而是理解三个独立组件的协同关系
3.1 三个必须独立安装、且版本必须严格匹配的组件
部署一个能正常工作的 Selenium 3 环境,绝不是pip install selenium==3.141.0就完事了。它涉及三个物理上完全独立、生命周期各自管理的组件,任何一个出错都会导致WebDriverException。它们是:
| 组件 | 类型 | 作用 | 安装方式 | 版本匹配关键点 |
|---|---|---|---|---|
| Selenium Python Client | Python 库 | 提供webdriver模块,封装 HTTP 客户端逻辑 | pip install selenium==3.141.0 | 必须与驱动的 JWP 协议版本兼容。Selenium 3.141.x 对应 JWP 的最终稳定版。 |
| Browser Driver (e.g., ChromeDriver) | 独立可执行文件 | 实现 WebDriver 协议的服务端,负责启动/控制真实浏览器 | 下载.zip解压,或用webdriver-manager | 必须与你本地安装的Chrome 浏览器主版本号严格一致。Chrome 114 需要 ChromeDriver 114.x。 |
| Browser (e.g., Google Chrome) | 操作系统应用 | 真正的渲染引擎和 JavaScript 运行时 | 系统包管理器或官网下载 | 主版本号(如 114)决定了它能接受哪个版本的 ChromeDriver 启动。 |
我踩过最深的坑,就是在一个 CI 环境里,apt-get install google-chrome-stable安装了 Chrome 115,但我手动下载的chromedriver_linux64.zip是 114 版本。结果driver = webdriver.Chrome()会抛出SessionNotCreatedException: session not created: This version of ChromeDriver only supports Chrome version 114。错误信息非常明确,但它指向的是“驱动不支持浏览器”,而不是“浏览器不支持驱动”。这提醒我们:驱动是“客户端”,浏览器是“服务端”,驱动的版本上限,由它所链接的浏览器决定。
3.2 手动部署全流程:从零开始,看清每一步的依赖
让我们抛弃所有自动化工具,手动走一遍部署流程,以彻底理解其内在逻辑。
第一步:确认并安装 Chrome 浏览器
# Ubuntu/Debian wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb sudo apt install ./google-chrome-stable_current_amd64.deb # 验证 google-chrome --version # 输出: Google Chrome 114.0.5735.198第二步:下载并配置 ChromeDriver
# 根据上一步的 Chrome 版本,去 https://chromedriver.chromium.org/ 查找对应驱动 # Chrome 114 对应 ChromeDriver 114.0.5735.90 wget https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip unzip chromedriver_linux64.zip # 将其移动到 PATH 下,或指定给 Python sudo mv chromedriver /usr/local/bin/ chmod +x /usr/local/bin/chromedriver # 验证 chromedriver --version # 输出: ChromeDriver 114.0.5735.90 (...)第三步:安装 Selenium Python Client
# 创建虚拟环境,隔离依赖 python3 -m venv selenium_env source selenium_env/bin/activate # 安装指定版本的 Selenium 3 pip install selenium==3.141.0 # 验证 Python 端是否能 import python -c "from selenium import webdriver; print('OK')"第四步:编写并运行一个最小验证脚本
# test_basic.py from selenium import webdriver from selenium.webdriver.chrome.options import Options # 1. 创建 Chrome 选项 chrome_options = Options() chrome_options.add_argument("--headless") # 无头模式,CI 环境必需 chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--disable-dev-shm-usage") # 2. 创建 WebDriver 实例 # 此时,Selenium 会尝试在 PATH 中查找 chromedriver driver = webdriver.Chrome(options=chrome_options) # 3. 执行一个最简单的操作 driver.get("https://www.python.org") print(driver.title) # 应该输出 "Welcome to Python.org" # 4. 清理 driver.quit()运行python test_basic.py。如果一切顺利,你会看到终端输出Welcome to Python.org。如果失败,请按以下顺序排查:
chromedriver是否在PATH中?which chromedriverchromedriver的版本是否与google-chrome --version的主版本号一致?selenium的版本是否为3.141.0?pip show seleniumgoogle-chrome是否真的能被chromedriver启动?chromedriver --version的输出里会显示它期望的 Chrome 路径,通常是/usr/bin/google-chrome,请确保该路径存在且可执行。
经验技巧:在 CI/CD 环境中,永远显式指定
executable_path,不要依赖PATH。因为 CI 环境的PATH可能很短,或者有多个版本的chromedriver冲突。正确的写法是:driver = webdriver.Chrome(executable_path="/usr/local/bin/chromedriver", options=chrome_options)。这样可以完全掌控依赖来源。
3.3webdriver-manager的工作原理与它为何不是“银弹”
webdriver-manager是一个非常流行的第三方库,它能自动下载并管理chromedriver。它的核心逻辑非常简单,却极其有效:
- 探测浏览器版本:它会调用
google-chrome --version(或firefox --version)来获取本地浏览器的主版本号。 - 查询映射表:它内置了一个庞大的 JSON 映射表(
webdriver_manager/core/constants.py),将 Chrome 主版本号(如114)映射到对应的chromedriver下载 URL(如https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip)。 - 下载与缓存:它会检查本地缓存目录(如
~/.wdm/drivers/)中是否已有该版本的驱动。如果没有,则下载、解压,并将路径返回给 Selenium。 - 注入路径:它返回的不是一个
Driver实例,而是一个str类型的路径。你需要把它传给webdriver.Chrome(executable_path=...)。
# 使用 webdriver-manager 的正确姿势 from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager # 这行代码会触发自动下载和缓存 service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service)它之所以不是“银弹”,是因为它无法解决所有问题:
- 权限问题:在某些受限的 CI 环境中,
webdriver-manager可能没有权限写入~/.wdm/目录。 - 网络问题:它需要访问
chromedriver.storage.googleapis.com,在某些网络环境下可能超时。 - 版本滞后:它的映射表更新可能比 Chrome 的正式发布慢几个小时,导致新版本 Chrome 发布后,
webdriver-manager还无法识别。
因此,我的经验是:在开发机上,用webdriver-manager提高效率;在生产/CI 环境中,坚持手动下载、校验、固定路径。后者虽然繁琐,但带来了 100% 的可预测性和稳定性。
4. 源码探析实战:从webdriver.Chrome()到第一个 HTTP 请求的逐行追踪
4.1 路径总览:selenium包的物理结构与核心模块定位
在开始追踪之前,我们必须清楚 Selenium 3 的源码是如何组织的。安装selenium==3.141.0后,其源码位于 Python 的site-packages目录下。核心模块的物理路径如下:
selenium/__init__.py: 包的入口,定义了__all__和__getattr__。selenium/webdriver/__init__.py: 导出Chrome,Firefox等类的顶层模块。selenium/webdriver/chrome/__init__.py: 导出Chrome类和Options类。selenium/webdriver/chrome/webdriver.py:Chrome类的定义,继承自ChromiumDriver。selenium/webdriver/chromium/__init__.py:ChromiumDriver的基类定义。selenium/webdriver/remote/__init__.py: 导出WebDriver(即RemoteWebDriver)类。selenium/webdriver/remote/webdriver.py:RemoteWebDriver类的定义,这是所有浏览器驱动的共同基类。selenium/webdriver/remote/remote_connection.py:RemoteConnection类的定义,负责所有 HTTP 通信。selenium/webdriver/remote/command.py:Command常量的定义,即那个_commands字典的源头。
这个结构清晰地体现了 Selenium 的分层思想:chrome/目录处理 Chrome 特有的逻辑;remote/目录处理所有驱动共有的、与协议相关的逻辑;webdriver/顶层目录则负责对外提供统一的 API。
4.2 第一行webdriver.Chrome()的源码之旅
现在,我们打开selenium/webdriver/chrome/webdriver.py,找到Chrome类的定义:
class Chrome(ChromiumDriver): def __init__(self, executable_path="chromedriver", port=0, options=None, service_args=None, desired_capabilities=None, service_log_path=None, chrome_options=None, keep_alive=True): ... # 关键:这里会创建一个 ChromiumService service = ChromiumService( executable_path=executable_path, port=port, service_args=service_args, log_path=service_log_path ) # 关键:这里会创建一个 ChromeOptions 实例,并合并用户传入的 options options = options or ChromeOptions() if chrome_options: warnings.warn('use options instead of chrome_options', DeprecationWarning, stacklevel=2) options = chrome_options # 关键:调用父类 ChromiumDriver 的 __init__ super().__init__( executable_path=executable_path, port=port, options=options, service_args=service_args, desired_capabilities=desired_capabilities, service_log_path=service_log_path, keep_alive=keep_alive, service=service )Chrome类的__init__方法,主要做了三件事:准备service(驱动服务)、准备options(启动参数)、然后调用父类ChromiumDriver的__init__。ChromiumDriver的__init__又会继续调用RemoteWebDriver的__init__。所以,真正的初始化逻辑,最终都汇聚到了selenium/webdriver/remote/webdriver.py的RemoteWebDriver.__init__方法中。
我们跳转到RemoteWebDriver.__init__:
def __init__(self, command_executor='http://127.0.0.1:4444/wd/hub', desired_capabilities=None, browser_profile=None, proxy=None, keep_alive=False, file_detector=None, options=None): # 1. 创建 RemoteConnection 实例 self.command_executor = command_executor if isinstance(command_executor, (str, bytes)): self.command_executor = RemoteConnection( remote_server_addr=command_executor, keep_alive=keep_alive) # 2. 创建 CommandExecutor 实例 self._commands = self.command_executor._commands # 3. 初始化 session_id 等属性 self.session_id = None self.capabilities = {} self._is_remote = True # 4. 关键:启动会话! self.start_session(desired_capabilities, browser_profile)这就是整个链条的“心脏”。self.start_session(...)是一切的起点。我们继续追踪这个方法:
def start_session(self, capabilities, browser_profile=None): # 1. 如果传入了 options,就用它来生成 capabilities if not isinstance(capabilities, dict): capabilities = dict(capabilities) # 2. 合并 browser_profile(已废弃,忽略) # 3. 最关键的一步:调用 execute() 方法,执行 NEW_SESSION 命令 response = self.execute(Command.NEW_SESSION, { 'desiredCapabilities': capabilities, 'requiredCapabilities': {}, }) # 4. 解析响应 self.session_id = response['sessionId'] self.capabilities = response['value']start_session方法的核心,就是调用self.execute(Command.NEW_SESSION, {...})。Command.NEW_SESSION是一个字符串常量,定义在selenium/webdriver/remote/command.py中,其值为'newSession'。execute方法会根据这个字符串,在self._commands字典中查到对应的(method, url),然后构造 HTTP 请求。
4.3execute()方法:Selenium 的“协议翻译器”
execute方法是RemoteWebDriver类中最重要的方法,它是整个 WebDriver 协议的“翻译中枢”。我们来看它的简化版逻辑:
def execute(self, driver_command, params=None): # 1. 根据 driver_command(如 'newSession')查找对应的 (method, url) command_info = self._commands[driver_command] assert len(command_info) == 2 command, path = command_info # 2. 将 $sessionId 等占位符替换成实际值 if self.session_id and '$sessionId' in path: path = path.replace('$sessionId', self.session_id) # 3. 构造完整的 URL url = '%s%s' % (self.command_executor._url, path) # 4. 准备请求体(payload) data = json.dumps(params) # 5. 发起 HTTP 请求(这才是真正的网络调用!) response = self.command_executor._request( method=command, url=url, data=data ) # 6. 解析响应 return response.get('value')这个方法的精妙之处在于,它把“协议命令”(Command.NEW_SESSION)和“网络传输”(_request)完全解耦了。_request方法属于RemoteConnection类,它才是真正使用urllib3或requests库发送 HTTP 请求的地方。而execute方法,只负责“翻译”——把一个高层的、语义化的命令,翻译成一个底层的、具体的 HTTP 请求。
我们可以用一个真实的抓包来验证这一点。在运行test_basic.py时,开启tcpdump或Wireshark,过滤port 9515,你会看到一个清晰的 HTTP 流:
POST /session HTTP/1.1 Host: 127.0.0.1:9515 Content-Length: 228 Content-Type: application/json;charset=UTF-8 {"desiredCapabilities":{"browserName":"chrome","version":"","platform":"ANY","javascriptEnabled":true,"cssSelectorsEnabled":true,"takesScreenshot":true,"nativeEvents":true,"rotatable":true},"requiredCapabilities":{}}这个请求体,就是execute方法中json.dumps(params)的结果。params字典中的desiredCapabilities键,正是start_session方法传入的那个字典。而browserName的值"chrome",则来自于Chrome类在初始化时,通过options对象推导出来的。
经验技巧:如果你想深度调试 Selenium 的通信过程,最有效的方法不是在 Python 代码里打
RemoteConnection._request方法里加日志。你可以在selenium/webdriver/remote/remote_connection.py的_request方法开头,加入print(f"Sending {method} to {url}: {data}")。这样,你就能看到每一个 WebDriver 命令背后,真实的 HTTP 请求是什么样子。这是理解整个框架最直接、最有效的方式。
5. 常见部署陷阱与源码级排错指南
5.1 陷阱一:“Message: session not created” —— 从错误堆栈反推根因的完整过程
这个错误是 Selenium 部署中最常见的“拦路虎”,但它的堆栈信息往往非常模糊,只告诉你“会话创建失败”,却不告诉你具体哪里失败了。让我们用源码级的思路,一步步拆解它。
典型错误信息:
selenium.common.exceptions.SessionNotCreatedException: Message: session not created from invalid argument: can't parse capabilities from invalid argument: cannot parse capability: goog:chromeOptions from invalid argument: unrecognized chrome option: headless排错步骤:
定位错误源头:这个异常是在
start_session方法中,self.execute(...)返回的响应里response.get('value')为空,或者response本身是一个错误响应(response.get('error') is not None)时抛出的。它最终会调用ErrorHandler.check_response(response)来解析错误。查看
ErrorHandler:在selenium/webdriver/remote/errorhandler.py中,check_response方法会根据response.get('status')和response.get('value', {}).get('message')来构造具体的异常。上面的错误信息,就来自response.get('value').get('message')。理解错误含义:
unrecognized chrome option: headless这个提示非常关键。它说明chromedriver服务端不认识headless这个选项。但这怎么可能?--headless是 Chrome 59+ 就支持的特性。原因只有一个:你本地的chromedriver版本太老,不支持新版本 Chrome 的新选项。例如,Chrome 114 引入了--headless=new模式,而一个老旧的 ChromeDriver 100 只认识--headless=old。验证与修复:回到命令行,运行
chromedriver --version和google-chrome --version,对比主版本号。如果它们不一致,或者chromedriver的版本明显低于google-chrome,那么解决方案就是下载匹配的chromedriver。
提示:
chromedriver的版本号格式是MAJOR.MINOR.BUILD.PATCH,而google-chrome的版本号是MAJOR.MINOR.BUILD.PATCH。必须保证 MAJOR(主版本号)完全一致。MINOR 可以不同,但最好也保持一致。
5.2 陷阱二:“Message: unknown error: DevToolsActivePort file doesn't exist” —— Chrome 启动失败的深层原因
这个错误通常出现在 Linux 无头环境中,它表明chromedriver成功启动了 Chrome 进程,但 Chrome 进程在启动后,未能成功创建用于 DevTools 通信的DevToolsActivePort文件。
源码级分析:
chromedriver在启动 Chrome 时,会通过--remote-debugging-port=XXXX参数告诉 Chrome 开启一个调试端口。- Chrome 启动后,会在其临时数据目录下创建一个名为
DevToolsActivePort的文件,里面记录了实际的调试端口和 WebSocket 地址。 chromedriver会轮询这个文件,直到它出现,然后才认为 Chrome 启动成功。
常见原因与修复:
- 缺少沙箱支持:在容器或某些 Linux 发行版中,Chrome 的 sandbox 机制可能被禁用。解决方案是添加
--no-sandbox参数。 - 共享内存不足:Chrome 需要
/dev/shm有足够的空间。解决方案是添加--disable-dev-shm-usage参数。 - 缺少字体库:Chrome 渲染页面需要一些系统字体。解决方案是安装
fonts-liberation或ttf-dejavu。
这些参数,都应该通过ChromeOptions添加,而不是作为chromedriver的参数。因为chromedriver只负责启动 Chrome,而这些是 Chrome 自己的启动参数。
chrome_options = Options() chrome_options.add_argument("--headless") chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--disable-dev-shm-usage") # 这些都是 Chrome 的参数,不是 chromedriver 的5.3 陷阱三:pip install selenium安装了错误的版本 —— 如何强制锁定与验证
在大型项目中,requirements.txt里只写selenium而不写版本,会导致pip install总是拉取最新版(可能是 Selenium 4)。而 Selenium 4 默认使用 W3C 协议,与旧的 JWP 驱动不兼容。
强制锁定版本:
# requirements.txt selenium==3.141.0验证安装版本:
pip show selenium | grep Version # 输出: Version: 3.141.0源码级验证:进入 Python 解释器,检查Command常量:
>>> from selenium.webdriver.remote.command import Command >>> Command.NEW_SESSION 'newSession' >>> Command.GET 'get'在 Selenium 4 中,Command.NEW_SESSION的值是'session'(小写 s),而Command.GET的值是'get'(与 3 相同,但其他命令有变化)。这个细微差别,是区分 JWP 和 W3C 协议客户端的最可靠方式。
我在实际项目中发现,最稳妥的部署策略是:在 CI 的Dockerfile中,明确写出RUN pip install selenium==3.141.0 && apt-get install -y google-chrome-stable && wget ... && unzip ... && mv chromedriver /usr/local/bin/。每一行都精确控制,杜绝任何不确定性。自动化是好东西,但在关键基础设施上,确定性永远比便利性更重要。