Reseña de nodriver: un driver CDP diminuto que no se puede importar en Python 3.14

Última actualización: August 18, 2026
Reseña de nodriver: un driver CDP diminuto que no se puede importar en Python 3.14
Resumen con IA
nodriver is a Python browser-automation library from ultrafunkamsterdam, the author behind undetected-chromedriver, and it bills itself as that project's successor. Its architectural break is specifically from Selenium and chromedriver: nodriver talks to Chromium over the Chrome DevTools Protocol (CDP), with no WebDriver binary in the loop, and its API is async. Playwright and Puppeteer are a different comparison class. They are also protocol-driven browser controllers, not WebDriver descendants; the useful differences are API design, packaging, browser provisioning, and compatibility policy. This review inventories nodriver 0.50.3 rather than testing it against live defenses.

nodriver es una biblioteca de automatización de navegadores en Python creada por ultrafunkamsterdam, el autor de undetected-chromedriver, y se presenta como la sucesora de ese proyecto. Su ruptura arquitectónica es, sobre todo, con Selenium y chromedriver: nodriver se comunica con Chromium a través de Chrome DevTools Protocol (CDP), sin binario de WebDriver de por medio, y su API es asíncrona. Playwright y Puppeteer pertenecen a otra categoría de comparación. También controlan el navegador mediante protocolos, pero no descienden de WebDriver; las diferencias útiles están en el diseño de la API, el empaquetado, el aprovisionamiento del navegador y la política de compatibilidad.

Esta reseña inventaría nodriver 0.50.3 en lugar de ponerlo a prueba contra defensas reales. Medí el paquete instalado, las importaciones, la superficie de la API, el peso en disco y la licencia, y luego lancé navegadores solo contra páginas servidas en 127.0.0.1. No intervino ningún destino en vivo, servicio anti-bot ni CAPTCHA. Los resultados describen el comportamiento del paquete y la divulgación predeterminada del navegador; no demuestran eficacia antidetención.

Tres cosas destacaron dentro de ese límite. Primero, en Python 3.14 la biblioteca no llega ni a importarse: un solo byte fuera de UTF-8 rompe todo el paquete antes de que puedas llamar a nada. Segundo, para un driver de esta categoría es sorprendentemente pequeño: tres dependencias directas declaradas más un paquete transitivo resuelto, y unos 17 MB en el entorno medido. Tercero, en mi propia página, la única propiedad en la que nodriver difiere de forma visible de Playwright y Puppeteer estándar es un booleano —y la cadena de user-agent en modo headless sigue diciendo HeadlessChrome en los tres. El resto de las sorpresas está en la licencia.

El fallo de importación en Python 3.14

Empieza por el hallazgo que te va a golpear primero, porque ocurre antes de que se ejecute tu código. En Python 3.14, un simple import nodriver falla de forma directa:

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

