Test de nodriver : un petit pilote CDP qui ne s’importe pas sous Python 3.14

Dernière mise à jour le August 18, 2026
Test de nodriver : un petit pilote CDP qui ne s’importe pas sous Python 3.14
Résumé IA
nodriver est une bibliothèque Python d’automatisation de navigateur signée ultrafunkamsterdam, l’auteur de undetected-chromedriver, et elle se présente comme la relève de ce projet. Sa rupture architecturale se fait clairement avec Selenium et chromedriver : nodriver parle à Chromium via le Chrome DevTools Protocol (CDP), sans binaire WebDriver intermédiaire, et son API est asynchrone. Playwright et Puppeteer appartiennent à une autre catégorie de comparaison. Eux aussi contrôlent le navigateur via un protocole, sans descendre de WebDriver ; les différences utiles tiennent surtout au design de l’API, au packaging, à la fourniture du navigateur et à la politique de compatibilité. Cette analyse se concentre sur nodriver 0.50.3 sans le confronter à des défenses réelles.

nodriver est une bibliothèque Python d’automatisation de navigateur signée ultrafunkamsterdam, l’auteur de undetected-chromedriver, et elle se présente comme la suite logique de ce projet. Sa rupture architecturale avec Selenium et chromedriver est nette : nodriver parle à Chromium via le Chrome DevTools Protocol (CDP), sans binaire WebDriver intermédiaire, et son API est asynchrone. Playwright et Puppeteer relèvent d’une autre famille de comparaison. Eux aussi pilotent le navigateur via un protocole, sans passer par WebDriver ; les différences vraiment utiles tiennent surtout au design de l’API, au packaging, au navigateur fourni et à la politique de compatibilité.

Cette analyse se concentre sur nodriver 0.50.3 sans le confronter à des défenses réelles. J’ai inspecté le paquet installé, ses imports, sa surface d’API, son encombrement disque et sa licence, puis je n’ai lancé des navigateurs que sur des pages servies depuis 127.0.0.1. Aucun site réel, aucun service anti-bot ni CAPTCHA n’a été impliqué. Les résultats décrivent le comportement du paquet et ce que le navigateur révèle par défaut ; ils ne prouvent rien sur l’efficacité de l’anti-détection.

Trois points ressortent clairement dans ce cadre. D’abord, sous Python 3.14, la bibliothèque ne s’importe tout simplement pas — un seul octet parasite suffit à faire tomber le paquet avant même le premier appel. Ensuite, pour un outil de cette catégorie, elle est vraiment compacte : trois dépendances directes déclarées, une seule dépendance transitive résolue, et environ 17 MB dans l’environnement mesuré. Enfin, sur ma propre page, la seule propriété où nodriver se distingue visiblement de Playwright et Puppeteer « stock » est un booléen — et, en mode headless, la chaîne user-agent affiche toujours HeadlessChrome dans les trois cas. Le reste des surprises se trouve du côté de la licence.

Le blocage à l’import sous Python 3.14

Commençons par le problème qui vous saute au visage en premier, puisqu’il survient avant même toute exécution de votre code. Sous Python 3.14, un simple import nodriver échoue immédiatement :

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

