nodriver è una libreria Python per l’automazione del browser sviluppata da ultrafunkamsterdam, l’autore di undetected-chromedriver, e viene presentata come la sua naturale evoluzione. La sua rottura architetturale riguarda in modo specifico Selenium e chromedriver: nodriver comunica con Chromium tramite il Chrome DevTools Protocol (CDP), senza alcun binario WebDriver in mezzo, e la sua API è asincrona. Playwright e Puppeteer stanno in una categoria diversa. Anche loro controllano il browser tramite protocollo, non sono discendenti di WebDriver; le differenze davvero utili stanno nel design dell’API, nel packaging, nella distribuzione del browser e nella politica di compatibilità.
Questa recensione analizza nodriver 0.50.3 invece di provarlo contro difese reali. Ho esaminato il pacchetto installato, le importazioni, la superficie API, l’ingombro su disco e la licenza, poi ho avviato i browser solo su pagine servite da 127.0.0.1. Nessun target reale, nessun servizio anti-bot, nessun CAPTCHA è stato coinvolto. I risultati descrivono il comportamento del pacchetto e le informazioni che il browser espone di default; non dimostrano l’efficacia contro il rilevamento.
Dentro questi limiti, tre cose saltano subito all’occhio. Primo, su Python 3.14 la libreria non si importa proprio: un singolo byte fuori posto manda in crash l’intero pacchetto prima ancora che tu possa chiamare qualsiasi cosa. Secondo, per un driver di questa categoria è sorprendentemente compatto: tre dipendenze dichiarate più un pacchetto transitivo risolto, e circa 17 MB nell’ambiente misurato. Terzo, sulla mia pagina di test, l’unica proprietà in cui nodriver si distingue visibilmente da Playwright e Puppeteer stock è un booleano — e la user-agent string in modalità headless continua a riportare HeadlessChrome in tutti e tre i casi. Il resto delle sorprese sta nella licenza.
Il blocco di import su Python 3.14
Partiamo dal problema che ti colpirà per primo, perché succede prima ancora che il tuo codice entri in esecuzione. Su Python 3.14, un semplice import nodriver fallisce subito:
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
Il file che fallisce è cdp/network.py, generato automaticamente (l’intestazione dice # DO NOT EDIT THIS FILE!). Contiene un singolo byte non UTF-8, 0xb1, corrispondente al simbolo ± nel commento #: JSON (±Inf)., e non ha alcuna dichiarazione di encoding sorgente. Il modulo viene caricato attraverso nodriver/__init__ → cdp/__init__ → network, quindi l’errore di parsing blocca l’importazione. Una scansione del pacchetto non ha trovato altri file sorgente non UTF-8.
Il confine di versione va spiegato con attenzione. Python 3.14.2 rifiuta il file, mentre Python 3.12.13 lo importa senza patch. I file network.py installati nei due ambienti sono identici a livello di byte (SHA-256 ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6), quindi il risultato non dipende da artefatti sorgente diversi. Questa recensione non ha isolato il cambiamento esatto del tokenizer di CPython responsabile, e Python 3.13 non è stato testato. Riporto quindi i due risultati misurati dell’interprete, senza sostenere che ogni versione precedente si comporti come la 3.12.
Da quei due punti misurati non deriva alcuna conclusione su Python 3.13.
Non si tratta di una scoperta nuova, ma di una riproduzione. Lo stesso traceback di Python 3.14 è già documentato in nodriver issue #35, con una proposta di correzione nella pull request #36. La versione 0.50.3 contiene ancora quel byte. I metadati PyPI elencano classifier fino a Python 3.13 e non dichiarano supporto per 3.14.
La soluzione è piccola quanto il bug: usa una versione dell’interprete testata, oppure riconverti quel file in UTF-8, come propone la pull request #36. Dopo la riconversione, il pacchetto si importa e si introspeziona su 3.14 senza che emergano altri blocchi. L’esecuzione pulita su 3.12.13 mostrata sotto verifica un percorso non patchato; Python 3.13 non è stato testato in questa recensione.
Su Python 3.12.13, import nodriver riesce senza patch. Tutti i numeri relativi al browser riportati più avanti in questa recensione provengono da quell’installazione. I dati su ingombro e tempi di importazione usano invece la copia con il singolo byte corretto su 3.14, e sono etichettati di conseguenza. Python 3.13 resta non testato qui; un risultato su 3.12 non equivale a copertura dell’intera gamma precedente.
Se usi di default interpreter moderni — e molti team adottano subito la nuova versione di Python — questo è un ostacolo reale, anche se banalmente risolvibile. È bene saperlo prima, per evitare di perdere un pomeriggio dietro a un SyntaxError in un file che non hai mai scritto.
Cos’è davvero nodriver, sotto il cofano
La descrizione in una riga ("CDP-native, no webdriver") sembra marketing finché non guardi cosa c’è nel wheel. nodriver include un set completo di binding del DevTools Protocol: il pacchetto nodriver.cdp contiene 57 moduli di dominio del protocollo — accessibility, dom, network, page, fetch, runtime, target, storage, input, emulation e così via. Questo conteggio dei moduli è il meccanismo dietro la promessa "CDP diretto, niente Selenium". Invece di invocare un eseguibile chromedriver che parla WebDriver e fa da traduttore, nodriver genera oggetti Python per i domini CDP e parla il protocollo direttamente via WebSocket. cdp/network.py — il file con il byte errato — è uno di questi 57 moduli auto-generati, motivo per cui il problema vive in codice generato che nessuno modifica a mano.
Sopra questo livello di protocollo c’è un modello a oggetti più comodo. L’oggetto Tab espone 62 metodi pubblici, e la superficie di ricerca è più ampia di quella che molti driver offrono: ricerca testuale con find() e find_all(), CSS con select() e select_all(), e un punto d’accesso nativo xpath(). XPath, CSS e ricerca testuale sullo stesso oggetto è davvero comodo; altre librerie a volte costringono a scendere su evaluate() per XPath. Il costruttore Config espone user_data_dir, headless, browser_executable_path, browser_args, sandbox, lang (valore predefinito 'en-US'), host, port, expert, più **kwargs. Durante questa inventariazione dell’API, la costruzione di Config(headless=True) ha prodotto 16 flag di avvio di Chromium, tra cui --no-first-run, --no-default-browser-check, --remote-allow-origins=* e --homepage=about:blank. In questa fase non ho chiamato start(). I test del browser più avanti sono una sessione separata.

La libreria espone anche una superficie API orientata all’anti-rilevamento — metodi di cui ho confermato l’esistenza, ma di cui non ho verificato il comportamento contro alcun target. Lo segnalo una volta e poi passo oltre, perché il loro effetto su un servizio reale è esattamente ciò che ho scelto di non testare. In modo neutro, posso dire che la nomenclatura di nodriver è più sobria di quella di alcuni concorrenti marchiati "stealth". La sua narrazione è architetturale — CDP-native, profilo nuovo a ogni esecuzione — non una lista di nomi tipo detect_and_bypass. Consideralo un’osservazione sul design dell’API, non una dichiarazione sui risultati.
Questi conteggi arrivano dall’importazione del pacchetto e dall’introspezione con gli strumenti di Python — inspect, scansione dei moduli, conteggio degli attributi. Nessun sito è stato toccato. Se vuoi ripetere il test, i numeri provengono direttamente dal pacchetto, non da impressioni soggettive.
Cosa dichiara di sé

Ecco una domanda a cui puoi rispondere senza avvicinarti a una difesa reale: quando nodriver controlla un browser, cosa rivela quel browser alla pagina che sta visitando? Ho creato una pagina che legge gli elementi più ovvi — navigator.webdriver, user-agent, piattaforma, lingue, conteggio dei plugin e dell’hardware, struttura di window.chrome, cosa dice la Permissions API, geometria di finestra e schermo — l’ho servita su 127.0.0.1 e l’ho aperta con quattro stack: nodriver, Botasaurus, e come controlli Playwright stock e Puppeteer stock. Tutti e quattro guidavano la stessa build di Chrome (Chrome for Testing 151.0.7922.10), quindi tutto ciò che cambia dipende dalla libreria e non dal browser. Modalità headless e headed, tre run ciascuna. Ogni valore sotto è rimasto stabile su tutte e tre le esecuzioni.
| Stack | Modalità | navigator.webdriver | Token nella user-agent | 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"] |
La differenza più chiara è quel singolo booleano. Con queste configurazioni di avvio predefinite, nodriver riporta navigator.webdriver come false, mentre Playwright e Puppeteer stock riportano true, sia in headless sia in headed. Il test mette in evidenza una differenza a livello di configurazione; non dimostra che il valore derivi semplicemente dall’assenza di un binario WebDriver.
In tutti e quattro gli stack, la proprietà resta comunque il getter nativo del browser su Navigator.prototype — function get webdriver() { [native code] } — mai una proprietà propria dell’istanza né una funzione sostituita. La proprietà non è stata riscritta da JavaScript della pagina dopo il caricamento. Il test non ha identificato l’argomento di avvio o il percorso sorgente responsabile del valore diverso.
Il secondo dettaglio è quello che ridimensiona il marketing. In modalità headless, la user-agent di nodriver continua a dichiarare HeadlessChrome/151.0.0.0 — identico a Puppeteer stock, identico a Playwright stock. In modalità headed diventa Chrome/151.0.0.0, ancora in modo identico. Se pensavi che una libreria anti-detection mascherasse di default la stringa di auto-identificazione più famosa nell’automazione browser, non è così. Dovresti impostarlo tu.
Quasi tutto il resto era uguale su tutti e quattro gli stack, ed è importante dirlo chiaramente perché restringe la storia — ogni proprietà qui sotto ha restituito lo stesso valore su nodriver, Botasaurus, Playwright e Puppeteer:
| Proprietà | Valore identico su tutti e quattro gli stack |
|---|---|
platform | MacIntel |
vendor | Google Inc. |
| Plugin | cinque |
| Tipi MIME | due |
pdfViewerEnabled | true |
| Core logici | dodici |
| Memoria dispositivo riportata | 16 GB |
| Touch point | zero |
window.chrome | presente, con app/csi/loadTimes e senza runtime |
| Stringa del renderer WebGL | identica su tutti e quattro |
Il vecchio caso in cui la Permissions API e Notification.permission si contraddicono non è emerso da nessuna parte — tutti e quattro hanno riportato default e prompt in modo coerente. Ho anche cercato in document e window eventuali residui in stile cdc_, tipici dei vecchi stack WebDriver: assenti in tutti e quattro.
L’unico punto in cui nodriver sembra più simile a un browser automatizzato di base che a un controllo è la geometria della finestra. In headless, nodriver riporta outerWidth/outerHeight pari a 0×0 su uno schermo 800×600; Playwright headless riporta 1280×720 perché imposta per te un viewport. Puppeteer si comporta come nodriver e resta a 0×0. È una differenza di configurazione predefinita, non di capacità, e la puoi cambiare.
Un’ultima cosa da sapere prima di metterlo in produzione: nodriver non include un browser, quindi di default usa quello già presente sulla tua macchina. Nel mio caso ha rilevato automaticamente /Applications/Google Chrome.app — Chrome 150.0.7871.187 — e la user-agent esposta era quella versione, non una versione fissata. La versione del browser che il tuo parco macchine dichiara è semplicemente quella che è installata sulle tue macchine.
Chiariamo cosa è e cosa non è. È un resoconto di ciò che un sistema automatizzato dichiara quando nessuno gli chiede di nascondersi — utile se sei dal lato della difesa, utile se vuoi sapere cosa trasmettono i tuoi strumenti. Non è una misura di quanto tutto ciò conti per un servizio specifico. Non l’ho testato, e nessuna riga sopra va letta come se implicasse un esito.
Riesce davvero a estrarre contenuti da una pagina?
Dichiararsi è una cosa; restituire l’HTML giusto è il vero lavoro. Ho eseguito nodriver sullo stesso fixture a tre classi di contenuto usato dal resto di questo benchmark, così i numeri si allineano con tutti gli altri strumenti misurati qui. La pagina contiene tre elementi: A, un link statico il cui marcatore è un letterale nei byte serviti; B, un nodo creato da uno script inline durante il parsing, con marcatore e URL assemblati da frammenti in modo che solo l’esecuzione del JavaScript li riveli; e C, un nodo iniettato 800 ms dopo l’evento load, costruito nello stesso modo. La classe C è la parte avversaria: una lettura presa all’evento load non può vederla.
| Stack | Lettura predefinita | Con un'attesa esplicita |
|---|---|---|
| nodriver 0.50.3 | 2 su 3 (A + B, manca C) | 3 su 3 |
| Botasaurus 4.0.92 | 2 su 3 | 3 su 3 |
| Playwright 1.56.0 | 2 su 3 | 3 su 3 |
| Puppeteer 24.16.0 | 2 su 3 | 3 su 3 |
nodriver arriva esattamente dove arrivano i pesi massimi. browser.get() seguito direttamente da tab.get_content() è uno snapshot al momento del load: esegue correttamente il JavaScript — la classe B lo dimostra, perché la classe B non esiste nei byte serviti eppure viene recuperata — ma non vede nulla che venga iniettato dopo il caricamento. Aggiungendo tab.select("#delayed-injected") ottieni tutte e tre. Stesso problema, stessa soluzione, come in Playwright e Puppeteer. Risultato stabile in tre repliche e tre run separati dell’intera suite, senza flakiness.
Ho variato il ritardo di iniezione per trovare il punto in cui la lettura predefinita rinuncia. nodriver smette di vedere la classe C quando l’iniezione avviene 100 ms o più dopo il load — la stessa soglia dei due controlli stock. (Botasaurus qui è l’eccezione, ed è la differenza davvero interessante tra i due strumenti anti-detection: il suo get() attende per impostazione predefinita il caricamento completo della pagina, quindi la lettura di default intercetta ancora l’iniezione a 300 ms. Il costo è di circa 250 ms per navigazione.)
L’attesa stessa ha una particolarità da mettere a budget. In questo sweep, tab.select() di nodriver si è mosso a blocchi di polling grossolani invece di seguire da vicino il ritardo di iniezione:
| Ritardo della classe 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 |
In questo fixture, un’iniezione a 100 ms ha portato a circa 1,1 secondi per select(). Il loop installato esegue await self e poi await self.sleep(0.5) dopo un mancato riscontro; il ciclo combinato misurato qui si è avvicinato a un secondo, ma await self non è stabilito come uno sleep universale di durata fissa. Tutti i nodi ritardati testati sono stati trovati. Il waiter di Puppeteer ha seguito questi ritardi in modo più preciso. Molte attese in serie potrebbero amplificare la differenza, anche se questa recensione non ha valutato una pagina di produzione con trenta selettori.
L’avvio è l’altro punto in cui il tema async e leggero incontra la realtà. Portare su il browser ha collocato nodriver nella stessa fascia di Botasaurus e Puppeteer, e ben dietro Playwright:
| Stack | Avvio browser, su più esecuzioni |
|---|---|
| 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 |
Una volta avviato, il percorso naviga-e-leggi di nodriver è il più rapido dei quattro: 119–129 ms. Leggero al volante, ordinario all’accensione.
Installazione e ingombro: la parte davvero buona
Qui nodriver guadagna davvero l’etichetta di "mirato", e si tratta di un dato di installazione benigno e verificabile, non di qualcosa che riguardi lo scraping. Questo è ciò che risolve un pip install nodriver pulito:
| Dato di installazione | nodriver 0.50.3 |
|---|---|
| Dipendenze runtime dirette dichiarate | tre — websockets, mss, deprecated |
| Dipendenza transitiva risolta | wrapt (tramite deprecated) |
Totale misurato di site-packages | circa 17,2 MB su 6 directory dist-info, incluso pip dell’ambiente |
| Quota di nodriver | 3,7 MB |
| numpy / lxml | nessuno dei due |
| Binario del browser al momento dell’installazione | nessun download |
Per una categoria che di solito si porta dietro uno stack di rendering, è una soluzione leggera.
La distinzione tra pacchetti diretti e transitivi conta per la manutenzione. I metadati di nodriver richiedono mss, websockets e deprecated; wrapt arriva perché deprecated lo richiede. La sesta directory dist-info nell’ambiente da 17,2 MB è pip, già presente in quel virtual environment. Quindi "17,2 MB su sei distribuzioni" descrive l’ambiente misurato, mentre "quattro pacchetti runtime aggiunti" descrive ciò che l’installazione ha risolto. Sono conteggi collegati, ma non equivalenti.
Il confronto che rende il numero significativo: sulla stessa macchina, il framework fratello Botasaurus pesa 122,3 MB su 44 pacchetti, cioè circa sette volte l’ingombro su disco. Questa è la differenza tra un driver CDP focalizzato e un framework completo, e funziona in entrambe le direzioni. nodriver ti offre un albero di dipendenze sottile e leggibile, davvero verificabile; Botasaurus ti dà di più già pronto, ma ti addebita spazio e superficie di dipendenza. Nessuno dei due è "migliore" in astratto — dipende se vuoi un driver o un framework — ma se dai valore a un’installazione piccola e ispezionabile, nodriver è insolitamente pulito per ciò che fa.
L’ingombro ridotto su disco non implica un import leggero. Sulla copia 3.14 con il byte corretto, import nodriver ha richiesto circa 158 ms (mediana su importazioni in subprocess fresh, circa 151–199 ms). L’import non patchato su 3.14 fallisce, quindi non viene riportato alcun tempo per quel caso. I 57 moduli CDP vengono caricati subito, e la memoria residente dopo l’import era 31,5–31,9 MB prima dell’esistenza di qualsiasi processo Chrome. Un browser avviato aggiungerebbe molto di più e non è stato incluso in questa misurazione della memoria.
Due precisazioni da applicare a quei numeri. Provengono da una sola macchina — macOS arm64 — e i dati su ingombro e tempo di importazione sono stati raccolti su Python 3.14 con la copia corretta di un singolo byte, perché il pacchetto non patchato su quell’interprete non si importa proprio. Su una versione supportata di Python la patch non serve: ho confermato un import pulito e non patchato su 3.12.13, che è il contesto da cui provengono tutte le misurazioni del browser. E a runtime serve comunque un vero browser Chrome, Chromium, Edge o Brave — nodriver controlla un browser esistente, non ne installa uno, quindi quel costo vive fuori dai 17 MB; e, come mostrato nella sezione sulla disclosure, la versione di Chrome che la tua macchina ha installato è quella che il traffico dichiara.
La licenza è il vero fattore decisivo per l’adozione

La maggior parte delle recensioni di uno strumento gratuito tratta "è open source" come fine del discorso sulla licenza. Per nodriver è solo l’inizio, perché la licenza è AGPL-3.0 — confermata sia nel LICENSE.txt del wheel sia nello spdx_id del repository. È una forte licenza copyleft di rete, e comporta un impegno molto diverso rispetto a ciò che chiedono i vicini.
I progetti con cui di solito si confronta nodriver sono permissivi, e proprio per questo il contrasto è concreto:
| Strumento | Licenza | Se esegui una copia modificata come servizio di rete |
|---|---|---|
| nodriver | AGPL-3.0 | la Sezione 13 può richiedere all'operatore di offrire agli utenti remoti il sorgente corrispondente della versione modificata |
| Playwright | Apache-2.0 | nessuna clausola equivalente di network copyleft |
| Puppeteer | Apache-2.0 | come sopra |
| Botasaurus | MIT | come sopra |
Il perimetro conta. La Sezione 13 dell’AGPL si applica a una versione modificata del programma coperto, usata per interazioni di rete da remoto. Questa recensione non stabilisce se il codice del servizio circostante rientri o meno nel perimetro dell’opera coperta, né risolve casi limite di uso interno o tra confini aziendali. Se un prodotto ospitato modifica nodriver, esamina il testo della licenza e l’architettura con il tuo legale. Questo è un segnale tecnico per l’adozione, non consulenza legale.
Non sto giudicando se l’AGPL sia buona o cattiva — il copyleft è una scelta legittima e molti progetti seri lo adottano. Sto segnalando che "nodriver è free e open source" è vero ma incompleto. L’obbligo esiste, è diverso dal default permissivo di questo angolo dell’ecosistema, e va considerato nella decisione invece di essere ridotto a "gratuito". (PyPI, per quel che vale, non mostra alcun classifier di licenza; il testo AGPL è nel wheel e l’ID SPDX è nel repository, quindi non fare affidamento sull’indice dei pacchetti per scoprire questo dettaglio.)
I metadati, con data
Numeri puntuali del repository e del pacchetto, direttamente dalle API GitHub e da PyPI:
| Dato | Valore al 14 luglio 2026 |
|---|---|
| Star | 4.511 |
| Fork | 422 |
| Issue aperte | 14 |
| Creato | febbraio 2024 |
| Ultimo push | maggio 2026 |
| Ultima release PyPI | 0.50.3 |
| Wheel | puro Python py3-none-any |
requires-python | >=3.9 |
| Classifier Python | 3.7–3.13 |
I segnali di manutenzione osservabili sono misti: il repository ha ricevuto un push a maggio 2026, mentre il pacchetto più recente misurato conteneva ancora il problema di importazione su Python 3.14 e la correzione proposta non era ancora stata rilasciata alla data della ricerca. Star e conteggio delle issue aperte non bastano da soli a stabilire se quel ritmo soddisfi la tua soglia di manutenzione.
Pro e contro
Pro:
- Installazione piccola e leggibile: tre dipendenze dichiarate più
wrapttransitiva, circa 17,2 MB su sei directorydist-infonell’ambiente misurato (una èpip), nessun numpy/lxml e nessun download del browser all’installazione. - Veramente CDP-native: include 57 moduli di binding del DevTools Protocol e parla il protocollo direttamente, senza binario chromedriver/Selenium nel mezzo.
- Ampia superficie di ricerca elementi su
Tab(62 metodi pubblici) con XPath, CSS e ricerca testuale di prima classe — senza dover ricorrere aevaluate()grezzo per XPath. - Asincrono per progettazione, profilo nuovo a ogni esecuzione e un’interfaccia
Configpulita per i parametri comuni (headless, percorso eseguibile, argomenti, lingua, porte). - Estrae contenuti da una pagina costruita in JavaScript con la stessa competenza dei pesi massimi: 2 su 3 classi di contenuto nella lettura predefinita, 3 su 3 con un’attesa esplicita — identico a Playwright e Puppeteer stock sullo stesso fixture, stabile su tre run. Navigate-and-read è stato il più veloce dei quattro, a 119–129 ms.
- Riporta
navigator.webdrivercomefalsedi default, laddove i controlli stock riportanotrue, senza patchare la proprietà — il descriptor resta il getter nativo del browser. - Identità architetturale chiara: successore CDP-native di undetected-chromedriver.
Contro:
- Non si importa su Python 3.14 out of the box — un file non UTF-8 di un solo byte (
cdp/network.py) generaSyntaxErrorall’importazione. Riprodotto sull’issue aperta #35, ancora non risolto in 0.50.3. Fissa ≤3.13 (verificato pulito su 3.12.13) oppure riconverti il file. - AGPL-3.0 è un vero fattore di adozione per chiunque esegua una copia modificata come servizio di rete — più restrittiva dei peer Apache/MIT.
- Il piccolo footprint su disco non significa import leggero: circa 158 ms di cold-start e ~31,5 MB residenti prima che esista qualsiasi browser, perché tutti i 57 moduli CDP vengono caricati subito.
tab.select()usa un polling con back-off di mezzo secondo, quindi le attese brevi si arrotondano verso l’alto — 100 ms di attesa costano circa 1,1 s, mentre il waiter di Puppeteer costa circa 210 ms. Corretto sempre, ma il costo cresce con molte attese brevi.- Le esecuzioni headless continuano a dichiarare
HeadlessChromenella user-agent di default, esattamente come i controlli stock; la configurazione predefinita non nasconde la stringa di auto-identificazione più ovvia. - Serve ancora un vero binario Chrome/Chromium/Edge/Brave a runtime; la piccola installazione via pip è solo metà della storia delle dipendenze, e la versione di Chrome presente sulla tua macchina è quella che il traffico dichiara.
- L’efficacia contro qualsiasi sistema anti-bot qui non è verificata — l’intera ipotesi stealth non è stata testata per scelta.
Ciò che non ho testato, e quindi su cui non posso esprimermi: memoria per tab, latenza round-trip CDP, gestione dei profili, throughput su larga scala, Linux o Windows, Python 3.13 nello specifico e — il punto principale — efficacia reale contro anti-bot su servizi live. In questa recensione ogni browser ha parlato solo con un fixture su 127.0.0.1. Tutti i numeri provengono da una sola macchina (macOS arm64); le metriche di browser-driving sono su Python 3.12.13 senza patch, mentre i dati più vecchi su footprint e tempi di importazione sono su Python 3.14 con la copia corretta di un byte.
Non ho nemmeno eseguito una matrice di upgrade del browser, quindi la compatibilità con future release di Chrome resta un controllo operativo per chi adotta, non un risultato di questa recensione.
Per chi è adatto, e chi dovrebbe evitarlo
nodriver è adatto se vuoi un driver Chromium leggero, asincrono e CDP-native, e sei disposto a gestire browser, aggiornamenti e runtime. L’albero delle dipendenze ridotto è più facile da auditare, e il design CDP-first si presta al controllo a livello di protocollo. La compatibilità con i container resta non testata: questa recensione non ha esercitato immagini Linux, installazione del browser, librerie condivise, sandboxing o cleanup dei processi.
Due gruppi dovrebbero guardare altrove. Se sei su Python 3.14 e non vuoi fissare la versione dell’interprete o patchare un file vendorizzato, aspetta il rilascio della correzione — oggi il blocco di importazione è un hard stop. E se l’AGPL-3.0 è un problema per il modo in cui vuoi distribuire il tuo prodotto — per esempio un servizio ospitato con modifiche private — la licenza da sola è già un motivo sufficiente per valutare un’alternativa permissiva prima di costruire sopra questa libreria. Nessuno dei due punti è un difetto del codice; sono vincoli che è meglio conoscere ora che durante una revisione di conformità.
Evitalo anche se ciò che ti serve davvero sono dati, non un browser da pilotare a mano. nodriver ti offre una tab scriptabile e 62 metodi; trasformare una pagina renderizzata in record puliti e strutturati resta comunque codice che devi scrivere tu. È un altro lavoro, ed è il punto in cui entra un’API gestita.
Alternative e dove si colloca Thunderbit
Il quadro onesto prima di tutto: nodriver è gratuito, AGPL e self-hosted. Il browser lo esegui tu, gli aggiornamenti li gestisci tu, il runtime e ogni suo errore li possiedi tu. Per uno sviluppatore che vuole esattamente quel livello di controllo, nessun servizio gestito può competere sul prezzo con una libreria che hai già in casa.
Nel mondo open source, confronta per compito e non per logo. Se stai valutando driver per browser reali, il nostro confronto tra Playwright e Puppeteer copre i due pesi massimi più ovvi accanto a nodriver. Scrapling è il vicino Python più vicino sull’asse orientato allo stealth, se è questo il motivo della tua ricerca. Per output già pronti per LLM invece del controllo grezzo del browser, Crawl4AI renderizza le pagine e restituisce Markdown, mentre Scrapy resta il framework di riferimento per grandi crawl senza browser. Se stai confrontando più strumenti insieme, la rassegna degli scraper open source mette le categorie una accanto all’altra.
Dichiarazione: questo articolo è pubblicato da Thunderbit. Thunderbit è un servizio gestito di estrazione, quindi il confronto è basato sul compito e non sull’architettura. nodriver ti offre un livello di controllo del browser che ospiti e programmi tu; Thunderbit gestisce il rendering e restituisce contenuti di pagina o record strutturati come servizio. Usa nodriver quando il controllo del browser a livello di protocollo e l’hosting proprio sono requisiti. Considera un estrattore gestito quando il record di output e la delega operativa contano più del possesso del browser. I dettagli variabili su endpoint, crediti e limiti batch appartengono alla pagina prezzi, non dentro un benchmark di libreria.
Il compromesso sta in dove vive il lavoro: nodriver lascia a te la gestione del browser e la manutenzione del runtime senza una fee per richiesta; una API gestita prende in carico quel livello e addebita le chiamate.
Prova Thunderbit per l'estrazione di dati dal Web
Verdetto
Usa nodriver se vuoi un driver Chromium piccolo, asincrono e CDP-native, hai verificato che funzioni sulla tua versione di Python (qui 3.12.13 si importa senza problemi) e hai valutato l’AGPL-3.0 in relazione al modo in cui distribuisci il tuo prodotto. Il grafo runtime risolto aggiunge quattro pacchetti, l’ambiente misurato occupa circa 17 MB, e la libreria include 57 moduli CDP con una Tab da 62 metodi e XPath nativo. Su questo fixture ha restituito 2 su 3 classi di contenuto in modalità predefinita e 3 su 3 con un’attesa, in linea con Playwright e Puppeteer stock.
Bisogna però pesare onestamente i limiti. Su Python 3.14 non si importa affatto finché non correggi un byte non UTF-8 — un problema documentato e ancora aperto, non un mistero, ma uno stop netto il giorno in cui lo incontri. La licenza è AGPL-3.0, una decisione reale per chiunque esegua una copia modificata come servizio, non una formalità. L’installazione piccola non compra un import piccolo, perché tutti quei moduli CDP vengono caricati in anticipo, e il polling a mezzo secondo di select() fa sì che le attese brevi costino circa un secondo ciascuna. Sulla domanda di disclosure predefinita che ho potuto effettivamente misurare, il quadro è più ristretto di quanto suggerisca il marketing: un solo booleano differisce da Puppeteer stock, la user-agent headless continua a dire HeadlessChrome, e ogni altra proprietà misurata era identica su tutti e quattro gli stack. E l’intera premessa anti-detection — il motivo per cui molte persone scoprono nodriver — è qualcosa che ho volutamente lasciato non testato. Ho inventariato la libreria e l’ho fatta girare su una pagina della mia macchina, invece di opporla a difese reali, e preferisco dirtelo chiaramente piuttosto che offrirti una promessa di bypass che non posso sostenere. Sulle domande a cui ho potuto rispondere, nodriver è un driver ben costruito e insolitamente leggero, con due bordi netti — un muro sulla versione di Python e una licenza copyleft — che conviene vedere arrivare.
Prova Thunderbit per l'estrazione di dati dal Web Get Started Free
FAQ
Perché import nodriver fallisce su Python 3.14?
Perché cdp/network.py contiene un byte ± non UTF-8 senza dichiarazione di encoding sorgente. Python 3.14.2 rifiuta il file e interrompe l’importazione transitiva; Python 3.12.13 importa senza problemi lo stesso file identico a livello di byte, senza patch. Questa recensione non ha isolato il cambiamento esatto dell’interprete e non ha testato Python 3.13. Il percorso upstream è nodriver issue #35 e pull request #36. Usa una versione che hai testato, oppure riconverti il file in UTF-8.
Cosa ti dà il fatto di essere "CDP-native, no webdriver", e quanto costa installarlo?
nodriver include 57 moduli di dominio del DevTools Protocol e parla CDP via WebSocket invece di invocare chromedriver tramite Selenium. Tab espone 62 metodi, incluso XPath nativo. I metadati del pacchetto dichiarano tre requisiti runtime (websockets, mss, deprecated); la risoluzione aggiunge wrapt. L’ambiente misurato occupava circa 17,2 MB su sei directory dist-info, incluso pip, senza numpy, lxml o binari del browser scaricati. Due precisazioni: l’import della copia patchata ha richiesto circa 158 ms perché tutti i 57 moduli CDP vengono caricati subito, e serve comunque separatamente un browser della famiglia Chrome.
La licenza AGPL-3.0 conta davvero per il mio progetto? Dipende da come distribuisci. AGPL-3.0 è una licenza copyleft di rete: se esegui una versione modificata di nodriver come servizio usato da altri, sei tenuto a offrire a quegli utenti il sorgente modificato. Per uno script personale o uno strumento interno che non esponi mai, il problema non si pone. Per un prodotto commerciale ospitato e basato su un nodriver modificato, è una questione reale da discutere con chi si occupa di compliance — ed è più restrittiva delle licenze Apache-2.0 e MIT dei tool comparabili.
Cosa espone nodriver di sé — e questo significa che batte Cloudflare?
La prima metà è misurata; la seconda no, e la differenza è fondamentale. Su una pagina che ho servito da 127.0.0.1, usando la stessa build di Chrome dei controlli: navigator.webdriver torna false, mentre Playwright stock e Puppeteer stock restituiscono entrambi true. Quel valore è impostato al momento dell’avvio del browser, non patchando la proprietà — il descriptor resta il getter nativo di Chrome. Oltre a quel booleano, quasi tutto coincideva con i controlli: stessa stringa di piattaforma, cinque plugin, dodici core, 16 GB di memoria dispositivo riportata, stessa struttura di window.chrome, nessuna contraddizione nella Permissions API e nessun residuo in stile cdc_ su document o window. Le esecuzioni headless continuano a dichiarare HeadlessChrome/151.0.0.0 nella user-agent, come entrambi i controlli — questo non viene mascherato per te. Tutto ciò però non dice se funzioni davvero contro un servizio anti-bot reale. Non ho mai puntato nodriver a un sito live, non ho mai contattato un servizio anti-bot e non ho mai toccato un CAPTCHA; era fuori scope per scelta. La tabella sulla disclosure sopra dice cosa dichiara lo stack. Non dice chi ascolta né cosa ne faccia.
nodriver gestisce correttamente contenuti renderizzati in JavaScript?
Sì, con la solita precisazione su quando fai la lettura. Su un fixture con tre classi di contenuto, una browser.get() + tab.get_content() predefinita ha restituito 2 su 3 — esegue correttamente JavaScript (la classe iniettata in modo sincrono non esiste nei byte serviti ed è comunque arrivata), ma legge all’evento load, quindi perde ciò che viene iniettato dopo. Aggiungendo tab.select("#delayed-injected") ottieni 3 su 3. È identico a Playwright stock e Puppeteer stock sulla stessa pagina. Tieni conto di un dettaglio: select() usa un polling con back-off di mezzo secondo, quindi un’attesa di 100 ms costa circa 1,1 secondi.


