nodriver 是 ultrafunkamsterdam 開發的 Python 瀏覽器自動化函式庫;這位作者同時也是 undetected-chromedriver 的開發者,而 nodriver 也被定位為該專案的接班人。它的架構差異,重點在於不再依賴 Selenium 與 chromedriver:nodriver 直接透過 Chrome DevTools Protocol(CDP)與 Chromium 溝通,整個流程不需要 WebDriver 二進位檔,API 也採用非同步設計。Playwright 和 Puppeteer 屬於另一類比較對象;它們同樣是以協定為基礎的瀏覽器控制器,並非 WebDriver 的延伸,真正有意義的差別主要在 API 設計、封裝方式、瀏覽器供應模式,以及相容性政策。
這篇評測針對的是 nodriver 0.50.3,沒有拿它去對抗真實的防護機制。我檢查了已安裝套件、匯入行為、API 介面、磁碟占用與授權,並且只在 127.0.0.1 上提供的頁面上啟動瀏覽器。沒有接觸任何真實目標、反機器人服務或 CAPTCHA。結果描述的是套件本身的行為與預設瀏覽器揭露內容,並不能用來證明它的反偵測效果。
在這個範圍內,有三件事特別突出。第一,在 Python 3.14 上,這個函式庫根本無法匯入——只因為一個多餘的位元組,就會在你呼叫任何功能之前把整個套件卡死。第二,以這類驅動器來說,它的體積相當小:只列出三個直接相依套件、外加一個解析出的遞移相依,而且在測試環境中大約只有 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!)。它含有一個非 UTF-8 位元組 0xb1,對應註解 #: JSON (±Inf). 裡的 ±,而且沒有宣告來源編碼。模組會經由 nodriver/__init__ → cdp/__init__ → network 載入,因此解析失敗會直接中止匯入。整個套件掃描下來,沒有發現其他非 UTF-8 的來源檔案。
版本邊界要小心措辭。Python 3.14.2 會拒絕這個檔案,而 Python 3.12.13 則可以直接匯入未修補版本。這兩個環境中的 network.py 位元組內容完全相同(SHA-256 ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6),所以不是來源檔不同造成結果差異。這篇評測沒有進一步釐清究竟是 CPython tokenizer 的哪個變動導致此問題,也沒有測試 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 一樣小:使用已測試過的 Python 版本,或者像 pull request #36 提議的那樣,把那個檔案重新編碼為 UTF-8。完成重新編碼後,這個套件在 3.14 上可以正常匯入與 introspect,不會再冒出第二道阻礙。下面那個乾淨的 3.12.13 執行結果,驗證的是未修補路徑的一種情況;本次評測沒有測試 Python 3.13。
在 Python 3.12.13 上,import nodriver 不需要任何修補 就能成功。後面所有瀏覽器相關數據都來自這個安裝環境。至於磁碟占用與匯入時間,則是來自 3.14 上那份僅重編碼單一位元組的複本,並已在文中標明。Python 3.13 在本次評測中仍未測試;一個 3.12 結果,不能代表整個更早版本區間。
如果你平常就用最新的直譯器,而且不少團隊也很快就會升到新 Python,那這確實是一道實際存在、雖然很容易修復的門檻。先知道它存在,就不會為了一個你沒寫過的 SyntaxError 白白浪費半天。
nodriver 底層到底是什麼
那句一句話介紹——「原生 CDP,不用 webdriver」——乍看像是行銷話術,但只要看它實際隨 wheel 打包了什麼,就知道不是空話。nodriver 內建了一整套 DevTools Protocol bindings:nodriver.cdp 套件裡有 57 個協定域模組,包含 accessibility、dom、network、page、fetch、runtime、target、storage、input、emulation 等等。這個模組數量,就是它能說自己是「直接走 CDP、不靠 Selenium」的機制。它不是透過 chromedriver 可執行檔去說 WebDriver 再幫你轉譯,而是直接把 CDP 的各個 domain 轉成 Python 物件,並透過 WebSocket 與協定本身溝通。那個有問題位元組所在的 cdp/network.py,正是這 57 個自動生成模組之一,所以這個錯誤出現在沒人手寫維護的生成檔裡。
在這層協定之上,還包了一個更好上手的物件模型。Tab 物件暴露了 62 個公開方法,而且尋找元素的能力比多數驅動器都更完整:可以用 find() 與 find_all() 做文字比對,用 select() 與 select_all() 做 CSS 選擇,還能直接呼叫原生的 xpath()。同一個物件同時支援 XPath、CSS 和文字搜尋,確實很方便;有些其他函式庫甚至得退回去用 evaluate() 才能處理 XPath。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 自己的 introspection 工具量出來的——包含 inspect、模組遍歷與屬性計數。完全沒有碰到任何網站。你如果想重跑,數字都直接來自套件本身,不是憑感覺。
它對外宣告了什麼