Le fichier en cause est cdp/network.py, généré automatiquement (son en-tête indique # DO NOT EDIT THIS FILE!). Il contient un octet non UTF-8, 0xb1, correspondant au ± dans le commentaire #: JSON (±Inf)., et aucune déclaration d’encodage source. Le module est chargé via nodriver/__init__cdp/__init__network, donc l’erreur de parsing coupe l’import net. Un balayage du paquet n’a révélé aucun autre fichier source non UTF-8.

La frontière de version mérite d’être formulée avec prudence. Python 3.14.2 rejette le fichier, tandis que Python 3.12.13 l’importe sans correctif. Les fichiers network.py installés sont strictement identiques en bytes dans ces environnements (SHA-256 ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6), donc la différence ne s’explique pas par des artefacts source distincts. Cette analyse n’a pas isolé le changement exact du tokenizer CPython responsable, et Python 3.13 n’a pas été testé. Elle rapporte donc les deux résultats observés, sans prétendre que toutes les versions antérieures se comportent comme Python 3.12.

Aucune conclusion ne peut être tirée de ces deux extrêmes mesurés sur Python 3.13.

Il s’agit d’une reproduction, pas d’une découverte. Le même traceback sous Python 3.14 est documenté dans le ticket nodriver #35, avec un correctif proposé dans la pull request #36. La version 0.50.3 contient encore l’octet fautif. Les métadonnées PyPI listent des classifieurs jusqu’à Python 3.13 et ne revendiquent pas la prise en charge de Python 3.14.

Le contournement est aussi petit que le bug : utilisez une version de Python déjà testée, ou réencodez ce seul fichier en UTF-8, comme le propose la pull request #36. Après ce réencodage, le paquet s’importe et s’inspecte correctement sous 3.14 sans autre blocage. Le test propre sous 3.12.13 ci-dessous vérifie un chemin non corrigé ; Python 3.13 n’a pas été testé dans cette étude.

Sous Python 3.12.13, import nodriver réussit sans correctif. Tous les chiffres de pilotage de navigateur cités plus loin proviennent de cette installation. Les mesures de taille et de temps d’import utilisent la copie réencodée d’un seul octet sous 3.14 et sont indiquées comme telles. Python 3.13 reste ici non testé ; un résultat sous 3.12 ne couvre pas toute la plage antérieure.

Si vous utilisez par défaut des interpréteurs récents — et beaucoup d’équipes adoptent vite un nouveau Python — c’est un vrai mur, même s’il se corrige facilement. Mieux vaut savoir qu’il existe pour éviter de perdre une demi-journée sur un SyntaxError dans un fichier que vous n’avez jamais écrit.

Ce qu’est nodriver, concrètement

La description en une ligne (« CDP-native, sans webdriver ») sonne comme un slogan jusqu’à ce qu’on regarde ce que contient réellement le wheel. nodriver embarque son propre ensemble complet de bindings du DevTools Protocol : le package nodriver.cdp compte 57 modules de domaines protocolaire — accessibility, dom, network, page, fetch, runtime, target, storage, input, emulation, et ainsi de suite. Ce nombre de modules matérialise très concrètement l’idée de « CDP direct, sans Selenium ». Au lieu de lancer un exécutable chromedriver qui parle WebDriver et fait l’intermédiaire, nodriver génère des objets Python pour les domaines CDP et parle lui-même le protocole via WebSocket. cdp/network.py — le fichier contenant l’octet fautif — fait partie de ces 57 modules générés automatiquement ; c’est pourquoi le blocage se situe dans du code généré que personne ne modifie à la main.

Au-dessus de cette couche protocolaire se trouve un modèle d’objets plus pratique. L’objet Tab expose 62 méthodes publiques, et la surface de recherche est plus large que chez la plupart des drivers : correspondance textuelle via find() et find_all(), CSS via select() et select_all(), et un point d’entrée xpath() natif. Avoir XPath, CSS et recherche textuelle sur un même objet est vraiment pratique ; certaines bibliothèques vous obligent à descendre jusqu’à evaluate() pour XPath. Le constructeur Config expose user_data_dir, headless, browser_executable_path, browser_args, sandbox, lang (valeur par défaut 'en-US'), host, port, expert, ainsi que **kwargs. Lors de cet inventaire d’API, Config(headless=True) a produit 16 arguments de lancement Chromium, dont --no-first-run, --no-default-browser-check, --remote-allow-origins=* et --homepage=about:blank. Je n’ai pas appelé start() pendant cette étape d’inspection. Les tests navigateur ci-dessous ont été exécutés séparément.

System diagram: Direct CDP Control Path

La bibliothèque expose bien une surface d’API orientée anti-détection — des méthodes dont j’ai confirmé l’existence, sans toutefois tester leur comportement face à une cible. Je le signale une fois puis je passe, car l’effet réel sur un service donné est précisément ce que j’ai choisi de ne pas évaluer. Formulé simplement : le vocabulaire de nodriver est plus mesuré que celui de certains concurrents axés « stealth ». Son discours est d’abord architectural — CDP-native, profil neuf à chaque exécution — plutôt qu’une liste de méthodes nommées detect_and_bypass. Il s’agit d’un constat de conception d’API, pas d’une promesse de résultat.

Ces chiffres proviennent de l’import du paquet et de l’introspection Python standard — inspect, parcours des modules, comptage d’attributs. Aucun site n’a été contacté. Si vous voulez reproduire l’exercice, les nombres sortent directement du paquet ; ce n’est pas une impression subjective.

Ce que le navigateur révèle de lui-même

Measured results chart: Browser disclosure fields in the tested stacks

Voici une question à laquelle on peut répondre sans aller tester la moindre défense réelle : quand nodriver pilote un navigateur, qu’est-ce que ce navigateur divulgue spontanément à la page qu’il consulte ? J’ai écrit une page qui lit les signaux évidents — navigator.webdriver, le user-agent, la plateforme, les langues, le nombre de plugins et de cœurs matériels, la forme de window.chrome, ce que dit l’API Permissions, la géométrie de la fenêtre et de l’écran — puis je l’ai servie sur 127.0.0.1 avant d’y envoyer quatre stacks : nodriver, Botasaurus, et les versions stock de Playwright et Puppeteer comme contrôles. Les quatre pilotaient exactement la même version de Chrome (Chrome for Testing 151.0.7922.10), donc toute différence observée vient de la bibliothèque, pas du navigateur. Mode headless et mode headed, trois exécutions chacun. Toutes les valeurs ci-dessous sont restées stables sur les trois passages.

StackModenavigator.webdriverJeton user-agentnavigator.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 différence la plus nette est ce seul booléen. Dans ces configurations de lancement par défaut, nodriver renvoie navigator.webdriver à false, alors que Playwright et Puppeteer stock renvoient true, en headless comme en headed. Ce test met en évidence une différence de configuration ; il ne prouve pas que cette valeur découle simplement de l’absence d’un binaire WebDriver.

Dans les quatre stacks, la propriété reste le getter natif du navigateur sur Navigator.prototypefunction get webdriver() { [native code] } — et non une propriété propre à l’instance, ni une fonction remplacée. La propriété n’a pas été réécrite par du JavaScript de page après le chargement. Le test n’a pas isolé l’argument de lancement ou le chemin source à l’origine de la différence.

Le deuxième détail nuance le discours marketing. En mode headless, le user-agent de nodriver annonce toujours HeadlessChrome/151.0.0.0 — identique à Puppeteer stock, identique à Playwright stock. En mode headed, il devient Chrome/151.0.0.0, là encore à l’identique. Si vous pensiez qu’une bibliothèque anti-détection masque par défaut la chaîne d’identification la plus célèbre de l’automatisation navigateur, ce n’est pas le cas. Il faut le faire soi-même.

Presque tout le reste était identique entre les quatre stacks, et c’est important de le dire clairement car cela resserre l’histoire — chaque propriété ci-dessous a renvoyé la même valeur sur nodriver, Botasaurus, Playwright et Puppeteer :

PropriétéValeur identique sur les quatre stacks
platformMacIntel
vendorGoogle Inc.
Pluginscinq
Types MIMEdeux
pdfViewerEnabledtrue
Cœurs logiquesdouze
Mémoire appareil déclarée16 GB
Points tactileszéro
window.chromeprésent, avec app/csi/loadTimes et sans runtime
Chaîne du renderer WebGLidentique sur les quatre

Le vieux classique où l’API Permissions contredit Notification.permission n’apparaissait nulle part — les quatre renvoyaient default et prompt de manière cohérente. J’ai aussi parcouru document et window à la recherche des résidus de type cdc_ connus des anciens stacks WebDriver : rien sur les quatre.

Le seul point où nodriver ressemble davantage à un navigateur automatisé « nu » qu’à un contrôle bien réglé est la géométrie de fenêtre. En headless, nodriver renvoie outerWidth/outerHeight à 0×0 sur un écran 800×600 ; Playwright headless renvoie 1280×720 parce qu’il définit un viewport pour vous. Puppeteer se retrouve avec nodriver à 0×0. C’est une différence de configuration par défaut, pas de capacité, et vous pouvez la modifier.

Dernière chose utile avant un déploiement : nodriver ne fournit pas de navigateur, donc, par défaut, il utilise celui déjà installé sur votre machine. Sur la mienne, cela a conduit à détecter automatiquement /Applications/Google Chrome.app — Chrome 150.0.7871.187 — et c’est cette version qui a été révélée dans le user-agent, pas une version figée. La version de navigateur exposée par votre parc est simplement celle qui s’y trouve déjà.

Disons clairement ce que ceci est, et ce que cela n’est pas. C’est un relevé de ce qu’une stack automatisée divulgue lorsqu’on ne lui demande pas de masquer quoi que ce soit — utile côté défense, utile aussi si vous voulez savoir ce que votre propre outillage diffuse. Ce n’est pas une mesure de l’importance de ces signaux pour tel ou tel service. Je ne l’ai pas testé, et aucune ligne ci-dessus ne doit être lue comme un résultat d’évasion.

Peut-il vraiment extraire du contenu d’une page ?

Se montrer est une chose ; récupérer le bon HTML en est une autre. J’ai exécuté nodriver sur le même fixture à trois classes de contenu que le reste de ce dépôt de benchmark utilise, afin que les chiffres soient comparables aux autres outils mesurés ici. La page comporte trois éléments : A, un lien statique dont l’identifiant est un littéral dans les octets servis ; B, un nœud généré par un script inline pendant le parsing, avec son identifiant et son URL assemblés à partir de fragments, de sorte que seul l’exécution du JavaScript permet de les révéler ; et C, un nœud injecté 800 ms après l’événement de chargement, construit de la même façon. La classe C est le cas adversarial — une lecture au moment du load ne peut pas la voir.

StackLecture par défautAvec attente explicite
nodriver 0.50.32 sur 3 (A + B, rate C)3 sur 3
Botasaurus 4.0.922 sur 33 sur 3
Playwright 1.56.02 sur 33 sur 3
Puppeteer 24.16.02 sur 33 sur 3

nodriver se situe exactement au même niveau que les poids lourds. browser.get() suivi immédiatement de tab.get_content() fournit un instantané pris au chargement : le JavaScript est correctement exécuté — la classe B le prouve, puisqu’elle n’existe nulle part dans les octets servis — mais tout ce qui est injecté après le chargement est manqué. Ajoutez tab.select("#delayed-injected") et vous obtenez les trois éléments. Même écueil, même solution, comme avec Playwright et Puppeteer. Le résultat a été stable sur trois répétitions et trois exécutions complètes de la suite, sans aucune instabilité.

J’ai fait varier le délai d’injection pour voir à partir de quand la lecture par défaut décroche. nodriver cesse de voir la classe C dès que l’injection survient 100 ms ou plus après le chargement — la même limite que les deux contrôles stock. (Botasaurus fait figure d’exception ici, et c’est la différence vraiment intéressante entre ces deux bibliothèques anti-détection : son get() attend par défaut le chargement complet de la page, donc sa lecture par défaut capture encore une injection à 300 ms. Cela lui coûte environ 250 ms par navigation.)

L’attente elle-même a une particularité à anticiper. Dans ce balayage, tab.select() chez nodriver avançait par paliers de polling assez grossiers, plutôt que de coller étroitement au délai d’injection :

Délai de la classe C0 ms100 ms400 ms800 ms1500 ms
nodriver select()124–152 ms1132–11411128–11292132–21772138–2150
Puppeteer waitForSelector113–129 ms203–216512–516911–9191608–1611

Sur ce fixture, une injection à 100 ms a conduit à environ 1,1 s pour select(). La boucle installée exécute await self, puis await self.sleep(0.5) après un échec ; le cycle combiné mesuré est arrivé ici près d’une seconde, mais await self n’est pas établi comme un sommeil fixe universel. Tous les nœuds différés testés ont été trouvés. L’attente de Puppeteer suivait ces délais de plus près. De nombreuses attentes en série pourraient amplifier l’écart, même si cette analyse n’a pas benchmarké une page de production à trente sélecteurs.

Le démarrage est l’autre endroit où le discours « asynchrone et léger » rencontre la réalité. La mise en route du navigateur a placé nodriver dans la même zone que Botasaurus et Puppeteer, et bien derrière Playwright :

StackLancement du navigateur, sur plusieurs exécutions
nodriver 0.50.3910–1583 ms
Botasaurus 4.0.92986–1151 ms
Puppeteer 24.16.0969–1008 ms
Playwright 1.56.0282–365 ms

Une fois lancé, le couple navigation+lecture de nodriver est le plus rapide des quatre, à 119–129 ms. Léger au volant, ordinaire à l’allumage.

Installation et empreinte : le vrai bon point

C’est ici que nodriver mérite vraiment son étiquette de projet « ciblé », et il s’agit d’un fait d’installation simple et vérifiable, pas d’un commentaire sur le scraping. Voici ce que résout un pip install nodriver propre :

Fait d’installationnodriver 0.50.3
Dépendances d’exécution directes déclaréestroiswebsockets, mss, deprecated
Dépendance transitive résoluewrapt (via deprecated)
Taille totale mesurée de site-packagesenviron 17,2 MB sur 6 répertoires dist-info, y compris le pip de l’environnement
Part propre à nodriver3,7 MB
numpy / lxmlaucun des deux
Binaire de navigateur au moment de l’installationaucun téléchargement

Pour une catégorie qui traîne souvent une pile de rendu complète, c’est une empreinte très légère.

La distinction entre paquets directs et transitifs compte pour la maintenance. Les métadonnées de nodriver demandent mss, websockets et deprecated ; wrapt arrive parce que deprecated en dépend. Le sixième répertoire dist-info dans l’environnement de 17,2 MB est pip, déjà présent dans ce virtualenv. Ainsi, « 17,2 MB sur six distributions » décrit l’environnement mesuré, tandis que « quatre paquets runtime ajoutés » décrit ce que l’installation a résolu. Ce sont des comptages liés, mais non interchangeables.

La comparaison qui donne du sens au chiffre : sur la même machine, le framework frère Botasaurus pèse 122,3 MB sur 44 paquets — soit environ sept fois l’empreinte disque. C’est la différence entre un pilote CDP ciblé et un framework tout-en-un, et cette différence va dans les deux sens. nodriver vous donne un arbre de dépendances mince et lisible, que vous pouvez vraiment auditer ; Botasaurus vous apporte davantage dès l’installation, mais vous le facture en espace disque et en surface de dépendances. Aucun n’est « meilleur » en absolu — tout dépend si vous voulez un driver ou un framework — mais si vous privilégiez une installation petite et inspectable, nodriver est d’une propreté inhabituelle pour ce qu’il fait.

La petite taille disque n’implique pas un petit import. Sur la copie Python 3.14 corrigée d’un octet, import nodriver a pris environ 158 ms (médiane sur des imports en sous-processus frais, soit environ 151–199 ms). L’import non corrigé sous 3.14 échoue, donc aucun temps n’est rapporté pour ce cas. Les 57 modules CDP sont chargés d’emblée, et la mémoire résidente après import était de 31,5–31,9 MB avant même l’existence d’un processus Chrome. Un navigateur lancé ajouterait bien davantage, et n’a pas été inclus dans cette mesure mémoire.

Deux réserves accompagnent ces chiffres. Ils proviennent d’une seule machine — macOS arm64 — et les mesures de taille et de temps d’import ont été prises sous Python 3.14 sur la copie réencodée d’un octet, car le paquet non corrigé ne s’importe pas du tout sur cet interpréteur. Sur un Python pris en charge, ce correctif n’est pas nécessaire : j’ai confirmé un import propre, sans correctif, sous 3.12.13, et c’est de là que viennent toutes les mesures navigateur. Il faut aussi un binaire Chrome, Chromium, Edge ou Brave au runtime — nodriver pilote un navigateur existant, il n’en fournit pas — donc ce coût est en dehors des 17 MB, et comme la section de divulgation l’a montré, la version annoncée est celle que votre machine a effectivement installée.

La licence est la vraie décision d’adoption

System diagram: The license is the real adoption decision

La plupart des analyses d’outils gratuits considèrent « c’est open source » comme la fin de la discussion sur la licence. Pour nodriver, c’est seulement le début, car la licence est AGPL-3.0 — confirmé à la fois dans LICENSE.txt du wheel et dans le spdx_id du dépôt. C’est une licence de copyleft réseau forte, et son engagement est matériellement différent de celui demandé par les voisins.

Les alternatives auxquelles on compare le plus souvent nodriver sont permissives, ce qui rend le contraste très concret :

OutilLicenceSi vous exécutez une copie modifiée comme service réseau
nodriverAGPL-3.0La section 13 peut obliger l’opérateur à proposer le code source correspondant de sa version modifiée aux utilisateurs distants
PlaywrightApache-2.0pas de clause équivalente de copyleft réseau
PuppeteerApache-2.0idem
BotasaurusMITidem

Le périmètre compte. La section 13 de l’AGPL vise une version modifiée du programme concerné lorsqu’elle est utilisée pour des interactions réseau à distance. Cette analyse ne tranche pas la question de savoir si le code du service environnant constitue ou non une partie de l’œuvre couverte, ni les cas limites d’usage interne ou de périmètre entreprise. Si un produit hébergé modifie nodriver, faites relire la licence et l’architecture par un juriste. Il s’agit d’un signal technique d’adoption, pas d’un conseil juridique.

Je ne porte pas de jugement sur le fait que l’AGPL soit « bonne » ou « mauvaise » — le copyleft est un choix légitime et de nombreux projets sérieux l’utilisent. Je signale simplement que « nodriver est libre et open source » est vrai, mais incomplet. L’obligation existe, elle est différente du réflexe permissif habituel dans cette zone de l’écosystème, et elle doit entrer dans la décision au lieu d’être réduite à un simple « gratuit ». (PyPI, au passage, ne fournit aucun classifieur de licence ; le texte AGPL est dans le wheel et l’ID SPDX est dans le dépôt, donc ne comptez pas sur l’index du paquet pour vous le signaler.)

Des métadonnées datées

Chiffres ponctuels du dépôt et du paquet, directement issus de l’API GitHub et de PyPI :

FaitValeur au 14 juillet 2026
Stars4 511
Forks422
Problèmes ouverts14
Créationfévrier 2024
Dernier pushmai 2026
Dernière version PyPI0.50.3
Wheelpur Python py3-none-any
requires-python>=3.9
Classifieurs Python3.7–3.13

Les signaux de maintenance observables sont mitigés : le dépôt a reçu un push en mai 2026, mais le dernier paquet mesuré contenait encore le problème d’import Python 3.14 et le correctif proposé n’était pas encore publié à la date de recherche. Le nombre d’étoiles et d’issues ouvertes ne suffit pas à dire si ce rythme répond à votre seuil de maintenance.

Avantages et inconvénients

Avantages :

  • Installation petite et lisible : trois dépendances déclarées, plus le transitif wrapt, environ 17,2 MB sur six répertoires dist-info dans l’environnement mesuré (dont pip), pas de numpy/lxml, et aucun téléchargement de navigateur à l’installation.
  • Vraiment CDP-native : embarque ses propres bindings DevTools Protocol en 57 modules et parle le protocole directement, sans binaire chromedriver/Selenium dans la boucle.
  • Large surface de recherche sur Tab (62 méthodes publiques) avec XPath, CSS et recherche textuelle natifs — pas besoin de descendre à evaluate() pour XPath.
  • Conçu en asynchrone, profil neuf à chaque exécution, et surface Config propre pour les réglages courants (headless, chemin du binaire, arguments, langue, ports).
  • Récupère du contenu d’une page construite en JavaScript aussi bien que les gros acteurs : 2 sur 3 classes de contenu en lecture par défaut, 3 sur 3 avec attente explicite — identique à Playwright et Puppeteer stock sur le même fixture, stable sur trois exécutions. Le couple navigation+lecture était le plus rapide des quatre à 119–129 ms.
  • Renvoie navigator.webdriver à false par défaut, là où les deux contrôles stock renvoient true, sans patcher la propriété — le descriptor reste le getter natif du navigateur.
  • Identité architecturale nette : successeur CDP-native de undetected-chromedriver.

Inconvénients :

  • Ne s’importe pas sur Python 3.14 tel quel — un fichier non UTF-8 d’un octet (cdp/network.py) déclenche un SyntaxError à l’import. Reproduit à partir du ticket ouvert #35, toujours non corrigé en 0.50.3. Il faut pinner ≤3.13 (testé proprement sous 3.12.13) ou réencoder le fichier.
  • AGPL-3.0 est un vrai sujet pour quiconque exécute une copie modifiée comme service réseau — plus strict que les équivalents Apache/MIT.
  • La petite taille disque ne signifie pas petit import : environ 158 ms au démarrage à froid et ~31,5 MB résidents avant tout navigateur, parce que les 57 modules CDP sont chargés d’emblée.
  • tab.select() interroge avec un back-off de demi-seconde, donc les petites attentes s’arrondissent à la hausse — une attente de 100 ms coûte environ 1,1 s, contre ~210 ms pour le waiter de Puppeteer. C’est exact à chaque fois, mais l’écart s’additionne sur de nombreuses petites attentes.
  • En headless, le user-agent annonce toujours HeadlessChrome par défaut, exactement comme les contrôles stock ; rien dans la configuration par défaut ne masque la chaîne d’auto-identification la plus évidente.
  • Il faut toujours un vrai binaire Chrome/Chromium/Edge/Brave au runtime ; le petit pip install ne raconte qu’une partie de l’histoire, et la version de Chrome présente sur votre machine est celle que votre trafic révèle.
  • L’efficacité contre un système anti-bot quelconque n’a pas été vérifiée ici — toute la promesse stealth n’a pas été testée, par conception.

Ce que je n’ai pas testé, et sur quoi je ne peux donc pas me prononcer : la mémoire par onglet, la latence aller-retour CDP, la gestion des profils, le débit à grande échelle, Linux ou Windows, Python 3.13 spécifiquement, et — le plus important — l’efficacité réelle contre un service anti-bot vivant. Tous les navigateurs de cet article n’ont parlé qu’à un fixture local sur 127.0.0.1. Tous les chiffres proviennent d’une seule machine (macOS arm64) ; les mesures de navigation navigateur sont sous Python 3.12.13 sans correctif, et les mesures plus anciennes d’empreinte et de temps d’import sont sous Python 3.14 sur la copie réencodée d’un octet.

Je n’ai pas non plus exécuté de matrice de mise à jour du navigateur, donc la compatibilité avec les futures versions de Chrome reste un contrôle opérationnel à la charge de l’adoptant, et non un résultat de cette revue.

Pour qui, et qui devrait passer son chemin

nodriver convient si vous voulez un driver Chromium léger, asynchrone et CDP-native, et que vous acceptez de gérer vous-même le navigateur, ses mises à jour et son exécution. Son arbre de dépendances réduit est plus facile à auditer, et sa conception orientée CDP se prête bien au contrôle de bas niveau. L’aptitude aux conteneurs reste non testée : cette étude n’a pas exercé d’images Linux, d’installation de navigateur, de bibliothèques système partagées, de sandbox ni de nettoyage de processus.

Deux groupes devraient regarder ailleurs. Si vous êtes sous Python 3.14 et que vous ne voulez ni figer votre interpréteur ni patcher un fichier vendorisé, attendez la publication du correctif — le blocage à l’import est aujourd’hui rédhibitoire. Et si l’AGPL-3.0 pose problème pour votre mode de livraison — par exemple un service hébergé avec modifications privées — la licence suffit à elle seule à justifier l’examen d’une alternative permissive avant de bâtir dessus. Aucun de ces points n’est un reproche au code ; ce sont des contraintes qu’il vaut mieux découvrir maintenant qu’en audit de conformité.

Évitez-le aussi si ce qu’il vous faut vraiment, c’est de la donnée, pas un navigateur que vous pilotez à la main. nodriver vous donne un onglet scriptable et 62 méthodes ; transformer une page rendue en enregistrements propres et structurés reste votre travail. C’est un autre métier, et c’est là qu’une API managée entre en scène.

Alternatives, et la place de Thunderbit

D’abord le cadrage honnête : nodriver est gratuit, sous AGPL, et auto-hébergé. Vous exécutez le navigateur, vous gérez les mises à jour, vous assumez le runtime et toutes ses pannes. Pour un développeur qui veut exactement ce degré de contrôle, aucun service managé ne bat le prix d’une bibliothèque que vous avez déjà.

Dans l’open source, comparez selon la tâche plutôt que selon le logo. Si vous évaluez des drivers de navigateur réel, notre comparatif Playwright vs Puppeteer couvre les deux gros noms évidents aux côtés de nodriver. Scrapling est le voisin Python le plus proche sur l’axe orienté stealth si c’est votre critère. Pour un résultat prêt pour les LLM plutôt qu’un contrôle brut du navigateur, Crawl4AI rend les pages et renvoie du Markdown, et Scrapy reste la référence pour les grands crawls sans navigateur. Si vous comparez plusieurs de ces solutions à la fois, le tour d’horizon des extracteurs open source met les catégories côte à côte.

Divulgation : cet article est publié par Thunderbit. Thunderbit est un service d’extraction managé, donc la comparaison se fait par tâche, pas par architecture. nodriver vous donne une couche de contrôle navigateur que vous hébergez et programmez ; Thunderbit gère le rendu et renvoie le contenu de la page ou des enregistrements structurés en tant que service. Utilisez nodriver quand le contrôle navigateur au niveau protocolaire et l’auto-hébergement sont des exigences. Envisagez un extracteur managé lorsque le format de sortie et le transfert opérationnel comptent plus que la possession du navigateur. Les détails variables sur les endpoints, les crédits et les limites par lot relèvent de la page tarifaire, pas d’un benchmark de bibliothèque.

Le compromis, c’est l’endroit où vit le travail : nodriver garde la gestion du navigateur et la maintenance runtime de votre côté, sans frais de service par requête ; une API managée prend cette couche en charge et facture les appels.

Essayez Thunderbit pour l’extraction de données Web

Verdict

Utilisez nodriver si vous voulez un driver Chromium petit, asynchrone et CDP-native, si vous avez vérifié sa compatibilité avec votre version de Python (3.12.13 s’est importé proprement ici), et si vous avez étudié l’AGPL-3.0 au regard de votre mode de distribution. Son graphe runtime résolu ajoute quatre paquets, l’environnement mesuré occupe environ 17 MB, et la bibliothèque embarque 57 modules de domaine CDP avec une surface Tab de 62 méthodes et un XPath natif. Sur ce fixture, elle a renvoyé 2 sur 3 classes de contenu par défaut et 3 sur 3 avec une attente, comme Playwright et Puppeteer stock.

Mais il faut chiffrer les réserves avec honnêteté. Sous Python 3.14, elle ne s’importe pas du tout tant que vous n’avez pas corrigé un octet non UTF-8 — un problème documenté et encore ouvert, pas un mystère, mais un arrêt net le jour où vous le rencontrez. La licence est AGPL-3.0, ce qui constitue une vraie décision pour quiconque exécute une copie modifiée comme service, pas une formalité. La petite installation n’achète pas un petit import, puisque tous ces modules CDP se chargent d’emblée, et le polling de select() par demi-seconde fait que les attentes courtes coûtent environ une seconde chacune. Sur la question de divulgation par défaut que j’ai réellement pu mesurer, le tableau est plus étroit que ce que laisse entendre le marketing : un seul booléen diffère de Puppeteer stock, le user-agent headless dit toujours HeadlessChrome, et toutes les autres propriétés mesurées étaient identiques sur les quatre stacks. Et la promesse anti-détection dans son ensemble — la raison pour laquelle beaucoup de gens découvrent nodriver — est quelque chose que j’ai volontairement choisi de ne pas tester. J’ai inventorié la bibliothèque et je l’ai pilotée contre une page sur ma propre machine plutôt que de l’opposer à des défenses réelles, et je préfère vous le dire clairement plutôt que de vous vendre une affirmation de contournement que je ne peux pas étayer. Sur les points que j’ai pu vérifier, nodriver est un pilote bien conçu, d’une légèreté inhabituelle, avec deux arêtes franches — un mur de version Python et une licence copyleft — qu’il faut voir venir.

Essayez Thunderbit pour l’extraction de données Web Get Started Free

FAQ

Pourquoi import nodriver échoue-t-il sous Python 3.14 ? Parce que cdp/network.py contient un octet ± non UTF-8 sans déclaration d’encodage source. Python 3.14.2 rejette le fichier et interrompt l’import transitif ; Python 3.12.13 importe le même fichier, byte pour byte, sans correctif. Cette analyse n’a pas isolé le changement exact de l’interpréteur et n’a pas testé Python 3.13. Le fil upstream est le ticket nodriver #35 et la pull request #36. Utilisez une version que vous avez testée, ou réencodez le fichier en UTF-8.

Qu’apporte le fait d’être « CDP-native, sans webdriver », et que coûte l’installation ? nodriver embarque 57 modules de domaines DevTools Protocol et parle CDP via WebSocket au lieu d’invoquer chromedriver à travers Selenium. Tab expose 62 méthodes, dont XPath natif. Les métadonnées du paquet déclarent trois dépendances runtime (websockets, mss, deprecated) ; leur résolution ajoute wrapt. L’environnement mesuré occupait environ 17,2 MB sur six répertoires dist-info, pip inclus, sans numpy, lxml ni binaire de navigateur téléchargé. Deux réserves : l’import sur la copie corrigée prenait environ 158 ms parce que les 57 modules CDP se chargent d’emblée, et il faut toujours un navigateur de la famille Chrome séparément.

La licence AGPL-3.0 a-t-elle un impact pour mon projet ? Cela dépend de votre mode de livraison. L’AGPL-3.0 est une licence de copyleft réseau : si vous exécutez une version modifiée de nodriver en tant que service utilisé par d’autres, vous devez leur proposer le code source correspondant de cette version modifiée. Pour un script personnel ou un outil interne jamais exposé, cela ne pose généralement pas de problème. Pour un produit commercial hébergé construit sur un nodriver patché, c’est une vraie question à poser à la personne en charge de la conformité — et c’est plus strict que l’Apache-2.0 et le MIT des outils comparables.

Qu’est-ce que nodriver révèle sur lui-même — et cela veut-il dire qu’il bat Cloudflare ? La première partie est mesurée ; la seconde ne l’est pas, et la différence compte. Sur une page servie depuis 127.0.0.1, avec le même Chrome que les contrôles : navigator.webdriver revient à false, alors que Playwright stock et Puppeteer stock renvoient tous deux true. Cette valeur est définie au lancement du navigateur, pas via un patch de propriété — le descriptor reste le getter natif de Chrome. Au-delà de ce booléen, presque tout le reste était identique aux contrôles : même chaîne de plateforme, cinq plugins, douze cœurs, 16 GB de mémoire déclarée, même forme de window.chrome, aucune contradiction de l’API Permissions, et aucun résidu de type cdc_ sur document ou window. En headless, le user-agent affiche toujours HeadlessChrome/151.0.0.0, comme les deux contrôles — cela n’est pas masqué pour vous. Rien de tout cela ne dit en revanche si ces signaux fonctionnent contre un vrai service anti-bot. Je n’ai jamais pointé nodriver vers un site réel, jamais contacté de service anti-bot, et jamais touché à un CAPTCHA ; c’était hors périmètre par conception. Le tableau de divulgation ci-dessus dit ce que la stack annonce. Il ne dit rien sur qui écoute, ni sur la manière dont il réagit.

nodriver gère-t-il correctement le contenu rendu en JavaScript ? Oui, avec la réserve habituelle sur le moment de la lecture. Sur un fixture à trois classes de contenu, un browser.get() suivi de tab.get_content() renvoie 2 sur 3 — il exécute bien JavaScript (la classe injectée de façon synchrone n’existe nulle part dans les bytes servis et a pourtant bien été récupérée), mais il lit au moment du chargement, donc il rate tout ce qui est injecté après. Ajouter tab.select("#delayed-injected") permet de passer à 3 sur 3. C’est identique à Playwright et Puppeteer stock sur la même page. Prévoyez un point de friction : select() interroge avec un back-off d’une demi-seconde, donc une attente de 100 ms coûte environ 1,1 s.

Ke
Ke
CTO chez Thunderbit | Data Scientist senior et expert en ML Forte d’une expérience de près de dix ans en machine learning et en data science, Ke Shen est diplômé de Columbia University et ancien Data Scientist senior chez Walmart Labs. Grâce à une expertise approfondie, reconnue par ses pairs, en Python, R, Java et statistiques, il partage des analyses éprouvées sur le passage d’algorithmes d’IA complexes de la théorie à une architecture prête pour la production.
Table des matières
Thunderbit · Agent IA de données web

Extrayez des données de n'importe quelle page en 1 clic

Approuvé par plus de 250 000 utilisateurs
formule gratuite disponible
De la page web au tableur
Décrivez ce dont vous avez besoin — l'agent IA de Thunderbit l'extrait et l'exporte vers Excel, Google Sheets, Airtable ou Notion. Gratuit pour commencer.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week