El archivo que falla es cdp/network.py, generado automáticamente (su cabecera dice # DO NOT EDIT THIS FILE!). Contiene un byte no UTF-8, 0xb1, correspondiente al ± en el comentario #: JSON (±Inf)., y no incluye declaración de codificación de origen. El módulo se carga a través de nodriver/__init__cdp/__init__network, así que el fallo de parseo aborta la importación. Un análisis del paquete no encontró ningún otro archivo fuente no UTF-8.

El límite entre versiones requiere una formulación cuidadosa. Python 3.14.2 rechaza el archivo, mientras que Python 3.12.13 lo importa sin parche. Los archivos network.py instalados son idénticos a nivel de bytes en esos entornos (SHA-256 ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6), así que no se trata de artefactos de origen distintos. Esta reseña no aisló el cambio exacto del tokenizador de CPython responsable, y Python 3.13 no fue probado. Por tanto, informa de los dos resultados medidos sin afirmar que toda versión anterior se comporte como 3.12.

No se puede inferir nada sobre Python 3.13 a partir de esos dos puntos medidos.

Esto es una reproducción, no un descubrimiento. El mismo traceback de Python 3.14 aparece documentado en nodriver issue #35, con una posible corrección en pull request #36. La versión 0.50.3 todavía contiene el byte problemático. Los metadatos de PyPI listan clasificadores hasta Python 3.13 y no reclaman compatibilidad con 3.14.

La solución es tan pequeña como el error: usar una versión de intérprete probada o volver a codificar ese archivo a UTF-8, tal como propone el pull request #36. Tras la recodificación, el paquete se importa y se inspecciona en 3.14 sin que aparezca un segundo bloqueo. La ejecución limpia en 3.12.13 que verás abajo verifica una ruta sin parche; Python 3.13 no se probó en esta reseña.

En Python 3.12.13, import nodriver funciona sin parche. Todos los datos de conducción del navegador que aparecen más adelante en esta reseña proceden de esa instalación. Las cifras de tamaño y tiempo de importación usan la copia con un byte recodificado en 3.14 y están etiquetadas como tales. Python 3.13 sigue sin probarse aquí; un único resultado en 3.12 no equivale a cobertura de todo el rango anterior.

Si usas intérpretes modernos por defecto —y muchos equipos se suben rápido a una nueva versión de Python—, esto es una pared real, aunque fácil de arreglar. Conviene saberlo para no perder una tarde con un SyntaxError en un archivo que nunca escribiste.

Qué es realmente nodriver por dentro

La descripción de una línea ("nativo de CDP, sin webdriver") suena a marketing hasta que miras lo que trae el wheel. nodriver incluye su propio conjunto completo de bindings del DevTools Protocol: el paquete nodriver.cdp contiene 57 módulos de dominios del protocolo —accessibility, dom, network, page, fetch, runtime, target, storage, input, emulation y así sucesivamente. Ese conteo de módulos es el mecanismo detrás de la afirmación "CDP directo, sin Selenium". En lugar de lanzar un ejecutable chromedriver que habla WebDriver y traduce por ti, nodriver genera objetos Python para los dominios CDP y habla el protocolo directamente por WebSocket. cdp/network.py —el archivo con el byte defectuoso— es uno de esos 57 módulos autogenerados, por eso el fallo vive en código generado que nadie edita a mano.

Encima de esa capa de protocolo hay un modelo de objetos más amigable. El objeto Tab expone 62 métodos públicos, y la superficie para localizar elementos es más amplia de lo que la mayoría de drivers se molestan en ofrecer: coincidencia de texto con find() y find_all(), CSS con select() y select_all(), y un punto de entrada nativo xpath(). XPath, CSS y búsqueda de texto en el mismo objeto resulta realmente cómodo; en otras bibliotecas a veces te obligan a bajar a evaluate() para XPath. El constructor Config expone user_data_dir, headless, browser_executable_path, browser_args, sandbox, lang (por defecto 'en-US'), host, port, expert y **kwargs. Durante este inventario de la API, construir Config(headless=True) produjo 16 flags de lanzamiento de Chromium, incluyendo --no-first-run, --no-default-browser-check, --remote-allow-origins=* y --homepage=about:blank. En esa fase no llamé a start(). Las pruebas de navegador que aparecen más abajo fueron una ejecución aparte.

System diagram: Direct CDP Control Path

La biblioteca sí expone una superficie de API orientada a la antidetención —métodos cuya existencia confirmé, pero cuyo comportamiento no ejercité contra ningún destino. Lo menciono una vez y sigo, porque su efecto en un servicio real es justo lo que decidí no probar. Digo esto de forma neutral: la nomenclatura de nodriver es más contenida que la de algunos competidores con marca de stealth. Su historia es arquitectónica —nativo de CDP, perfil nuevo en cada ejecución— más que una ristra de nombres estilo detect_and_bypass. Tómalo como una observación sobre diseño de API, no como una afirmación sobre resultados.

Esos conteos salieron de importar el paquete y usar la introspección de Python —inspect, recorrido de módulos, conteo de atributos. No se tocó ningún sitio. Si quieres repetirlo, los números salen directamente del paquete; no son impresiones subjetivas.

Qué anuncia sobre sí mismo

Measured results chart: Browser disclosure fields in the tested stacks

Hay una pregunta que puedes responder sin acercarte a ninguna defensa real: cuando nodriver controla un navegador, ¿qué le cuenta ese navegador a la página que está mirando sobre sí mismo? Yo escribí una página que lee lo obvio —navigator.webdriver, el user-agent, la plataforma, los idiomas, los recuentos de plugins y hardware, la forma de window.chrome, lo que dice la Permissions API, y la geometría de ventana y pantalla—, la serví en 127.0.0.1 y apunté cuatro stacks hacia ella: nodriver, Botasaurus, y Playwright y Puppeteer estándar como controles. Los cuatro controlaron la misma compilación de Chrome (Chrome for Testing 151.0.7922.10), así que cualquier diferencia depende de la biblioteca y no del navegador. Headless y con interfaz, tres ejecuciones cada uno. Todos los valores de abajo se mantuvieron en las tres pruebas.

StackModenavigator.webdriverUser-agent tokennavigator.languages
nodriver 0.50.3headlessfalseHeadlessChrome/151.0.0.0["en-US"]
nodriver 0.50.3headedfalseChrome/151.0.0.0["en-US"]
Botasaurus 4.0.92headless / headedfalseHeadlessChrome/151 / Chrome/151["en-US"]
Playwright 1.56.0headless / headedtrueHeadlessChrome/151 / Chrome/151["en-US","en"]
Puppeteer 24.16.0headless / headedtrueHeadlessChrome/151 / Chrome/151["en-US"]

La diferencia más clara es ese único booleano. Con estas configuraciones de lanzamiento por defecto, nodriver reporta navigator.webdriver como false, mientras que Playwright y Puppeteer estándar reportan true, tanto en headless como en headed. La prueba identifica una diferencia a nivel de configuración; no demuestra que ese valor se deba solo a la ausencia de un binario WebDriver.

En los cuatro stacks, la propiedad sigue siendo el getter nativo del navegador en Navigator.prototypefunction get webdriver() { [native code] }— y nunca una propiedad propia de la instancia ni una función reemplazada. El JavaScript de la página no reescribió la propiedad después de cargar. La prueba no enumeró el argumento de lanzamiento ni la ruta de código responsables del valor distinto.

El segundo detalle es el que matiza el marketing. En modo headless, el user-agent de nodriver sigue anunciando HeadlessChrome/151.0.0.0 —idéntico a Puppeteer estándar, idéntico a Playwright estándar. Si ejecutas en modo con interfaz, pasa a Chrome/151.0.0.0, otra vez de forma idéntica. Si pensabas que una biblioteca antidetención oculta por defecto la cadena autoidentificativa más famosa de la automatización de navegadores, no lo hace. Eso tendrías que configurarlo tú.

Casi todo lo demás fue igual en los cuatro stacks, y conviene decirlo de forma explícita porque acota la historia: todas las propiedades siguientes devolvieron lo mismo en nodriver, Botasaurus, Playwright y Puppeteer:

PropertyValue, identical on all four stacks
platformMacIntel
vendorGoogle Inc.
Pluginsfive
MIME typestwo
pdfViewerEnabledtrue
Logical corestwelve
Reported device memory16 GB
Touch pointszero
window.chromepresent, with app/csi/loadTimes and no runtime
WebGL renderer stringidentical across all four

El viejo tópico de la contradicción entre la Permissions API y Notification.permission no apareció en ningún caso: los cuatro informaron default y prompt de forma coherente. También busqué en document y window restos estilo cdc_, conocidos en antiguos stacks de WebDriver: vacío en los cuatro.

El único punto en que nodriver parece más un navegador automatizado desnudo que un control es la geometría de ventana. En headless, nodriver reporta outerWidth/outerHeight de 0×0 sobre una pantalla de 800×600; Playwright headless reporta 1280×720 porque te configura un viewport. Puppeteer queda junto a nodriver en 0×0. Esa es una diferencia de configuración por defecto, no de capacidad, y puedes cambiarla.

Una última cosa que conviene saber antes de desplegar: nodriver no incluye navegador, así que por defecto usa el Chrome que ya tengas instalado en tu máquina. En la mía eso significó que detectó automáticamente /Applications/Google Chrome.app —Chrome 150.0.7871.187—, y el user-agent que divulgó fue esa versión, no una fijada. La versión de navegador que ve tu infraestructura es la que tenga instalada tu flota.

Dicho claramente: esto es un registro de lo que divulga un stack automatizado cuando nadie le ha pedido que oculte nada; útil si estás del lado de la defensa, útil si quieres saber qué transmite tu propia herramienta. No es una medida de si eso importa o no para un servicio concreto. No lo probé, y ninguna fila de arriba debe leerse como si implicara un resultado.

¿Puede realmente sacar contenido de una página?

Anunciarse es una cosa; devolver el HTML correcto es el trabajo. Ejecuté nodriver contra el mismo fixture de tres clases de contenido que usa el resto de este repositorio de benchmark, así que los números se alinean con el resto de herramientas medidas aquí. La página contiene tres elementos: A, un enlace estático cuyo marcador es un literal en los bytes servidos; B, un nodo construido por un script inline durante el parseo, con su marcador y su URL ensamblados a partir de fragmentos para que solo al ejecutar el JavaScript aparezcan; y C, un nodo inyectado 800 ms después del evento load, construido igual. La clase C es la adversarial: una lectura tomada en load no puede verla.

StackDefault readWith an explicit wait
nodriver 0.50.32 of 3 (A + B, misses C)3 of 3
Botasaurus 4.0.922 of 33 of 3
Playwright 1.56.02 of 33 of 3
Puppeteer 24.16.02 of 33 of 3

nodriver cae exactamente donde caen los pesos pesados. browser.get() seguido directamente de tab.get_content() es una instantánea en el evento load: renderiza bien JavaScript —la clase B lo demuestra, porque la clase B no existe en los bytes servidos—, pero se pierde cualquier cosa inyectada después de la carga. Añade tab.select("#delayed-injected") y obtienes las tres. Mismo fallo clásico, misma solución, que en Playwright y Puppeteer. Estable en tres repeticiones y tres ejecuciones completas de la suite, sin anomalías.

Barrí el retardo de inyección para encontrar hasta dónde llega la lectura por defecto. nodriver deja de ver la clase C cuando la inyección ocurre 100 ms o más después del load —el mismo umbral que en ambos controles estándar. (Botasaurus es la excepción aquí, y es la diferencia realmente interesante entre las dos bibliotecas antidetención: su get() bloquea por defecto hasta que termina de cargarse la página, así que su lectura por defecto sigue captando inyecciones a 300 ms. Paga unos 250 ms por navegación por ello.)

La espera en sí tiene una rareza que conviene presupuestar. En este barrido, tab.select() de nodriver cayó en bloques de sondeo gruesos en lugar de seguir de cerca el retardo de inyección:

Class-C delay0 ms100 ms400 ms800 ms1500 ms
nodriver select()124–152 ms1132–11411128–11292132–21772138–2150
Puppeteer waitForSelector113–129 ms203–216512–516911–9191608–1611

En este fixture, una inyección de 100 ms terminó costando cerca de 1.1 segundos para select(). El bucle instalado hace await self y luego await self.sleep(0.5) después de un fallo; el ciclo combinado medido quedó cerca de un segundo aquí, aunque no está establecido que await self sea una espera fija y universal. Todos los nodos retardados se encontraron. El esperador de Puppeteer siguió estos retardos con más precisión. Muchas esperas en serie podrían acumular la diferencia, aunque esta reseña no midió una página de producción con treinta selectores.

El arranque es el otro punto donde la narrativa de asíncrono y ligero se encuentra con la realidad. Levantar el navegador puso a nodriver en la misma zona que Botasaurus y Puppeteer, y bastante por detrás de Playwright:

StackBrowser launch, across runs
nodriver 0.50.3910–1583 ms
Botasaurus 4.0.92986–1151 ms
Puppeteer 24.16.0969–1008 ms
Playwright 1.56.0282–365 ms

Una vez en marcha, el tramo de navegar y leer de nodriver es el más rápido de los cuatro: 119–129 ms. Ligero al volante, normal al encendido.

Instalación y peso: la parte realmente buena

Aquí es donde nodriver se gana la etiqueta de "enfocado", y es un hecho de instalación benigno y verificable, no algo sobre scraping. Esto es lo que resuelve una instalación limpia de pip install nodriver:

Install factnodriver 0.50.3
Declared direct runtime dependenciesthreewebsockets, mss, deprecated
Resolved transitive dependencywrapt (via deprecated)
Measured site-packages totalabout 17.2 MB across 6 dist-info directories, including the environment's pip
nodriver's own share of that3.7 MB
numpy / lxmlneither
Browser binary at install timenone pulled down

Para una categoría que suele arrastrar una pila de renderizado, es un toque ligero.

La distinción entre paquetes directos y transitivos importa para el mantenimiento. Los metadatos de nodriver piden mss, websockets y deprecated; wrapt llega porque deprecated lo necesita. El sexto directorio dist-info en el entorno de 17.2 MB es pip, que ya formaba parte de ese virtualenv. Así que "17.2 MB en seis distribuciones" describe el entorno medido, mientras que "cuatro paquetes de runtime añadidos" describe lo que resolvió la instalación. Son conteos relacionados, no intercambiables.

La comparación que hace que el número signifique algo: en la misma máquina, el framework hermano Botasaurus ocupa 122.3 MB en 44 paquetes —aproximadamente siete veces el peso en disco. Esa es la diferencia entre un driver CDP enfocado y un framework con todo incluido, y el coste va en ambas direcciones. nodriver te da un árbol de dependencias delgado y legible, realmente auditable; Botasaurus te da más cosas desde el primer momento y te cobra disco y superficie de dependencias por ello. Ninguno es "mejor" en abstracto —depende de si quieres un driver o un framework—, pero si valoras una instalación pequeña e inspeccionable, nodriver es inusualmente limpio para lo que hace.

El pequeño peso en disco no implica una importación pequeña. En la copia de Python 3.14 corregida a un byte, import nodriver tardó unos 158 ms (mediana de importaciones nuevas en subprocessos, aproximadamente 151–199 ms). La importación sin parche en 3.14 falla, así que no se informa tiempo para ella. Los 57 módulos CDP se cargan de forma eager, y la memoria residente tras la importación fue de 31.5–31.9 MB antes de que existiera cualquier proceso de Chrome. Un navegador arrancado añadiría mucho más y no se incluyó en esta medición de memoria.

Dos advertencias acompañan a esos números. Proceden de una sola máquina —macOS arm64—, y las cifras de peso y tiempo de importación se tomaron en Python 3.14 sobre la copia con un byte parcheado, porque el paquete sin parche no se importa en ese intérprete. En un Python compatible no necesitas el parche: confirmé una importación limpia sin parche en 3.12.13, y de ahí vienen todas las mediciones de navegador. Y sigue siendo necesario un binario real de Chrome, Chromium, Edge o Brave en tiempo de ejecución —nodriver controla un navegador existente, no lo incluye—, así que ese coste de instalación queda fuera de los 17 MB, y, como mostró la sección de divulgación, la versión que anuncia tu tráfico es la del Chrome que tenga tu máquina.

La licencia es la verdadera decisión de adopción

System diagram: The license is the real adoption decision

La mayoría de las reseñas de una herramienta gratuita tratan "es open source" como el final de la conversación sobre licencias. En nodriver es el principio, porque la licencia es AGPL-3.0 —confirmado tanto en LICENSE.txt del wheel como en el spdx_id del repositorio. Es una licencia copyleft de red fuerte, y supone un compromiso materialmente distinto al que piden los vecinos.

Los pares con los que la gente suele comparar nodriver son permisivos, y eso hace que el contraste sea claro:

ToolLicenseIf you run a modified copy as a network service
nodriverAGPL-3.0Section 13 can require an operator to offer the corresponding source of its modified version to remote users
PlaywrightApache-2.0no equivalent network-copyleft clause
PuppeteerApache-2.0as above
BotasaurusMITas above

El alcance importa. La sección 13 de la AGPL se refiere a una versión modificada del programa cubierto usada para interacción remota por red. Esta reseña no decide si el código del servicio circundante forma parte o no de la obra cubierta, ni resuelve casos límite de uso interno o dentro del perímetro de una empresa. Si un producto alojado modifica nodriver, revisa el texto de la licencia y la arquitectura con asesoría legal. Esto es una señal técnica de adopción, no asesoramiento jurídico.

No estoy editorializando sobre si la AGPL es buena o mala —el copyleft es una elección legítima y muchos proyectos serios lo usan. Lo que señalo es que "nodriver es libre y open source" es cierto pero incompleto. La obligación es real, es distinta del valor por defecto permisivo en este rincón del ecosistema, y debe formar parte de la decisión en lugar de diluirse en "gratis". (PyPI, por cierto, no incluye ningún clasificador de licencia; el texto AGPL está en el wheel y el id SPDX está en el repositorio, así que no dependas del índice de paquetes para que te lo destaque.)

Metadatos con fecha

Números puntuales del repositorio y del paquete, directamente de la API de GitHub y de PyPI:

FactValue on July 14, 2026
Stars4,511
Forks422
Open issues14
CreatedFebruary 2024
Last pushedMay 2026
Latest PyPI release0.50.3
Wheelpure-Python py3-none-any
requires-python>=3.9
Python classifiers3.7–3.13

Las señales observables de mantenimiento son mixtas: el repositorio recibió pushes en mayo de 2026, mientras que el paquete medido más reciente seguía conteniendo el problema de importación en Python 3.14 y la corrección propuesta aún no se había publicado en la fecha de investigación. Las estrellas y el número de incidencias abiertas no resuelven por sí solos si ese ritmo encaja con tu umbral de mantenimiento.

Pros y contras

Pros:

  • Instalación pequeña y legible: tres dependencias declaradas más wrapt transitiva, ~17.2 MB en seis directorios dist-info en el entorno medido (uno es pip), sin numpy/lxml y sin descarga de navegador en la instalación.
  • Realmente nativo de CDP: incluye sus propios 57 bindings de dominios de DevTools Protocol y habla el protocolo directamente, sin binario chromedriver/Selenium en el flujo.
  • Amplia superficie de búsqueda de elementos en Tab (62 métodos públicos) con XPath, CSS y búsqueda de texto de primera clase; no hace falta bajar a evaluate() para XPath.
  • Diseñado como asíncrono, con perfil nuevo en cada ejecución y una interfaz Config limpia para los controles comunes (headless, ruta del ejecutable, argumentos, idioma, puertos).
  • Extrae contenido de una página construida con JavaScript tan bien como los grandes: 2 de 3 clases de contenido en una lectura por defecto, 3 de 3 con una espera explícita —idéntico a Playwright y Puppeteer estándar en el mismo fixture, estable en tres ejecuciones. La navegación y lectura fueron las más rápidas de las cuatro, con 119–129 ms.
  • Reporta navigator.webdriver como false por defecto donde ambos controles estándar devuelven true, sin parchear la propiedad: el descriptor sigue siendo el getter nativo del navegador.
  • Identidad arquitectónica clara como sucesor nativo de CDP de undetected-chromedriver.

Contras:

  • No se importa en Python 3.14 de serie: un archivo no UTF-8 de un byte (cdp/network.py) lanza SyntaxError al importar. Reproducido frente al issue abierto #35, todavía sin corregir en 0.50.3. Fija ≤3.13 (verificado limpio en 3.12.13) o recodifica el archivo.
  • AGPL-3.0 es una consideración real para cualquiera que ejecute una copia modificada como servicio de red, más estricta que los pares Apache/MIT.
  • El pequeño peso en disco no implica una importación pequeña: ~158 ms de arranque en frío y ~31.5 MB residentes antes de existir cualquier navegador, porque los 57 módulos CDP se cargan al inicio.
  • tab.select() hace polling con una espera de medio segundo, así que las esperas cortas se redondean hacia arriba: una espera de 100 ms cuesta ~1.1 s, donde el esperador de Puppeteer cuesta ~210 ms. Acierta siempre, pero se acumula con muchas esperas pequeñas.
  • Los modos headless siguen anunciando HeadlessChrome en el user-agent por defecto, exactamente igual que los controles estándar; nada en la configuración por defecto oculta la cadena autoidentificativa más obvia.
  • Sigue necesitando en tiempo de ejecución un binario real de Chrome/Chromium/Edge/Brave; la instalación ligera de pip es solo la mitad de la historia de dependencias, y el Chrome que tenga tu máquina es la versión que divulga tu tráfico.
  • La eficacia frente a cualquier sistema anti-bot no está verificada aquí —toda la premisa stealth quedó sin probar por diseño.

Lo que no probé, y por tanto no puedo afirmar: memoria por pestaña, latencia de ida y vuelta CDP, gestión de perfiles, rendimiento a escala, Linux o Windows, Python 3.13 específicamente y —lo importante— eficacia real frente a cualquier servicio anti-bot en vivo. Todos los navegadores de esta reseña solo se comunicaron con un fixture en 127.0.0.1. Todas las cifras proceden de una sola máquina (macOS arm64); las métricas de conducción del navegador corresponden a Python 3.12.13 sin parche, y las cifras antiguas de peso y tiempo de importación corresponden a Python 3.14 sobre la copia con un byte parcheado.

Tampoco ejecuté una matriz de actualización de navegador, así que la compatibilidad con futuras versiones de Chrome sigue siendo una comprobación operativa para quien adopte la herramienta, no un resultado de esta reseña.

Para quién es y quién debería saltárselo

nodriver encaja si quieres un driver CDP nativo, ligero y asíncrono para un Chromium real, y te sientes cómodo haciéndote cargo del navegador, sus actualizaciones y el tiempo de ejecución. El árbol de dependencias pequeño es más fácil de auditar, y el diseño centrado en CDP encaja con el control a nivel de protocolo. La idoneidad para contenedores sigue sin probarse: esta reseña no ejercitó imágenes Linux, instalación de navegador, bibliotecas compartidas, sandboxing ni limpieza de procesos.

Dos grupos deberían mirar a otra parte. Si estás en Python 3.14 y no quieres fijar tu intérprete o parchear un archivo vendorizado, espera a que se publique la corrección: hoy el fallo de importación es un bloqueo duro. Y si la AGPL-3.0 es un problema para la forma en que planeas distribuirlo —un servicio alojado con modificaciones privadas—, la licencia por sí sola ya es motivo suficiente para valorar una alternativa permisiva antes de construir sobre esto. Ninguno de los dos puntos critica el código; ambos son restricciones que prefieres conocer ahora y no durante una revisión de cumplimiento.

Tampoco te conviene si lo que realmente necesitas son datos, no un navegador que controlas a mano. nodriver te entrega una pestaña programable y 62 métodos; convertir una página renderizada en registros limpios y estructurados sigue siendo trabajo tuyo. Ese es otro trabajo, y ahí es donde entra una API gestionada.

Alternativas y dónde encaja Thunderbit

Primero, el encuadre honesto: nodriver es gratis, AGPL y autogestionado. Tú ejecutas el navegador, tú administras las actualizaciones, tú asumes el tiempo de ejecución y todos sus fallos. Para un desarrollador que quiere exactamente ese nivel de control, ningún servicio gestionado compite en precio con una biblioteca que ya tienes.

Dentro del open source, compara por tarea y no por marca. Si evalúas drivers de navegador real, nuestra comparativa de Playwright y Puppeteer cubre los dos pesos pesados obvios a los que nodriver se parece en forma. Scrapling es el vecino más cercano en Python sobre el eje orientado a stealth, si esa es tu razón para mirar. Si lo que buscas es salida lista para LLM en vez de control bruto del navegador, Crawl4AI renderiza páginas y devuelve Markdown, y Scrapy sigue siendo el framework de referencia para rastreos grandes sin navegador. Si comparas varios de estos a la vez, el resumen de scrapers open source pone las categorías una al lado de la otra.

Divulgación: este artículo está publicado por Thunderbit. Thunderbit es un servicio gestionado de extracción, así que la comparación se hace por tarea y no por arquitectura. nodriver te da una capa de control del navegador que tú alojas y programas; Thunderbit gestiona el renderizado y devuelve contenido de página o registros con forma de esquema como servicio. Usa nodriver cuando el control del navegador a nivel de protocolo y el autoalojamiento sean requisitos. Considera un extractor gestionado cuando el registro de salida y el traspaso operativo importen más que ser dueño del navegador. Los detalles mutables de endpoint, crédito y límites por lote pertenecen a la página de precios, no dentro de un benchmark de biblioteca.

La diferencia está en dónde vive el trabajo: nodriver mantiene la gestión del navegador y el mantenimiento del runtime de tu lado, sin una tarifa por solicitud; una API gestionada asume esa capa y cobra por llamada.

Prueba Thunderbit para extracción de datos web

Veredicto

Usa nodriver si quieres un driver Chromium pequeño, asíncrono y nativo de CDP, has confirmado que funciona con tu versión de Python (aquí importó limpio en 3.12.13) y has revisado la AGPL-3.0 para el modo en que distribuyes tu software. Su grafo de runtime resuelto añadió cuatro paquetes, el entorno medido ocupó ~17 MB y la biblioteca trae 57 módulos de dominio CDP, una superficie Tab con 62 métodos y XPath nativo. En este fixture devolvió 2 de 3 clases de contenido por defecto y 3 de 3 con espera, igual que Playwright y Puppeteer estándar.

Aun así, dimensiona bien las advertencias. En Python 3.14 no se importa en absoluto hasta que arreglas un byte no UTF-8 —un issue documentado y todavía abierto, no un misterio, pero sí una parada en seco el día que te lo encuentres. La licencia es AGPL-3.0, una decisión real para cualquiera que ejecute una copia modificada como servicio, no una formalidad. La instalación pequeña no compra una importación pequeña, porque esos módulos CDP se cargan al inicio, y el polling de medio segundo de select() hace que las esperas cortas cuesten cerca de un segundo cada una. En la cuestión de divulgación por defecto que sí pude responder, la imagen es más estrecha de lo que sugiere el marketing: un booleano difiere de Puppeteer estándar, el user-agent headless sigue diciendo HeadlessChrome, y todas las demás propiedades que medí eran idénticas en los cuatro stacks. Y toda la premisa de antidetención —la razón por la que mucha gente encuentra nodriver en primer lugar— es algo que deliberadamente no probé. Inventarié la biblioteca y la conduje contra una página en mi propia máquina en lugar de enfrentarlo a defensas reales, y prefiero decírtelo así de claro antes que venderte una afirmación de bypass que no puedo respaldar. En las preguntas que sí pude responder, nodriver es un driver bien construido, inusualmente ligero, con dos bordes afilados —una barrera por versión de Python y una licencia copyleft— que conviene ver venir.

Prueba Thunderbit para extracción de datos web Get Started Free

Preguntas frecuentes

¿Por qué falla import nodriver en Python 3.14? Porque cdp/network.py contiene un byte ± no UTF-8 sin declaración de codificación de origen. Python 3.14.2 rechaza el archivo y aborta la importación transitiva; Python 3.12.13 importa el mismo archivo idéntico en bytes sin parche. Esta reseña no aisló el cambio exacto del intérprete y no probó Python 3.13. El rastro upstream está en nodriver issue #35 y pull request #36. Usa una versión que hayas probado o recodifica el archivo a UTF-8.

¿Qué aporta eso de "nativo de CDP, sin webdriver" y cuánto cuesta instalarlo? nodriver incluye 57 módulos de dominio del DevTools Protocol y habla CDP por WebSocket en lugar de invocar chromedriver mediante Selenium. Tab expone 62 métodos, incluido XPath nativo. Los metadatos del paquete declaran tres dependencias de runtime (websockets, mss, deprecated); al resolverlas se añade wrapt. El entorno medido usó unos 17.2 MB en seis directorios dist-info, incluido pip, sin numpy, lxml ni un binario de navegador descargado. Dos advertencias: la importación de la copia parcheada tardó unos 158 ms porque los 57 módulos CDP se cargan de forma eager, y sigue siendo necesario un navegador de la familia Chrome por separado.

¿Importa la licencia AGPL-3.0 para mi proyecto? Depende de cómo lo distribuyas. AGPL-3.0 es una licencia copyleft de red: si ejecutas una versión modificada de nodriver como servicio para otros usuarios, debes ofrecerles ese código fuente modificado. Para un script personal o una herramienta interna que nunca expones, no suele ser problema. Para un producto comercial alojado sobre un nodriver parcheado, sí es una cuestión real que conviene plantear a quien lleve el cumplimiento, y es más estricta que las licencias Apache-2.0 y MIT de herramientas comparables.

¿Qué revela nodriver sobre sí mismo y eso significa que vence a Cloudflare? La primera parte está medida; la segunda no, y la diferencia importa. En una página que serví desde 127.0.0.1, usando la misma compilación de Chrome que los controles: navigator.webdriver devuelve false, mientras que Playwright y Puppeteer estándar reportan true. Ese valor se fija al lanzar el navegador, no parcheando la propiedad: el descriptor sigue siendo el getter nativo de Chrome. Más allá de ese booleano, casi todo coincide con los controles: misma cadena de plataforma, cinco plugins, doce núcleos, 16 GB de memoria declarada, la misma forma de window.chrome, sin contradicción en la Permissions API y sin restos estilo cdc_ en document o window. Los modos headless siguen anunciando HeadlessChrome/151.0.0.0 en el user-agent, igual que ambos controles; eso no queda oculto por ti. Lo que nada de esto te dice es si funciona contra un servicio anti-bot real. Nunca apunté nodriver a un sitio en vivo, nunca contacté un servicio anti-bot y nunca toqué un CAPTCHA; eso quedó fuera de alcance por diseño. La tabla de divulgación de arriba dice lo que anuncia el stack. No dice quién lo escucha ni qué hace con ello.

¿nodriver maneja bien el contenido renderizado con JavaScript? Sí, con la advertencia habitual sobre cuándo lees. En un fixture con tres clases de contenido, browser.get() + tab.get_content() por defecto devolvió 2 de 3: ejecuta JavaScript correctamente —la clase inyectada síncronamente no existe en los bytes servidos y aun así apareció—, pero lee en el evento load, así que se pierde lo que se inyecta después. Añadir tab.select("#delayed-injected") devolvió 3 de 3. Es idéntico a Playwright y Puppeteer estándar en la misma página. Presupuesta una rareza: select() hace polling con una espera de medio segundo, así que una espera de 100 ms cuesta alrededor de 1.1 segundos.

Ke
Ke
CTO en Thunderbit | Científico de datos sénior y experto en ML Con casi una década de experiencia en aprendizaje automático y ciencia de datos, Ke Shen es exalumno de la Universidad de Columbia y antiguo científico de datos sénior en Walmart Labs. Con una sólida experiencia, reconocida por sus pares, en Python, R, Java y estadística, comparte conocimientos probados en el campo sobre cómo llevar algoritmos complejos de IA desde la teoría hasta una arquitectura lista para producción.
Tabla de contenidos
Thunderbit · Agente de datos web con IA

Extrae datos de cualquier página en 1 clic

Con la confianza de más de 250.000 usuarios
plan gratuito disponible
De la página web a la hoja de cálculo
Describe lo que necesitas — el agente de IA de Thunderbit lo extrae y lo exporta a Excel, Google Sheets, Airtable o Notion. Empieza gratis.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week