1. 无头浏览器是什么,chromedriver在其中扮演什么角色
先从一个实际场景说起。我有一次临时要写一个定时任务,每天凌晨去某个数据平台抓取最新的行情表,不需要弹出浏览器窗口,服务器上也没有显示器,但页面里的表格是用JavaScript异步渲染的,直接requests根本拿不到渲染后的HTML。当时第一反应就是用无头浏览器,配合chromedriver把事情办了。
无头浏览器,说白了就是一个不带界面、不显示窗口的浏览器内核。它照样能解析HTML、执行JavaScript、加载CSS、发Ajax请求,只是不把画面渲染给你看。真正跑起来之后,你感知不到它的存在,但它做的事情和普通浏览器几乎一模一样。
chromedriver又叫ChromeDriver,是Google官方提供的一个独立服务程序。它实现了W3C的WebDriver标准,负责在Selenium这类自动化框架和Chrome浏览器之间搭桥。当你的代码调用Selenium时,Selenium会把操作指令发给chromedriver,chromedriver再把指令翻译成Chrome能理解的底层控制信号,驱动浏览器去执行点击、输入、跳转、截屏、抓取数据等操作。简单理解,chromedriver就是Python代码和Chrome浏览器之间的翻译官。
那为什么不能用Python代码直接控制Chrome?因为Chrome的进程间通信协议极其复杂,涉及DevTools Protocol、多进程架构、沙箱机制等一大堆底层设计,普通开发者没必要直接面对这些。chromedriver把这层复杂度全部封装好,暴露出一套相对简单的HTTP接口。你在代码里写driver.get(url),实际上是Selenium向chromedriver发了一个HTTP请求,chromedriver再通过Chrome DevTools Protocol与浏览器实例通信,整个过程在几十毫秒内完成。
这套架构适用的场景很广:自动化测试、定时爬虫、网页截图、性能监控、表单批量提交、无头环境下的页面渲染,都是它的用武之地。对于刚开始接触自动化的人来说,也是入门门槛相对较低的一条路,不需要你了解浏览器内部机制,只要会写Python基本语法就能跑起来。
2. chromedriver与浏览器的版本匹配问题
按正常流程,装完Python的selenium库,再下载一个chromedriver放到系统PATH里,基本就能跑了。但这里有个最常见的坑,也是很多新手踩进去就出不来的地方,那就是chromedriver版本和Chrome浏览器版本必须严格匹配。
chromedriver和Chrome的版本号是强绑定关系。比如Chrome是108版本,你最好用108版本的chromedriver;Chrome升级到146了,你还在用108的chromedriver,启动时就会报版本不匹配错误,最常见的是session not created或This version of ChromeDriver only supports Chrome version XX。版本不匹配时,chromedriver无法正常工作,因为两者之间的内部协议和接口参数会随版本调整而发生变化。
具体怎么确认当前Chrome的版本?有两个办法。本地有图形界面的话,打开Chrome,点右上角菜单,进入“关于Chrome”,就能看到当前版本号。服务器上没图形界面的话,可以执行命令:
google-chrome --version或者:
chromium --version输出结果大概是这样的:Google Chrome 146.0.6214.76。你需要把主版本号记住,然后去下载对应主版本的chromedriver。主版本号匹配基本就能用,比如146.0.6214.76对应chromedriver 146,不要纠结后面那串小版本号,一般不需要完全一致。
下载地址很多人找不到官方渠道,这里直接说清楚。Google官方维护了一个Chrome for Testing的下载页面,里面列出了所有chromedriver历史版本。如果你用的是Linux系统,下载那个linux64的压缩包;Windows系统就下win32版本;macOS根据芯片选mac-x64或mac-arm64。下载解压后,里面只有一个chromedriver可执行文件,把它放到一个固定目录,再把目录加到PATH环境变量里,或者直接在代码里指定路径,都可以。
有一个比较新的方式值得推荐,就是使用Chrome for Testing自己携带的chromedriver,它是Google专门为自动化测试场景推出的版本,版本号对应关系更明确,避免了很多匹配上的混乱。
这里还要多说一句,不要用apt或yum直接安装chromedriver,那些仓库里带的版本往往跟不上Chrome的更新节奏。比如系统Chrome已经升到146了,apt源里的chromedriver还停留在120,装完必报错。手动下载虽然麻烦一点,但版本可控,排查问题也容易。
3. 实操:Python + Selenium + chromedriver实现无头抓取
理论说得再多,不如跑一个真实示例。下面这套代码我在多个项目里用过,稍微调整一下就能用。
3.1 基础环境准备
先装Python依赖:
pip install selenium然后确认chromedriver已经放到可执行路径里。验证方法很简单,在终端执行:
chromedriver --version如果能看到类似ChromeDriver 146.0.6214.76的输出版本号,说明环境OK。
3.2 启动无头浏览器
这是最核心的一段代码,我加了详细注释:
from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 创建配置对象 options = Options() # 开启无头模式,这是关键选项 options.add_argument('--headless=new') # 禁用GPU加速,无头模式下GPU没有意义,还容易引发报错 options.add_argument('--no-sandbox') # 解决无头模式下部分服务器缺少Chrome依赖库的问题 options.add_argument('--disable-dev-shm-usage') # 隐藏自动化控制标识,减少被网站检测的风险 options.add_argument('--disable-blink-features=AutomationControlled') # 设置一个真实的User-Agent,避免被识别为爬虫 options.add_argument( 'user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' 'AppleWebKit/537.36 (KHTML, like Gecko) ' 'Chrome/146.0.0.0 Safari/537.36' ) # 初始化driver driver = webdriver.Chrome(options=options) # 打开目标页面 driver.get('https://example.com') # 等待页面中的某个元素出现,避免页面还没渲染完就去抓数据 try: element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, 'content')) ) print('页面加载完成') except Exception as e: print('等待超时:', e) # 获取页面标题 print('标题:', driver.title) # 获取渲染后的完整HTML html = driver.page_source print('页面源码长度:', len(html)) # 关闭浏览器 driver.quit()这段代码有几个关键点,我逐一说为什么这么写。
--headless=new是Chrome 109版本之后的新无头模式参数。早期的--headless参数在新版本里虽然还能用,但Google官方已经建议换成新版无头模式,功能更接近完整浏览器,对JavaScript的支持也更全面。
--no-sandbox这个参数在无头模式下基本是必加的。原因是Chrome在Linux系统上默认启用沙箱机制,而很多服务器环境没有配置专门的沙箱用户组,导致启动失败,报错信息类似Failed to move to new namespace。加上这个参数直接禁用沙箱,虽然安全性会降低一点,但在自动化测试场景下可以接受。
--disable-dev-shm-usage是专门为解决/dev/shm空间不足的问题。有些容器环境把/dev/shm限制得很小,只有64MB左右,Chrome默认会在这个目录下创建共享内存文件,空间不够就崩溃。加上这个参数后,Chrome会改用/tmp目录,空间问题就解决了。
为什么无头模式能代替传统爬虫?因为它太接近真实浏览器了。requests只能拿到服务器返回的原始HTML,页面里那些靠JavaScript动态生成的表格、图表、下拉菜单,requests根本看不到。chromedriver驱动的是完整Chrome内核,所有脚本都执行完了,你再去拿page_source,拿到的就是最终结果。
3.3 无头模式下的截图
除了抓数据,截图也是高频需求,尤其是做页面监控的时候。无头模式下截图特别轻量,不占显示器资源,很适合做定时任务。代码很简单:
# 设置窗口大小 driver.set_window_size(1920, 1080) # 截取全页面 driver.save_screenshot('page_full.png') # 截取某个元素 element = driver.find_element(By.ID, 'chart') element.screenshot('chart.png')这里有个细节,无头模式下如果不设置窗口大小,Chrome会使用默认的800x600,很多页面在小分辨率下会走移动端适配,布局完全不一样,截出来的图没法看。所以截图前务必先设置窗口尺寸。
3.4 处理动态加载内容
很多网站现在都做了滚动加载,页面往下滑才会加载更多数据。这种场景用Selenium也好处理,模拟滚动就行:
# 滚动到页面底部,触发懒加载 for i in range(5): driver.execute_script('window.scrollTo(0, document.body.scrollHeight)') time.sleep(2)execute_script可以执行任意JavaScript代码,这是Selenium里非常强悍的一个接口。滚动、获取元素属性、修改页面样式、触发事件,凡是用普通接口不好实现的,都可以用JavaScript来解决。
4. 国产操作系统上的兼容性现状与技术原因
回到标题的重点,为什么说目前chromedriver不支持国产操作系统?这个话题我在实际项目里体会很深。
国产操作系统,比如麒麟、统信UOS这些,本质上都是基于Linux内核构建的。既然Linux都支持,chromedriver理论上应该也能跑。但问题出在浏览器本身。国产操作系统自带的浏览器,通常是基于Firefox或Chromium源码定制的国产浏览器,它们为了适配本系统可能修改了很多底层配置,导致chromedriver无法识别。具体来说有几个层面的问题。
第一层,浏览器类型不匹配。chromedriver只能驱动真正原版的Chrome,或者说Google官方认证的Chromium版本。国产操作系统的浏览器虽然内核是Chromium,但做深度定制后,内部接口、版本标识、参数处理都会发生变化。chromedriver启动时会检查目标进程的版本信息,对不上就直接拒绝。这不是你的代码写得有问题,而是驱动和浏览器根本不认。
第二层,系统库依赖问题。chromedriver和Chrome都有大量动态链接库依赖,比如libnss3、libgbm、libasound等。国产操作系统往往精简了这些依赖库,或者使用了自己定制的版本。直接运行chromedriver,经常报缺少lib文件,或者报GLIBC版本不兼容。GLIBC是Linux系统最核心的C运行库,不同系统版本携带的版本号差别很大。chromedriver要求GLIBC_2.27以上,而某些国产系统还停留在2.17,程序根本跑不起来。
第三层,处理器架构差异。现在很多国产操作系统运行在ARM架构的处理器上,比如飞腾、鲲鹏这些。chromedriver和Chrome的官方构建版本只提供x86_64架构的Linux二进制文件,并没有官方ARM版本。在ARM环境里跑不了,这是架构层面的硬限制。
不要心急,这里说的是官方打包版不提供ARM二进制。但你仍然可以找到一些社区或者第三方编译的chromium-arm版本,比如Debian的仓库里就维护了arm64的chromium。配合同样从源码编译的chromedriver,是有机会跑通的。还有一点是很多人在国产系统上直接用Selenium自带的驱动管理器——webdriver-manager库来解决问题,它可以自动检测浏览器版本下载对应驱动,能在一定程度上减少因驱动和浏览器不匹配带来的麻烦。
我当时在神州鲲鹏服务器上加装了一层Ubuntu兼容层,然后在此兼容层里安装了原版Chromium和官方chromedriver,这些操作在OpenEuler系统里是支持的。但大多数还在用麒麟系统的场景,这个方案不一定能顺利走通。
第四层,权限与锁机制。国产操作系统为了软件安全,推出了各类权限管理机制,包括对可执行程序的运行控制、对文件目录的访问限制、对网络连接的监控策略等。chromedriver需要在系统上创建临时目录、写日志文件、建立本地网络服务,这些操作在默认安全策略下可能被拦截。
所以你能在国产操作系统里用国产浏览器吗?能用。很多国产浏览器自身就带有自动化测试模块,或者提供了专门的调试接口。但那是各自封闭的方案,和Selenium生态完全不通用,需要重新学习一套接口,维护成本太高。
我个人的建议是,如果你的爬虫或自动化测试项目必须跑在国产操作系统上,优先考虑换驱动协议,不要死磕chromedriver。比如改用geckodriver来驱动Firefox,或者直接用Playwright的浏览器池方案。Playwright的使用方式比Selenium更现代一点,接口也更简洁,在Linux环境下的适配性更好。
这里给一个改造思路,如果你有Selenium + chromedriver的存量代码,想迁移到Firefox上,核心改动并不大。把之前初始化Chrome驱动的那一段,替换成下面的写法:
from selenium import webdriver from selenium.webdriver.firefox.options import Options firefox_options = Options() firefox_options.add_argument('--headless') driver = webdriver.Firefox(options=firefox_options)Python侧的业务逻辑几乎不用改,因为Selenium的WebDriver API是统一的,By.ID、By.XPATH这些定位方式都通用。
再补充一个冷门但有用的点。国产系统里的Google Chrome浏览器本身也自带headless能力,它不需要chromedriver就能以无头方式直接截图和导出HTML。这种方式的限制在于无法从Selenium侧精细控制页面交互,只能解决部分静态页面的渲染问题。如果只是需要一个简单的页面快照服务,这是一个零成本方案。
5. 常见问题与排查技巧实录
写代码的过程就是踩坑的过程。这里把我在chromedriver使用中遇到过的高频问题整理成一个速查表,每个问题都有对应的解决思路。
5.1 chromedriver版本不匹配
报错信息:
SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 108 Current browser version is 146.0.6214.76解决办法:去官方页面下载与当前浏览器主版本一致的chromedriver,例如浏览器是146,就下载146的chromedriver。或者先用--disable-search-engine-choice-screen这类参数绕过,但不建议,治标不治本。
5.2 等待超时,元素定位不到
报错信息:
TimeoutException: Message: timeout: Timed out waiting for driver server to start这个报错表面上看起来是启动超时,但根因往往是chromedriver自身没有执行权限,或者文件的所属用户不对。在Linux上检查一下文件权限:
chmod +x /usr/local/bin/chromedriver排查顺序推荐如下:
| 排查步骤 | 操作 | 预期结果 |
|---|---|---|
| 1. 确认driver路径 | which chromedriver | 输出路径,不报错 |
| 2. 确认版本匹配 | chromedriver --version | 输出版本号 |
| 3. 确认执行权限 | ls -l /path/to/chromedriver | 有x权限标记 |
| 4. 测试浏览器启动 | google-chrome --version | 正常输出版本 |
还有一个常见情况是定位元素时超时。异步渲染的页面,数据可能要等好几秒才出现。解决办法是使用WebDriverWait显式等待,不要用time.sleep固定睡眠。显式等待的代码上面已经写过,这里不再重复。核心思路是轮询页面中某个目标元素是否出现,一旦出现立即继续执行,效率比固定等待高得多。
5.3 Selenium连接被拒绝
报错信息:
WebDriverException: Message: unknown error: cannot connect to chrome at localhost:9515这个报错一般有两种原因。第一种是chromedriver启动后,Chrome自身没有成功拉起。常见原因是系统缺少Chrome运行依赖库,比如libX11、libXss、libasound等。在Debian系系统上执行:
sudo apt-get install -y libnss3 libxss1 libasound2 libxrender1 libgbm1第二种原因是端口被占用。chromedriver默认监听的端口如果已经被其他进程占用,也会出现类似报错。可以加上一个随机端口参数来规避:
from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() # 使用随机端口,避免冲突 options.add_argument('--remote-debugging-port=0') # 注意:这里的参数是给chrome的,不是给ChromeDriver的需要注意,如果想固定driver端口,可以在初始化时通过service指定,效果更明确。
5.4 无头模式下拿不到Cookie
这个问题经常出现在登录态保持的场景。不少网站是点击登录之后,把登录凭证写入Cookie。如果你发现无头模式下明明登录成功了,但后续访问需要登录的页面还是被重定向到登录页,往往是因为Cookie没有正确保存和加载。
保存Cookie:
import json with open('cookies.json', 'w') as f: json.dump(driver.get_cookies(), f)加载Cookie:
with open('cookies.json', 'r') as f: cookies = json.load(f) # 先打开一次目标域名,才能写入Cookie driver.get('https://example.com') for cookie in cookies: driver.add_cookie(cookie) driver.refresh()这里有个细节,add_cookie之前必须先访问一次目标网站的根域名,否则浏览器不认这个Cookie对应的域。这是Chrome的强制策略,绕不开。
5.5 使用webdriver-manager库
既然下载chromedriver那么麻烦,有没有自动化管理的方案?有,就是webdriver-manager这个库,它能在运行时自动检测已安装Chrome的版本,然后下载对应版本的chromedriver到本地缓存。
pip install webdriver-managerPython代码里这样写:
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)webdriver-manager第一次运行时会去Google官方接口查询版本信息,然后下载对应的二进制文件。这个过程需要联网,下载速度根据网络环境而定,但好处是再也不用手动关心版本号了。
5.6 如何判断你的chromedriver是否真的在运行
写代码时如果报错,先做最基础的检查。打开终端,直接手动启动chromedriver:
chromedriver如果环境配置正确,它会输出类似下面的日志:
Starting ChromeDriver 146.0.6214.76 (f564a4ae502d) on port 9515 Only local connections are allowed.看到这个输出说明chromedriver本身能跑。此时再开另一个终端窗口,用curl测试:
curl http://localhost:9515/status返回JSON格式:
{"value": {"ready": true, "message": "ChromeDriver ready for new sessions."}}说明chromedriver服务正常。这是排查问题的第一步,先把最低层的基础打牢,再向上检查代码逻辑。
5.7 无头模式的常见误用
最后说一个很多人容易犯的错误,把无头模式开在本地图形环境里,结果截图出来是空白。原因很简单,你设置了--headless=new,但某个网站检测到无头模式后会拒绝渲染核心内容,或者返回一个空壳模板。这时候可以通过在JavaScript中隐藏webdriver属性来应对:
from selenium.webdriver.chrome.options import Options options = Options() options.add_argument('--headless=new') options.add_argument('--disable-blink-features=AutomationControlled')然后执行:
driver.execute_script('Object.defineProperty(navigator, "webdriver", {get: () => undefined})')这一行代码删除了navigator.webdriver标记,很多前端检测脚本就是靠这个属性来识别自动化浏览器的,隐藏之后能有效提升成功率。不过要注意,网站风控手段一直在更新,这种办法并不能保证100%通过所有检测。
6. 几个重要的环境细节
说完问题排查,再补充几个容易忽略但会直接影响成败的细节。
6.1 PATH路径下的权限
如果你把chromedriver放在/usr/local/bin/下,系统升级或者别人改了目录权限后,chromedriver可能变得不可执行。我自己的习惯是把chromedriver统一放在/opt/chromedriver/目录下,然后在代码里显式指定路径:
from selenium import webdriver from selenium.webdriver.chrome.service import Service service = Service('/opt/chromedriver/chromedriver') driver = webdriver.Chrome(service=service)这样不依赖系统的PATH变量,换机器部署时只要把整个目录拷过去就行。
6.2 多开浏览器实例
如果业务需要同时开多个Chrome实例,每个实例必须使用不同的用户数据目录,否则会互相抢占锁。示例:
from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service options = Options() options.add_argument('--user-data-dir=/tmp/chrome_profile_1') service = Service('/opt/chromedriver/chromedriver') driver = webdriver.Chrome(service=service, options=options)批量处理时,可以循环生成多个临时目录,确保每个driver实例独立运行。
6.3 内存与进程清理
无头浏览器跑久了,难免会有残留进程。Chrome每个标签页就是一个进程,如果代码中途崩溃,子进程可能不会自动退出。定时任务里最怕这种残留进程堆积,最后把服务器内存吃光。建议在每次任务结束后执行清理:
pkill -9 -f chromedriver pkill -9 -f chrome注意,这两个命令会杀掉所有相关进程,使用前要确保没有其他人在跑同类服务。这个命令放到Shell脚本里,配合定时任务执行,能保持服务器干净。
7. 从实际项目里总结的三句话
第一句,务必保持chromedriver版本和浏览器版本一致,这是所有问题中优先级最高的。所有莫名其妙的session not created错误,90%都是版本问题引起的。
第二句,无头浏览器适合做短小精悍的定时任务,不适合做大规模分布式爬虫。它太重了,每个实例要占几百MB内存。真要到大数据量抓取的场景,应该考虑用Playwright的持久化上下文,或直接走浏览器协议级别的实现。
第三句,在国产操作系统上如果遇到chromedriver无法正常运行,不要花太多时间深挖到底哪里不兼容。官方不支持就是事实,早点切换到Firefox或者Playwright才是有效的解决办法。我在实际项目里已经验证过,同样的登录脚本,从Chrome迁移到Firefox之后,原先在国产系统上跑不通的问题就没有了。
chromedriver本身是个很成熟稳定的工具,日常开发里完全够用。真正花时间的往往是环境适配和版本匹配这一类看似无关紧要、实际却决定成败的琐碎问题。把这些基础打牢,后面写自动化脚本就会顺手很多。