有個問題不用碰任何真實防護就能回答:當 nodriver 控制瀏覽器時,這個瀏覽器對它所看的頁面,會自我揭露哪些資訊?我寫了一個頁面,讀取最明顯的幾個欄位——navigator.webdriver、user-agent、platform、languages、plugin 與硬體核心數、window.chrome 的結構、Permissions API 的回應,以及視窗與螢幕幾何資訊——把它放在 127.0.0.1 上,然後讓四個堆疊去打它:nodriver、Botasaurus、以及原生 Playwright 和原生 Puppeteer 作為對照。四者都控制同一個 Chrome 版本(Chrome for Testing 151.0.7922.10),所以任何差異都來自函式庫,而不是瀏覽器本身。無頭與有頭模式,各跑三次。下面每個數值,在三次執行中都保持一致。
| 堆疊 | 模式 | navigator.webdriver | User-agent token | navigator.languages |
|---|---|---|---|---|
| nodriver 0.50.3 | headless | false | HeadlessChrome/151.0.0.0 | ["en-US"] |
| nodriver 0.50.3 | headed | false | Chrome/151.0.0.0 | ["en-US"] |
| Botasaurus 4.0.92 | headless / headed | false | HeadlessChrome/151 / Chrome/151 | ["en-US"] |
| Playwright 1.56.0 | headless / headed | true | HeadlessChrome/151 / Chrome/151 | ["en-US","en"] |
| Puppeteer 24.16.0 | headless / headed | 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. |
| Plugins | 5 個 |
| MIME types | 2 個 |
pdfViewerEnabled | true |
| 邏輯核心數 | 12 |
| 回報的裝置記憶體 | 16 GB |
| Touch points | 0 |
window.chrome | 存在,且有 app / csi / loadTimes,沒有 runtime |
| WebGL renderer 字串 | 四者完全一致 |
過去常見的那個老問題——Permissions API 和 Notification.permission 彼此矛盾——在這裡完全沒出現;四者都一致回報 default 與 prompt。我也掃過 document 與 window,查看舊式 WebDriver 常留下的 cdc_ 痕跡:四者都沒有。
nodriver 看起來比對照組更像「裸自動化瀏覽器」的地方,反而是視窗幾何。無頭 nodriver 在 800×600 的螢幕上回報 outerWidth / outerHeight 為 0×0;無頭 Playwright 則因為替你設定 viewport,所以回報 1280×720。Puppeteer 則跟 nodriver 一樣是 0×0。這是預設配置上的差異,不是能力差異,而且你可以自行調整。
部署前還有一件事值得知道:nodriver 不會自帶瀏覽器,所以開箱即用時,它會使用你機器上已安裝的 Chrome。以我的環境來說,它自動偵測到的是 /Applications/Google Chrome.app —— Chrome 150.0.7871.187 —— 而它揭露的 user-agent 也就是那個版本,而不是某個固定版本。你的整個機器群,對外揭露的瀏覽器版本,就是那些機器實際安裝的版本。
把這件事說清楚:這是對「自動化堆疊在沒人要求它隱藏時,會揭露什麼」的紀錄。對防守方有用,也適合想知道自家工具對外廣播什麼的人。但它不是在測量這些資訊對某個特定服務有沒有用。我沒有測那個,也不該從上面的任何一列推導出這種結果。
它真的能把頁面內容抓出來嗎?
會自我揭露是一回事,把正確的 HTML 抓回來又是另一回事。我用 nodriver 跑了同一個三類內容的測試頁,這個基準測試倉庫裡其他工具也是用同樣的頁面,所以數據可以跟它們對齊。這個頁面包含三個東西:A 是靜態連結,標記字串直接寫在回應內容裡;B 是解析期間由 inline script 建立的節點,它的標記與 URL 由片段組裝而成,只有執行 JavaScript 才看得到;C 是在 load 事件之後 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 類就證明了這點,因為 B 類在原始回應裡根本不存在——但凡是 load 之後才注入的內容,它就會漏掉。加上 tab.select("#delayed-injected") 之後,就能拿到全部三類。跟 Playwright 和 Puppeteer 一樣,同樣的坑、同樣的解法。三次重複測試、三輪完整套件執行都很穩定,沒有 flake。
我把注入延遲逐步拉長,找出預設讀取何時開始失手。結果顯示,只要注入在 load 之後 100 ms 或更晚 才發生,nodriver 就會開始看不到 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 的 waiter 對這些延遲的追蹤更細。若有大量連續等待,差距會累積,不過這篇評測沒有去測一個有三十個 selector 的正式生產頁面。
啟動速度則是另一個把「非同步又精簡」這個定位落實到現實的地方。把瀏覽器拉起來時,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 的 metadata 要求的是 mss、websockets 和 deprecated;wrapt 則是因為 deprecated 需要它才被帶進來。那個 17.2 MB 環境裡的第六個 dist-info 目錄是 pip,它本來就已存在於該虛擬環境中。所以「6 個 distribution、17.2 MB」描述的是測量到的環境,而「4 個新增執行期套件」描述的是安裝解析結果。兩者有關,但不能混用。
真正能讓這個數字產生意義的對比是:在同一台機器上,姊妹框架 Botasaurus 佔 122.3 MB、44 個套件——大約是 7 倍 的磁碟足跡。這就是精簡型 CDP 驅動與一體包框架之間的差別,而且這差別是雙面刃。nodriver 給你的是一條很薄、很容易審視的相依樹;Botasaurus 開箱就提供更多功能,但代價是磁碟與相依範圍更大。抽象地說,沒有誰一定比較好——取決於你想要的是 driver 還是 framework——但如果你重視小而可檢查的安裝,nodriver 在這方面算是相當乾淨。
不過,小磁碟占用不等於小匯入成本。在 Python 3.14 的那份已修補位元組複本上,import nodriver 約需 158 ms(以新的子程序匯入計算,中位數大約 151–199 ms)。未修補的 3.14 匯入會直接失敗,因此那個版本沒有時間數據。57 個 CDP domain 模組會在匯入時一次全部載入,而匯入後、尚未啟動任何 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 內確認過,也在 repo 的 spdx_id 內確認過。這是一種相當強的網路版 copyleft 授權,和周邊常見的寬鬆授權相比,承擔的義務明顯不同。
拿來跟 nodriver 比較的常見競品,大多都是寬鬆授權,這樣對比就很具體:
| 工具 | 授權 | 如果你把「修改過的版本」當作網路服務提供 |
|---|---|---|
| nodriver | AGPL-3.0 | 第 13 條可能要求營運者向遠端使用者提供其修改版的對應原始碼 |
| Playwright | Apache-2.0 | 沒有相對應的網路 copyleft 條款 |
| Puppeteer | Apache-2.0 | 同上 |
| Botasaurus | MIT | 同上 |
範圍也很重要。AGPL 第 13 條是針對「用於遠端網路互動的、受涵蓋程式之修改版」所做的規範。這篇評測不會判定周邊的服務程式碼是否也屬於被涵蓋的工作範圍,也不會處理內部使用或企業邊界的例外情境。如果你要把 nodriver 用在託管產品上,且會修改它,那就應該把授權文本與架構一起交由法律顧問檢視。這是一個技術採用提示,不是法律建議。
我不是在評論 AGPL 好或不好——copyleft 是正當的選擇,而且很多嚴肅專案都採用它。我真正要提醒的是:「nodriver 是免費且開源的」這句話沒錯,但不完整。這個義務是真實存在的,而且和這個生態圈裡常見的寬鬆預設不同;它應該被放進決策裡,而不是被簡化成單純的「免費」。(順帶一提,PyPI 本身完全沒有提供 license classifier;AGPL 文本在 wheel 裡,而 SPDX id 在 repo 裡,所以不要指望套件索引會主動替你呈現這件事。)
中繼資料,帶時間戳
以下是某個時間點的 repo 與套件數據,直接來自 GitHub API 與 PyPI:
| 事實 | 2026 年 7 月 14 日的數值 |
|---|---|
| Stars | 4,511 |
| Forks | 422 |
| 開放中的 issues | 14 |
| 建立時間 | 2024 年 2 月 |
| 最後推送 | 2026 年 5 月 |
| 最新 PyPI 發布 | 0.50.3 |
| Wheel | 純 Python py3-none-any |
requires-python | >=3.9 |
| Python classifiers | 3.7–3.13 |
可觀察到的維護訊號是混合的:repo 在 2026 年 5 月還有推送,但最新測得的套件仍包含 Python 3.14 匯入問題,而建議中的修補方案在研究日期時還沒正式釋出。Stars 與 open issues 的數字,並不能直接說明這個更新節奏是否符合你的維護標準。
優點與缺點
優點:
- 安裝體積小、結構清楚:只有三個直接相依套件,加上遞移的
wrapt;在測得環境中約 17.2 MB、涵蓋 6 個dist-info目錄(其中一個是pip),沒有 numpy/lxml,也不會在安裝時下載瀏覽器。 - 真正的原生 CDP:內建 57 個 DevTools Protocol 模組,直接透過協定溝通,不需要 chromedriver/Selenium 二進位檔。
Tab上的元素查找能力很完整(62 個公開方法),原生支援 XPath、CSS 和文字搜尋,不必為了 XPath 退回原始evaluate()。- 非同步設計、每次執行都用新設定檔,
Config也把常用控制項(headless、可執行檔路徑、參數、語言、port)整理得很乾淨。 - 在這個測試頁上,抓 JS 組成的內容能力和重量級工具一樣:預設讀取可拿到 3 類中的 2 類,加上等待後可拿到 3/3;與原生 Playwright、Puppeteer 的結果一致,而且三輪測試都穩定。導覽與讀取速度在四者中也是最快的,約 119–129 ms。
- 預設情況下
navigator.webdriver回傳false,而兩個原生控制組都回傳true,而且不是靠 patch 屬性達成——描述子仍是瀏覽器自己的原生 getter。 - 架構定位很清楚:它就是 undetected-chromedriver 的 CDP 原生接班人。
缺點:
- 在 Python 3.14 上開箱就無法匯入——
cdp/network.py那個含有非 UTF-8 位元組的單一檔案,會直接在匯入時丟出SyntaxError。這已在 issue #35 重現,且在 0.50.3 中仍未修復。要嘛把直譯器 pin 在 ≤3.13(這裡已在 3.12.13 驗證乾淨),要嘛手動重新編碼該檔案。 - AGPL-3.0 對任何以網路服務方式提供、且會修改該套件的人來說,確實是一個重要的採用考量,比 Apache/MIT 更嚴格。
- 小磁碟占用不代表小匯入成本:因為 57 個 CDP 模組會一次載入,冷啟動約 158 ms、匯入後常駐約 31.5 MB。
tab.select()會以 0.5 秒為單位輪詢,因此短等待會被往上捨入——100 ms 的等待會花到約 1.1 秒;雖然每次都能抓到,但如果很多小等待累積起來,成本會很明顯。- 無頭模式下,user-agent 預設仍然會宣告
HeadlessChrome,跟原生控制組完全相同;預設設定並不會幫你隱藏最明顯的自我識別字串。 - 執行時仍然需要真正的 Chrome/Chromium/Edge/Brave 二進位檔;輕量的 pip 安裝只完成了一半的依賴故事,而機器上裝的是哪個 Chrome,對外就會揭露哪個版本。
- 這裡沒有驗證它對任何反機器人系統的有效性——整個 stealth 前提,刻意沒有測。
我沒測、所以不能評論的項目包括:每個 tab 的記憶體、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 image、瀏覽器安裝、共用函式庫、sandbox 或程序清理。
有兩類人應該考慮別的選項。第一,如果你用的是 Python 3.14,而且不想 pin 直譯器或手動修補 vendored 檔案,那就先等修正正式釋出——這個匯入問題現在是硬性阻擋。第二,如果你打算用它做一個託管服務,而且會有私有修改,那 AGPL-3.0 光這一點就足以讓你先考慮授權更寬鬆的替代方案,再決定是否採用。這不是在否定程式碼品質,而是你寧可現在就知道限制,也不要等到合規審查才發現。
如果你真正需要的是資料,而不是一個要你手動操控的瀏覽器,也該跳過它。nodriver 提供的是可腳本化的 tab 與 62 個方法;把渲染好的頁面轉成乾淨、結構化的紀錄,仍然得你自己寫程式去做。那是另一份工作,也是託管 API 進場的地方。
替代方案,以及 Thunderbit 在哪裡適合
先講最誠實的定位:nodriver 是免費、AGPL、而且自架。瀏覽器你自己管、更新你自己處理、執行環境和其中所有失敗點也都由你承擔。對於想要這種控制力的開發者來說,沒有任何託管服務能在成本上打贏一個你已經可直接使用的函式庫。
若只看開源世界,請按用途比較,而不是按品牌。若你在評估真實瀏覽器驅動,我們的 Playwright 與 Puppeteer 比較 會把 nodriver 正面對上的兩個重量級工具講清楚。Scrapling 則是如果你關注的是 stealth 方向,最接近的 Python 同類。若你要的是可直接給 LLM 使用的輸出,而不是原始瀏覽器控制,Crawl4AI 會把頁面渲染後以 Markdown 交回給你;而 Scrapy 仍然是大規模、無瀏覽器爬取的代表框架。如果你一次要比較好幾個工具,開源爬蟲總覽 可以把這些類別放在一起看。
揭露一下:這篇文章由 Thunderbit 發布。Thunderbit 是一個託管式擷取服務,所以這裡的比較是以工作任務為基準,而不是以架構為基準。nodriver 提供的是你自己主機上執行、自己程式控制的瀏覽器控制層;Thunderbit 則以服務形式處理渲染,並回傳頁面內容或 schema 化紀錄。當你需要的是協定層級的瀏覽器控制與自架時,就選 nodriver。當你更在意的是輸出紀錄與作業交付,而不是自己擁有瀏覽器時,就可以考慮託管式擷取器。可變動的 endpoint、額度與批次上限資訊,應該看 價格頁,不要塞進函式庫基準測試裡。
這個取捨的核心在於:工作放在哪裡。nodriver 把瀏覽器管理與執行環境維護留在你這邊,且不收每次請求的服務費;託管 API 則接手那一層,並按呼叫次數計費。
結論
如果你想要的是一個小巧、非同步、原生 CDP 的 Chromium 驅動,而且已經確認它在你的 Python 版本上可用(這裡在 3.12.13 上已驗證乾淨匯入),同時也評估過 AGPL-3.0 對你的發佈方式意味著什麼,那就可以用 nodriver。它解析出的執行期相依只有 4 個套件,測得環境大約占 17 MB,並且內建 57 個 CDP domain 模組、Tab 有 62 個方法、也原生支援 XPath。在這個測試頁上,它預設可抓到 3 類內容中的 2 類,加上等待後可抓到 3/3,與原生 Playwright 和 Puppeteer 的表現相同。
但也要誠實看待限制。它在 Python 3.14 上完全無法匯入,直到你修好那一個非 UTF-8 位元組為止——這是文件化、仍未修復的問題,不是謎團,但你一踩到就是硬停。授權是 AGPL-3.0,對任何以服務方式提供、且會修改該複本的人來說,這是一個真實的決策點,不是形式。小安裝不代表小匯入,因為所有 CDP 模組都會在一開始載入,而 select() 的半秒輪詢會讓短等待幾乎都變成一秒左右。就我實際能回答的預設揭露問題來看,情況比行銷說法還窄:只有一個布林值跟原生 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 domain 模組,並透過 WebSocket 直接與 CDP 溝通,而不是經由 Selenium 去呼叫 chromedriver。Tab 提供 62 個方法,包括原生 XPath。套件 metadata 宣告了三個執行期需求(websockets、mss、deprecated);解析後會再帶入 wrapt。測得環境約有 17.2 MB、分布在 6 個 dist-info 目錄中,包含 pip,沒有 numpy、lxml,也沒有下載瀏覽器二進位檔。兩個注意點:因為 57 個 CDP 模組會一次載入,所以修補版的匯入時間大約 158 ms;此外,你還是得另外提供一個 Chrome 家族的瀏覽器。
AGPL-3.0 授權會影響我的專案嗎?
要看你的發佈方式。AGPL-3.0 是網路 copyleft 授權:如果你把「修改過的 nodriver」做成服務,讓別人透過網路使用,理論上你就必須向他們提供該修改版的原始碼。若只是個人腳本或從不對外開放的內部工具,通常就不是問題。若你要基於已修改的 nodriver 建一個託管商用產品,那就應該把這個問題交給負責合規的人一起看——而且它比 Apache-2.0 與 MIT 更嚴格。
nodriver 會揭露它自己哪些資訊?這是否代表它能打贏 Cloudflare?
前半段是量測結果,後半段不是,而這個差異很重要。在我架在 127.0.0.1 的頁面上,用與對照組相同的 Chrome 版本來驅動時:navigator.webdriver 會回傳 false,而原生 Playwright 與原生 Puppeteer 都回傳 true。這個值是在瀏覽器啟動時就已決定,不是靠 patch 屬性改掉的——描述子仍然是 Chrome 自己的原生 getter。除此之外,幾乎所有欄位都跟對照組完全一樣:platform 字串、5 個 plugins、12 顆核心、16 GB 的裝置記憶體回報、window.chrome 的結構、Permissions API 沒有矛盾、document 與 window 上也沒有 cdc_ 之類的殘留。無頭模式下,user-agent 還是會宣告 HeadlessChrome/151.0.0.0,跟兩個對照組一樣——這部分並沒有幫你遮掉。但這些資訊都不能告訴你,它們對真實反機器人服務到底有沒有用。我沒有把 nodriver 對準任何 live site,沒有接觸任何反機器人服務,也沒有碰 CAPTCHA;這些都刻意排除在範圍之外。上面的揭露表只是在說這個堆疊會宣告什麼,並沒有說誰在聽、或對方會怎麼反應。
nodriver 能正確處理 JavaScript 渲染的內容嗎?
可以,但要注意你「何時」讀取。在一個有三類內容的測試頁上,預設的 browser.get() + tab.get_content() 會拿到 3 類中的 2 類——它能正確執行 JavaScript(同步注入的那一類在原始回應裡根本不存在,卻還是能被取回),但它是在 load 事件時讀取,所以會漏掉之後才注入的內容。加入 tab.select("#delayed-injected") 後,就能拿到 3/3。這與原生 Playwright 和原生 Puppeteer 在同一頁上的表現完全一致。要留意的一個細節是:select() 會以 0.5 秒為單位輪詢,所以 100 ms 的等待大約會耗掉 1.1 秒。


