nodriver 是 ultrafunkamsterdam 推出的一款 Python 浏览器自动化库,作者也正是 undetected-chromedriver 的开发者,它自称是后者的继任者。它在架构上的分野主要是相对于 Selenium 和 chromedriver:nodriver 通过 Chrome DevTools Protocol(CDP)直接与 Chromium 通讯,不需要 WebDriver 二进制参与,且其 API 是异步的。Playwright 和 Puppeteer 则属于另一类比较对象。它们同样是基于协议的浏览器控制器,并不是 WebDriver 的后代;真正值得比较的差异在于 API 设计、打包方式、浏览器提供方式以及兼容性策略。
这篇评测聚焦的是 nodriver 0.50.3,而不是拿它去对抗真实防护。我的测试内容包括已安装包、导入情况、API 表面、磁盘占用和许可证,然后只在 127.0.0.1 上提供的页面上启动浏览器。没有涉及真实目标、反爬服务或验证码。结果描述的是这个包本身的行为和默认浏览器暴露信息;并不能据此证明它的反检测能力。
在这个边界内,有三点最显眼。第一,在 Python 3.14 上,这个库根本无法导入——只因一个多余字节,整个包在你调用任何功能前就先挂了。第二,作为同类驱动,它的体积出奇地小:只包含 3 个直接声明依赖,加上 1 个解析出的传递依赖,在我测量的环境里大约占 17 MB。第三,在我自己的页面上,nodriver 与原生 Playwright 和原生 Puppeteer 唯一明显不同的地方只是一个布尔值——而在无头模式下,这三者的 user-agent 里依然都写着 HeadlessChrome。剩下最让人意外的部分,反而在许可证上。
Python 3.14 上的导入故障
先说最先会卡住你的问题,因为它会在你的代码运行之前就发生。在 Python 3.14 上,直接执行 import nodriver 会立刻失败:
File ".../nodriver/cdp/network.py", line 1345
#: JSON (±Inf).
^
SyntaxError: Non-UTF-8 code starting with '\xb1' on line 1345, but no encoding declared; see PEP 263
出问题的文件是自动生成的 cdp/network.py(文件头写着 # DO NOT EDIT THIS FILE!)。它在注释 #: JSON (±Inf). 里含有一个非 UTF-8 字节 0xb1,却没有声明源码编码。模块的加载路径是 nodriver/__init__ → cdp/__init__ → network,因此解析失败会直接中断导入。包扫描没有发现其他非 UTF-8 源文件。
版本边界需要说得准确一些。Python 3.14.2 会拒绝这个文件,而 Python 3.12.13 在未打补丁时可以正常导入。两个环境中的 network.py 文件字节级完全一致(SHA-256 ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6),所以结果并不是因为源文件不同。这篇评测并没有去定位具体是哪一处 CPython 分词器变更导致的,也没有测试 Python 3.13。这里只如实报告这两个实测解释器结果,而不是声称所有更早版本都和 3.12 一样。
仅凭这两个测点,无法推出 Python 3.13 的结论。
这不是新发现,而是复现。同样的 Python 3.14 traceback 已经记录在 nodriver issue #35 中,修复建议则在 pull request #36 里。0.50.3 版本仍然保留着这个字节。PyPI 的分类信息只写到了 Python 3.13,也并未声明支持 3.14。
这个问题的规避方式和 bug 本身一样小:要么使用已经验证过的解释器版本,要么像 pull request #36 建议的那样,把那一个文件重新编码为 UTF-8。完成重新编码后,包在 3.14 上可以顺利导入并完成基本检查,不会再冒出第二个阻碍。本评测下方的干净 3.12.13 运行,验证的是未打补丁路径;Python 3.13 在这里没有测试。
如果你默认使用较新的解释器——很多团队确实会迅速跟进新版 Python——那这就是一堵真实存在、虽然很容易修的墙。提前知道它的存在,免得你被一个自己没写过的文件里的 SyntaxError 白白耗掉一下午。
nodriver 底层到底是什么
“一句话描述:原生 CDP、没有 webdriver”,听起来像营销文案,直到你看它的 wheel 里到底装了什么。nodriver 捆绑了自己完整的一套 DevTools Protocol 绑定:nodriver.cdp 包里包含 57 个协议域模块——accessibility、dom、network、page、fetch、runtime、target、storage、input、emulation 等等。这个模块数量,正是它宣称“直接 CDP、无需 Selenium”的实现基础。它不是调用一个会说 WebDriver、再替你翻译的 chromedriver 可执行文件,而是直接为 CDP 各域生成 Python 对象,并通过 WebSocket 直接讲协议。包含坏字节的 cdp/network.py 就是这 57 个自动生成模块之一,这也是为什么问题会落在没人手工编辑的生成代码里。
在协议层之上,是一个更友好的对象模型。Tab 对象暴露了 62 个公开方法,可搜索的范围比大多数驱动都更宽:find() / find_all() 可做文本匹配,select() / select_all() 可走 CSS,xpath() 则提供原生入口。把 XPath、CSS 和文本搜索放在同一个对象上,确实很顺手;有些库查 XPath 还得退回到 evaluate()。Config 构造函数则暴露了 user_data_dir、headless、browser_executable_path、browser_args、sandbox、lang(默认 'en-US')、host、port、expert,以及 **kwargs。在这次 API 清点中,构造 Config(headless=True) 生成了 16 个 Chromium 启动参数,其中包括 --no-first-run、--no-default-browser-check、--remote-allow-origins=* 和 --homepage=about:blank。这里我并没有调用 start();后面的浏览器测试是单独的一轮。

这个库确实提供了一些偏向反检测的 API 表面——我确认了它们存在,但并没有把这些能力拿去对任何目标做实测。这里提一次就够了,因为它对真实服务的实际影响,正是我刻意没有测试的部分。中性地说,nodriver 的命名比一些强调 stealth 的竞品克制得多。它强调的是架构:原生 CDP、每次运行都使用新配置文件,而不是一堆 detect_and_bypass 风格的方法名。把这理解为对 API 设计的观察,而不是对结果的承诺。
这些统计都是通过导入包并使用 Python 自带的内省工具得出的——inspect、模块遍历、属性计数。没有访问任何网站。如果你想自己重跑,数字都直接来自这个包,不是凭感觉猜的。
它会向外界暴露什么

有一个问题,不需要碰真实防护也能回答:当 nodriver 驱动浏览器时,这个浏览器会对它正在访问的页面透露什么信息?我写了一个页面,读取那些最显眼的东西——navigator.webdriver、user-agent、平台、语言、插件和硬件数量、window.chrome 的结构、Permissions API 的返回、窗口和屏幕尺寸——把它部署在 127.0.0.1 上,然后用四套栈访问:nodriver、Botasaurus,以及原生 Playwright 和原生 Puppeteer 作为对照。四者驱动的都是 同一个 Chrome 构建(Chrome for Testing 151.0.7922.10),因此任何差异都来自库本身,而不是浏览器。无头与有头模式各跑 3 次。下面所有结果在三次运行中都保持一致。
| 栈 | 模式 | navigator.webdriver | User-agent 标记 | navigator.languages |
|---|---|---|---|---|
| nodriver 0.50.3 | 无头 | false | HeadlessChrome/151.0.0.0 | ["en-US"] |
| nodriver 0.50.3 | 有头 | false | Chrome/151.0.0.0 | ["en-US"] |
| Botasaurus 4.0.92 | 无头 / 有头 | false | HeadlessChrome/151 / Chrome/151 | ["en-US"] |
| Playwright 1.56.0 | 无头 / 有头 | true | HeadlessChrome/151 / Chrome/151 | ["en-US","en"] |
| Puppeteer 24.16.0 | 无头 / 有头 | true | HeadlessChrome/151 / Chrome/151 | ["en-US"] |
最明显的差异就是那个布尔值。在这些默认启动配置下,nodriver 在无头和有头运行中都把 navigator.webdriver 报告为 false,而原生 Playwright 和 Puppeteer 都报告 true。这个测试只说明配置层面的差异;并不能证明这个值只是因为没有 WebDriver 二进制才会出现。
在这四套栈里,这个属性仍然是 Navigator.prototype 上浏览器原生的 getter —— function get webdriver() { [native code] }——既不是实例上的自有属性,也没有被替换成别的函数。页面 JavaScript 在加载后也没有重写它。测试没有去枚举导致这个值不同的具体启动参数或源码路径。
第二个细节就更能修正营销话术了。在无头模式下,nodriver 的 user-agent 仍然会显示 HeadlessChrome/151.0.0.0——和原生 Puppeteer 一样,和原生 Playwright 也一样。切换到有头模式后,它又变成 Chrome/151.0.0.0,同样完全一致。如果你以为一个反检测库会默认隐藏浏览器自动化里最有名的自我标识字符串,那它并不会。这个得你自己去改。
几乎所有其他字段在四套栈里都一样,这一点值得明确说出来,因为它缩小了故事范围——下面这些属性在 nodriver、Botasaurus、Playwright 和 Puppeteer 上读到的值都相同:
| 属性 | 四套栈都一致的值 |
|---|---|
platform | MacIntel |
vendor | Google Inc. |
| 插件 | 5 个 |
| MIME 类型 | 2 个 |
pdfViewerEnabled | true |
| 逻辑核心数 | 12 |
| 报告的设备内存 | 16 GB |
| 触控点 | 0 |
window.chrome | 存在,包含 app/csi/loadTimes,但没有 runtime |
| WebGL 渲染器字符串 | 四者完全相同 |
Permissions API 和 Notification.permission 互相矛盾的老问题,这里一个也没出现——四者都一致返回 default 和 prompt。我还检查了 document 和 window 里是否存在旧版 WebDriver 栈常见的 cdc_ 残留:四者都为空。
nodriver 看起来比“控制型浏览器”更像“裸奔的自动化浏览器”的唯一地方,是窗口几何信息。无头模式下的 nodriver 在 800×600 屏幕上返回 outerWidth / outerHeight 为 0×0;无头 Playwright 则因为默认帮你设置了视口,返回 1280×720。Puppeteer 和 nodriver 一样是 0×0。这是默认配置上的差异,不是能力上的差异,你可以自行修改。
还有一点在你投入使用前值得知道:nodriver 不自带浏览器,所以开箱即用时会使用你机器上已有的 Chrome。在我的机器上,它自动识别到了 /Applications/Google Chrome.app——Chrome 150.0.7871.187——它对外暴露的 user-agent 也是这个版本,而不是某个锁定版本。你机器上对外展示的浏览器版本,就是你设备里实际装着的那个版本。
要说清楚这是什么、不是什么。它记录的是:当自动化栈并没有被要求隐藏自己时,它会向外暴露什么。这对防守方很有用,也能帮助你理解自己工具链在广播什么。但它不是对这些信息是否会影响某个具体服务的测量。我没有测试这一点,所以上面任何一行都不应被解读为某种结论。
它能不能真正把页面内容抓下来?
暴露信息是一回事,拿到正确 HTML 才是正事。我让 nodriver 跑在同一个三类内容测试页上,这个测试页也是这套 benchmark 仓库里其他工具所用的样本,因此这里的数据可以和其它工具对齐。页面包含三种内容:A 是静态链接,标记直接写在返回的字节里;B 是在解析期间由内联脚本创建的节点,它的标记和 URL 都由片段拼接而成,只有执行 JavaScript 才能看到;C 是在加载事件后 800 ms 才注入的节点,拼接方式相同。C 类是对抗项——在 load 事件时读取,肯定看不到它。
| 栈 | 默认读取 | 显式等待后 |
|---|---|---|
| nodriver 0.50.3 | 3 类中的 2 类(A + B,漏掉 C) | 3 类中的 3 类 |
| Botasaurus 4.0.92 | 2/3 | 3/3 |
| Playwright 1.56.0 | 2/3 | 3/3 |
| Puppeteer 24.16.0 | 2/3 | 3/3 |
nodriver 的表现和这些重量级工具完全一致。browser.get() 后直接接 tab.get_content(),拿到的是 load 时刻的快照:它能正确执行 JavaScript(因为 B 类并不存在于返回的原始字节中,却仍然被读到了),但凡是加载后才注入的内容都会被漏掉。加上 tab.select("#delayed-injected"),就能拿到全部三类。跟 Playwright 和 Puppeteer 一样,是同样的坑,也是同样的修法。在三次重复和整套测试的三轮独立运行中结果都稳定,没有抖动。
我还把注入延迟往后拉,看看默认读取会在什么地方失效。nodriver 在注入延迟达到 100 ms 或更长时,就不再看到 C 类了——这和两个原生控制器的边界一致。(Botasaurus 在这里是例外,也是这两类反检测库里真正有意思的区别:它的 get() 默认会阻塞到完整页面加载,所以默认读取在 300 ms 注入时仍然能抓到内容。代价是每次导航大约多花 250 ms。)
等待机制本身还有一个值得提前预算的特性。在这次扫描里,nodriver 的 tab.select() 不是紧贴注入延迟,而是落在粗粒度轮询区间里:
| C 类延迟 | 0 ms | 100 ms | 400 ms | 800 ms | 1500 ms |
|---|---|---|---|---|---|
nodriver select() | 124–152 ms | 1132–1141 | 1128–1129 | 2132–2177 | 2138–2150 |
Puppeteer waitForSelector | 113–129 ms | 203–216 | 512–516 | 911–919 | 1608–1611 |
在这个测试页里,100 ms 的注入最终让 select() 花了大约 1.1 秒。已安装的循环在 miss 之后会执行 await self,然后再 await self.sleep(0.5);这次测出来的组合周期接近 1 秒,但不能据此把 await self 视为固定时长的普遍睡眠。所有测试过的延迟节点都找到了。Puppeteer 的等待器则更贴近这些延迟。若连续等待很多个元素,这种差异会叠加,不过这篇评测并没有去跑一个 30 个选择器的生产级页面。
启动速度是“异步且轻量”这个说法和现实碰面的另一个地方。把浏览器拉起来时,nodriver 与 Botasaurus 和 Puppeteer 处在同一档,而明显慢于 Playwright:
| 栈 | 跨运行的浏览器启动时间 |
|---|---|
| nodriver 0.50.3 | 910–1583 ms |
| Botasaurus 4.0.92 | 986–1151 ms |
| Puppeteer 24.16.0 | 969–1008 ms |
| Playwright 1.56.0 | 282–365 ms |
一旦跑起来,nodriver 在“导航 + 读取”这一步上是四者里最快的,耗时 119–129 ms。启动时不算特别快,跑起来后倒是很利索。
安装和占用:真正出色的部分
这里才是 nodriver 名字里“专注”二字真正说得通的地方,而且这是一条可以验证的、无害的安装事实,而不是和抓取能力相关的什么神话。一次干净的 pip install nodriver 会解析出如下结果:
| 安装事实 | nodriver 0.50.3 |
|---|---|
| 直接声明的运行时依赖 | 3 个 —— websockets、mss、deprecated |
| 解析出的传递依赖 | wrapt(来自 deprecated) |
测得的 site-packages 总占用 | 大约 17.2 MB,分布在 6 个 dist-info 目录中,其中包括环境里的 pip |
| nodriver 自身占比 | 3.7 MB |
| numpy / lxml | 都没有 |
| 安装时是否拉取浏览器二进制 | 没有 |
对于一个经常拖进来一整套渲染栈的类别来说,这个体积确实很轻。
直接依赖和传递依赖的区分,对维护很重要。nodriver 的元数据要求 mss、websockets 和 deprecated;wrapt 则是因为 deprecated 需要它才一起装上去的。17.2 MB 环境里的第 6 个 dist-info 目录是 pip,它本来就在那个虚拟环境里。所以,“17.2 MB、6 个发行包”描述的是测量环境,而“四个新增运行时包”描述的是安装解析结果。二者相关,但不能互换。
让这个数字更有意义的对比是:在同一台机器上,兄弟框架 Botasaurus 的体积是 122.3 MB,44 个包——大约是 nodriver 的 7 倍。这就是一个专注的 CDP 驱动和一个开箱即用的框架之间的差别,而且这种差异有利有弊。nodriver 给你的是一棵很薄、很清晰、而且确实能审计的依赖树;Botasaurus 则开箱功能更多,但你得为磁盘和依赖面付出代价。抽象地说,两者都没有绝对“更好”——关键看你要驱动器,还是要框架——但如果你看重的是安装体积小、依赖可读性强,nodriver 在这方面确实做得很干净。
不过,小磁盘占用并不意味着小导入成本。在修补字节后的 Python 3.14 版本上,import nodriver 大约耗时 158 ms(对全新子进程导入取中位数,约 151–199 ms)。未修补的 3.14 导入会失败,所以没有可报告的导入时间。57 个 CDP 域模块会被提前全部加载,而导入后的常驻内存,在任何 Chrome 进程存在之前,就已经达到 31.5–31.9 MB。真正启动浏览器后,内存会增加很多,这部分没有计入这次测量。
这组数字还要附带两个注意点。它们来自同一台机器——macOS arm64——而且体积和导入耗时数据是在 Python 3.14 上、对那个单字节修补版进行测量的,因为未修补包在那个解释器上根本导不进来。在受支持的 Python 上,你不需要补丁:我已经确认 3.12.13 可以干净导入未修补版本,这也是后文所有浏览器测量的来源。运行时仍然需要真实的 Chrome、Chromium、Edge 或 Brave 二进制——nodriver 驱动的是现有浏览器,不会随包附带,所以这部分安装成本不算在 17 MB 里;而正如前面的暴露信息部分所示,你机器上装着哪个 Chrome,对外展示的就是哪个版本。
许可证才是真正的采用决策点

很多对免费工具的评测,会把“它是开源的”当作许可证讨论的终点。对 nodriver 来说,这恰恰只是起点,因为它采用的是 AGPL-3.0——这一点在 wheel 内的 LICENSE.txt 和仓库里的 spdx_id 中都得到了确认。这是很强的网络传染型许可证,和周边工具相比,它带来的承诺明显不同。
大多数人会拿 nodriver 对比的同行,许可证都更宽松,这种反差也就更直观:
| 工具 | 许可证 | 如果你把修改后的副本作为网络服务运行 |
|---|---|---|
| nodriver | AGPL-3.0 | 第 13 条可能要求运营方把其修改版的对应源代码提供给远程用户 |
| Playwright | Apache-2.0 | 没有对应的网络传染条款 |
| Puppeteer | Apache-2.0 | 同上 |
| Botasaurus | MIT | 同上 |
适用范围很重要。AGPL 第 13 条针对的是被覆盖程序的修改版,并且它被用于远程网络交互的情形。本评测并不判断周边服务代码是否构成受覆盖作品,也不处理内部使用或企业边界等边缘情况。如果你的托管产品修改了 nodriver,请把许可证文本和系统架构一起交给法务审核。这是技术采用提示,不是法律意见。
我不是要评价 AGPL 是好是坏——copyleft 本身就是合法而合理的选择,很多严肃项目都在用。我要指出的是,“nodriver 是免费开源软件”这句话没错,但不完整。这个义务是真实存在的,它和这个生态里默认的宽松许可证不同,应该被放进决策里,而不是简单归并为“免费”。(顺带一提,PyPI 本身并没有给出任何 license 分类;AGPL 文本在 wheel 里,SPDX id 在仓库里,所以别指望包索引会替你把这点显出来。)
元数据:时间点记录
下面这些仓库和包层面的数字,直接来自 GitHub API 和 PyPI:
| 事实 | 2026 年 7 月 14 日的值 |
|---|---|
| Stars | 4,511 |
| Forks | 422 |
| Open issues | 14 |
| 创建时间 | 2024 年 2 月 |
| 最后一次推送 | 2026 年 5 月 |
| PyPI 最新发布 | 0.50.3 |
| Wheel | 纯 Python 的 py3-none-any |
requires-python | >=3.9 |
| Python 分类 | 3.7–3.13 |
可观察到的维护信号是混合的:仓库在 2026 年 5 月仍有推送,而最新测得的包里依然包含 Python 3.14 的导入问题,而且在本次研究日期时,建议中的修复还没有发布。Stars 数和 issue 数,并不能直接决定这个更新节奏是否符合你的维护标准。
优点与缺点
优点:
- 安装体积小、结构清晰:3 个直接依赖,加上传递依赖
wrapt,在测量环境里大约 17.2 MB、分布在 6 个dist-info目录中(其中一个是pip),没有 numpy/lxml,也不会在安装时下载浏览器。 - 真正原生 CDP:自带 57 个 DevTools Protocol 域绑定,直接通过协议通信,不需要 chromedriver/Selenium 二进制参与。
Tab的元素查找能力很广(62 个公开方法),支持原生 XPath、CSS 和文本搜索,不必为了 XPath 退回裸evaluate()。- 设计上是异步的,每次运行都会使用新配置文件,
Config也把常见开关(无头、可执行文件路径、参数、语言、端口)做得很清楚。 - 对 JavaScript 渲染页面的内容抓取能力和重量级工具一样稳:默认读取可拿到 3 类中的 2 类,配合显式等待可拿到 3/3,与同页上的原生 Playwright 和 Puppeteer 完全一致,且在三轮运行中都稳定。导航加读取是四者中最快的,119–129 ms。
- 默认情况下
navigator.webdriver会返回false,而原生控制器返回true;而且这个属性没有被打补丁,描述符仍是浏览器自己的原生 getter。 - 架构定位清晰:它可以看作 undetected-chromedriver 的 CDP 原生继任者。
缺点:
- 开箱即用无法在 Python 3.14 上导入——
cdp/network.py里一个非 UTF-8 字节会在导入时抛出SyntaxError。这个问题已在 issue #35 中复现,0.50.3 里仍未修复。要么把 Python 锁定在 ≤3.13(我已在 3.12.13 上验证可干净导入),要么重新编码该文件。 - AGPL-3.0 对任何把修改版作为网络服务运行的人来说,都是实打实的采用门槛;比 Apache/MIT 更严格。
- 磁盘体积小,不代表导入也轻:因为 57 个 CDP 模块会一次性加载,冷启动大约 158 ms,导入后常驻内存约 31.5 MB。
tab.select()的轮询退避是半秒级,所以短等待会被向上取整——100 ms 的等待可能花到约 1.1 秒,而 Puppeteer 的 waiter 约 210 ms。每次都能命中,但很多小等待叠加后会明显变慢。- 无头模式下,user-agent 仍默认暴露
HeadlessChrome,和原生控制器完全一样;默认配置并不会隐藏最显眼的浏览器自动化标识字符串。 - 运行时仍然需要真实的 Chrome/Chromium/Edge/Brave 二进制;轻量的 pip 安装只是整个依赖故事的一半,而你机器上装着哪个 Chrome,对外展示的就是哪个版本。
- 本文没有验证它对任何反爬系统的实际效果——整个“隐身”前提本身就是刻意未测试的。
我没有测试、因此也无法评价的内容包括:单个标签页内存、CDP 往返延迟、配置文件处理、规模化吞吐、Linux 或 Windows、特别是 Python 3.13,以及最重要的——对真实在线服务的反爬实战效果。本评测里的浏览器都只和 127.0.0.1 上的测试页面交互。所有数字都来自同一台机器(macOS arm64);浏览器驱动数据使用的是未打补丁的 Python 3.12.13,而较早的体积和导入时间数据则来自 Python 3.14 上的单字节修补版。
我也没有跑浏览器升级矩阵,因此它对未来 Chrome 版本的兼容性,仍然需要采用方自己做运维层面的检查,而不是这篇评测已经给出的结果。
适合谁,不适合谁
如果你想要一个轻量、异步、原生 CDP 的 Chromium 驱动,并且愿意自己负责浏览器、更新和运行时,nodriver 是合适的。它的依赖树小,更容易审计,CDP 优先的设计也适合协议级控制。容器环境适配仍未验证:本评测没有测试 Linux 镜像、浏览器安装、共享库、沙箱或进程清理。
有两类人应该考虑别的方案。如果你在 Python 3.14 上工作,而且不愿意锁定解释器版本或修补供应链文件,那就等修复正式发布吧——这个导入问题目前是硬性阻断。再者,如果你的交付方式会让 AGPL-3.0 变成负担——例如带有私有修改的托管服务——那仅凭许可证这一点,就足够你在动手前先比较一个宽松许可证的替代品。两者都不是在否定代码本身;只是这些限制,你最好现在就知道,而不是在合规审查时才发现。
如果你真正需要的是数据,而不是一个手工驱动的浏览器,也可以直接跳过它。nodriver 给你的是一个可脚本化的标签页和 62 个方法;把渲染后的页面变成干净、结构化记录,仍然需要你自己写代码。这是另一种工作,也是托管 API 介入的地方。
替代方案,以及 Thunderbit 的位置
先说最诚实的定位:nodriver 是免费、AGPL、可自托管的。浏览器你自己跑,更新你自己管,运行时和其中所有故障都归你。对于想要这种控制力的开发者来说,没有哪种托管服务能在价格上和你已经拥有的一套库竞争。
在开源范围内,最好按任务而不是按品牌来比。如果你在比较真实浏览器驱动,我们的 Playwright 与 Puppeteer 对比 会把 nodriver 身边那两个最明显的“重量级选手”讲清楚。若你关注的是隐身取向, Scrapling 是更接近的 Python 方案。如果你更想要能直接喂给 LLM 的输出,而不是原始浏览器控制,Crawl4AI 会把页面渲染后输出 Markdown,Scrapy 仍然是大规模无浏览器爬取的事实标准。如果你打算一次比较好几个项目,开源爬虫总览 会把这些类别并排放在一起。
披露一下:这篇文章由 Thunderbit 发布。Thunderbit 是一项托管式提取服务,所以这里的比较是按任务而不是按架构来看的。nodriver 提供的是你自己托管、自己编程的浏览器控制层;Thunderbit 则负责渲染,并以服务形式返回页面内容或按 schema 组织的记录。当你需要的是协议级浏览器控制和自托管时,用 nodriver。若你更在意输出记录本身,以及把运维交给服务商,那么就考虑托管提取器。至于可变的端点、额度和批量限制细节,应当去看 定价页,而不是塞进库基准里。
这两种方案的差异,在于工作放在哪里:nodriver 把浏览器管理和运行维护留在你这边,没有按请求收服务费;托管 API 则接管这层,并按调用计费。
结论
如果你想要一个小巧、异步、原生 CDP 的 Chromium 驱动,并且已经确认它适用于你的 Python 版本(这里 3.12.13 可干净导入),同时也评估过 AGPL-3.0 对你的交付方式意味着什么,那么可以用 nodriver。它解析出的运行时依赖只增加了 4 个包,测得环境约 17 MB,库本身包含 57 个 CDP 域模块,Tab 暴露 62 个方法,也支持原生 XPath。在这个测试页上,它默认读取可返回 3 类中的 2 类,加上等待可返回 3/3,和原生 Playwright、Puppeteer 表现一致。
不过,限制也要如实算进去。到了 Python 3.14,它根本无法导入,直到你修掉一个非 UTF-8 字节——这不是谜题,而是一个已知但尚未修复的问题,只是你碰上的那一天,它就是硬停止。许可证是 AGPL-3.0,这对任何把修改版作为服务运行的人来说都是真正的决策点,而不是形式主义。小安装并不等于小导入,因为所有 CDP 模块都会预加载,而且 select() 的半秒级轮询会让短等待都接近 1 秒。至于默认暴露信息,我能实际回答的问题也比营销说法窄得多:和原生 Puppeteer 相比,只是一个布尔值不同,无头 user-agent 仍然写着 HeadlessChrome,而我测到的其他所有属性,在四套栈里都完全一致。至于整个反检测前提——也就是很多人最初会找到 nodriver 的原因——我故意没有测试。我做的是清点库本身,并在自己机器上的页面上驱动它,而不是拿它去对抗真实防护。比起给你一个我无法背书的“绕过”承诺,我更愿意把这点明说。就我能够回答的问题而言,nodriver 是一款设计扎实、体积异常轻巧的驱动,但它有两个锋利边缘——Python 版本墙和 copyleft 许可证——你最好提前看到。
试用 Thunderbit 进行网页数据提取 Get Started Free
常见问题
为什么 import nodriver 在 Python 3.14 上会失败?
因为 cdp/network.py 里含有一个非 UTF-8 的 ± 字节,却没有声明源码编码。Python 3.14.2 会拒绝这个文件,并中止传递导入;Python 3.12.13 则能在未打补丁时导入这个字节级完全一致的文件。本评测没有去定位具体的解释器变化,也没有测试 Python 3.13。上游记录是 nodriver issue #35 和 pull request #36。请使用你已经验证过的版本,或者把该文件重新编码成 UTF-8。
“原生 CDP、没有 webdriver”到底有什么好处,安装代价又是什么?
nodriver 自带 57 个 DevTools Protocol 域模块,通过 WebSocket 直接讲 CDP,而不是通过 Selenium 去调用 chromedriver。Tab 暴露 62 个方法,包括原生 XPath。包元数据声明了 3 个运行时要求(websockets、mss、deprecated);解析后会再带上 wrapt。在测量环境中,总占用约 17.2 MB,分布在 6 个 dist-info 目录里,其中包括 pip,没有 numpy、lxml,也不会下载浏览器二进制。两个注意点:修补版导入大约要 158 ms,因为 57 个 CDP 模块会提前全部加载;而且你还是需要单独准备一个 Chrome 系浏览器。
AGPL-3.0 会影响我的项目吗? 这取决于你怎么交付。AGPL-3.0 是网络传染型许可证:如果你把修改过的 nodriver 版本作为服务提供给他人使用,你就有义务向他们提供对应的修改源代码。对于个人脚本或从不对外暴露的内部工具,这通常不是问题。对于一个建立在修改版 nodriver 上的托管商业产品,这就是一个需要提交给合规负责人的真实问题——而且它比同类工具上的 Apache-2.0 和 MIT 更严格。
nodriver 会向外暴露什么?这是否意味着它能打赢 Cloudflare?
前半句是测出来的,后半句不是,而这个区别很重要。在我通过 127.0.0.1 提供的页面上,驱动同一 Chrome 构建时:navigator.webdriver 返回 false,而原生 Playwright 和原生 Puppeteer 都返回 true。这个值是在浏览器启动时设定的,不是通过补丁改属性得到的——描述符仍然是 Chrome 自己的原生 getter。除此之外,几乎所有内容都和控制组一致:平台字符串、5 个插件、12 个核心、16 GB 设备内存、window.chrome 的结构、Permissions API 没有矛盾,document 和 window 上也没有 cdc_ 风格残留。无头模式下的 user-agent 仍然写着 HeadlessChrome/151.0.0.0,和两个控制组一样——这部分并没有帮你隐藏。以上这些都不能说明它对真实反爬服务是否有效。我从未把 nodriver 指向真实网站,也没有接触任何反爬服务或验证码;这些都不在本次范围之内。上面的暴露信息表只说明这个栈会怎么自报家门,不说明谁在监听,也不说明对方会怎么处理。
nodriver 能正确处理 JavaScript 渲染内容吗?
可以,但要注意你在什么时候读取。在一个包含三类内容的测试页上,默认的 browser.get() + tab.get_content() 返回了 3 类中的 2 类——它确实能正确执行 JavaScript(同步注入的 B 类并不在原始字节中,却仍然被读到了),但它是在 load 事件时读取,所以会漏掉后来注入的内容。加上 tab.select("#delayed-injected") 后,就能拿到 3/3。这和同页上的原生 Playwright 和 Puppeteer 完全一致。要预留一个小怪癖:select() 采用半秒级退避轮询,所以 100 ms 的等待大概要花 1.1 秒。


