nodriver — это библиотека для автоматизации браузера на Python от ultrafunkamsterdam, автора undetected-chromedriver, и сам проект подаёт себя как его преемника. По архитектуре он сознательно уходит от Selenium и chromedriver: nodriver общается с Chromium через Chrome DevTools Protocol (CDP), без WebDriver-бинарника в цепочке, а его API асинхронный. Playwright и Puppeteer — уже другой класс сравнения. Это тоже браузерные контроллеры, работающие через протокол, а не наследники WebDriver; полезнее сравнивать их по дизайну API, упаковке, поставке браузера и политике совместимости.
В этом обзоре рассматривается nodriver 0.50.3 — без проверки на живых антибот-защитах. Я изучил установленный пакет, импорты, поверхность API, занимаемое место на диске и лицензию, а браузеры запускал только на страницах, обслуживаемых с 127.0.0.1. Никаких реальных целей, антибот-сервисов или CAPTCHA в тесте не было. Результаты описывают поведение пакета и то, что браузер сообщает по умолчанию; они не доказывают эффективность обхода обнаружения.
Внутри этих рамок особенно бросились в глаза три вещи. Во-первых, в Python 3.14 библиотека вообще не импортируется — один случайный байт валит весь пакет ещё до того, как вы успеете вызвать хоть что-то. Во-вторых, для драйвера этого класса он удивительно компактный: три заявленные зависимости плюс одна транзитивная, и примерно 17 МБ в измеренной среде. В-третьих, на моей тестовой странице единственное заметное отличие nodriver от стандартных Playwright и Puppeteer — один булев флаг, а строка user-agent в headless-режиме у всех трёх всё равно содержит HeadlessChrome. Остальные сюрпризы связаны с лицензией.
Ошибка импорта в Python 3.14
Начнём с проблемы, которая ударит первой, потому что происходит ещё до выполнения вашего кода. В Python 3.14 обычный import nodriver падает сразу:
File ".../nodriver/cdp/network.py", line 1345
#: JSON (±Inf).
^
SyntaxError: Non-UTF-8 code starting with '\xb1' on line 1345, but no encoding declared; see PEP 263
Сбойный файл — сгенерированный cdp/network.py (в заголовке написано # DO NOT EDIT THIS FILE!). В нём есть один байт не в UTF-8 — 0xb1, это символ ± в комментарии #: JSON (±Inf)., и при этом отсутствует объявление кодировки исходника. Модуль загружается через цепочку nodriver/__init__ → cdp/__init__ → network, поэтому ошибка разбора обрывает импорт. Проверка пакета не нашла других исходников вне UTF-8.
Границу версии нужно формулировать аккуратно. Python 3.14.2 этот файл отвергает, а Python 3.12.13 импортирует его без патча. При этом установленный network.py в обеих средах побайтно одинаков (SHA-256 ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6), так что разница не объясняется разными артефактами исходников. В этом обзоре я не изолировал точное изменение токенизатора CPython, которое вызывает поведение, и Python 3.13 не тестировался. Здесь приводятся только два измеренных результата интерпретатора, без утверждения, что любая более ранняя версия ведёт себя как 3.12.
Из этих двух точек нельзя делать вывод о Python 3.13.
Это воспроизведение, а не новое открытие. Тот же traceback на Python 3.14 уже зафиксирован в issue #35 nodriver, а предполагаемое исправление предложено в pull request #36. В версии 0.50.3 этот байт всё ещё присутствует. Метаданные PyPI указывают классификаторы только до Python 3.13 и не заявляют поддержку 3.14.
Обходной путь так же мал, как и сама ошибка: использовать проверенную версию интерпретатора или перекодировать тот самый файл в UTF-8, как предлагает pull request #36. После перекодирования пакет импортируется и исследуется в 3.14 без появления второй блокирующей проблемы. Чистый запуск на 3.12.13 ниже подтверждает один непропатченный путь; Python 3.13 в этом обзоре не тестировался.
На Python 3.12.13 import nodriver проходит без патча. Все цифры по браузерной части ниже получены именно из этой установки. Показатели размера и времени импорта рассчитаны для копии с одним байтом, перекодированным в 3.14, и это отдельно помечено. Python 3.13 здесь по-прежнему не проверялся; один результат на 3.12 не означает покрытие всего более раннего диапазона.
Если у вас по умолчанию стоят современные интерпретаторы — а многие команды быстро обновляют Python — это вполне реальная стена, пусть и легко устранимая. Лучше знать о ней заранее, чем потерять полдня на SyntaxError в файле, который вы никогда не писали.
Что такое nodriver под капотом
Короткое описание («нативный CDP, без webdriver») звучит как маркетинг, пока не посмотришь, что лежит внутри wheel-файла. nodriver поставляется со своим полным набором привязок к DevTools Protocol: пакет nodriver.cdp содержит 57 модулей по доменам протокола — accessibility, dom, network, page, fetch, runtime, target, storage, input, emulation и так далее. Именно это число модулей и есть техническая основа заявления «прямой CDP, без Selenium». Вместо вызова chromedriver, который говорит по WebDriver и переводит команды, nodriver генерирует Python-объекты для доменов CDP и сам общается по протоколу через WebSocket. cdp/network.py — тот самый файл с плохим байтом — один из этих 57 сгенерированных модулей, поэтому проблема живёт в коде, который никто вручную не правит.
Поверх этого протокольного слоя лежит более удобная объектная модель. Объект Tab открывает 62 публичных метода, и возможности поиска здесь шире, чем у большинства драйверов: поиск текста через find() и find_all(), CSS через select() и select_all(), а также нативная точка входа xpath(). XPath, CSS и текстовый поиск в одном объекте действительно удобны; в других библиотеках ради XPath иногда приходится опускаться до evaluate(). Конструктор Config предоставляет user_data_dir, headless, browser_executable_path, browser_args, sandbox, lang (по умолчанию 'en-US'), host, port, expert и **kwargs. В ходе инвентаризации API Config(headless=True) сформировал 16 флагов запуска Chromium, включая --no-first-run, --no-default-browser-check, --remote-allow-origins=* и --homepage=about:blank. На этом этапе я не вызывал start(). Тесты браузера ниже были отдельным запуском.

У библиотеки есть и API, ориентированный на антидетект, — методы, существование которых я подтвердил, но поведение на реальных целях не проверял. Я отмечаю это один раз и двигаюсь дальше, потому что проверять такое на живом сервисе я сознательно не стал. Если говорить нейтрально, у nodriver более сдержанные имена, чем у некоторых stealth-брендированных конкурентов. Его история — архитектурная: CDP-native, свежий профиль на каждый запуск, — а не список методов в стиле detect_and_bypass. Это наблюдение про дизайн API, а не утверждение о результатах.
Эти числа получены через импорт пакета и стандартную интроспекцию Python — inspect, обход модулей, подсчёт атрибутов. Ни один сайт не затрагивался. Если хотите повторить, цифры прямо извлекаются из пакета; это не субъективные ощущения.
Что он сообщает о себе

Вот вопрос, на который можно ответить, вообще не подходя к реальной защите: что сообщает о себе браузер, когда nodriver им управляет? Я поднял страницу, которая читает очевидные вещи — navigator.webdriver, user-agent, platform, languages, количество плагинов и аппаратные характеристики, форму window.chrome, ответы Permissions API, геометрию окна и экрана, — отдал её с 127.0.0.1 и направил на неё четыре стека: nodriver, Botasaurus, а также стандартные Playwright и Puppeteer как контрольные. Все четыре управляли одной и той же сборкой Chrome (Chrome for Testing 151.0.7922.10), так что различия относятся к библиотеке, а не к браузеру. Headless и headed, по три прогона каждый. Все значения ниже совпадали во всех трёх повторах.
| Стек | Режим | navigator.webdriver | Токен 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"] |
Самое явное отличие — один булев флаг. При этих конфигурациях запуска nodriver отдаёт navigator.webdriver = false, а стандартные Playwright и Puppeteer — true, и в headless, и в headed режиме. Тест показывает разницу на уровне конфигурации; он не доказывает, что значение следует просто из отсутствия WebDriver-бинарника.
Во всех четырёх стеках свойство остаётся нативным геттером браузера на Navigator.prototype — function get webdriver() { [native code] }, а не собственным свойством экземпляра и не подменённой функцией. Свойство не переписывалось JavaScript-кодом страницы после загрузки. Тест не выяснял, какой именно аргумент запуска или путь исходного кода приводит к отличию.
Вторая деталь и есть то, что охлаждает маркетинговое обещание. В headless-режиме user-agent у nodriver всё равно сообщает HeadlessChrome/151.0.0.0 — точно так же, как у стандартного Puppeteer и стандартного Playwright. В headed-режиме строка меняется на Chrome/151.0.0.0, снова в точности совпадая. Если вы думали, что антидетект-библиотека по умолчанию маскирует самую известную самоидентифицирующую строку в браузерной автоматизации, то нет. За это придётся отвечать самому.
Почти всё остальное оказалось одинаковым у всех четырёх стеков, и это важно сказать прямо, чтобы не раздувать историю — все свойства ниже совпали у nodriver, Botasaurus, Playwright и Puppeteer:
| Свойство | Значение, одинаковое для всех четырёх стеков |
|---|---|
platform | MacIntel |
vendor | Google Inc. |
| Plugins | пять |
| MIME types | два |
pdfViewerEnabled | true |
| Logical cores | двенадцать |
| Reported device memory | 16 GB |
| Touch points | zero |
window.chrome | присутствует, с app/csi/loadTimes, но без runtime |
| WebGL renderer string | одинаковая у всех четырёх |
Старая классика, когда Permissions API и Notification.permission противоречат друг другу, нигде не проявилась — все четыре стека показали default и prompt согласованно. Я также проверил document и window на остатки вида cdc_, которые были известны по старым WebDriver-стекам: пусто у всех четырёх.
Единственное место, где nodriver выглядит даже более похожим на «голый» автоматизированный браузер, чем на контролируемый, — это геометрия окна. В headless-режиме nodriver показывает outerWidth/outerHeight 0×0 при размере экрана 800×600; headless Playwright показывает 1280×720, потому что сам задаёт viewport. Puppeteer ведёт себя как nodriver и остаётся на 0×0. Это различие конфигурации по умолчанию, а не возможностей, и его можно изменить.
Ещё один момент, который стоит знать перед внедрением: nodriver не поставляет браузер, поэтому из коробки он использует тот Chrome, который уже установлен на машине. На моей системе это автоматически определился /Applications/Google Chrome.app — Chrome 150.0.7871.187 — и именно эту версию сообщал user-agent, а не зафиксированную. Версия браузера, которую будет раскрывать ваша инфраструктура, — это та версия, что реально установлена у вас.
Нужно честно сказать, что это такое и чем не является. Это запись того, что сообщает автоматизированный стек, когда его никто не просит скрываться, — полезно с позиции защиты и полезно, если вы хотите понимать, что транслируют ваши собственные инструменты. Но это не мера того, насколько это важно для какого-то конкретного сервиса. Я этого не проверял, и ни одну строку выше не следует читать как обещание результата.
Может ли он вообще забирать контент со страницы?
Самообъявление — это одно, а вернуть правильный HTML — совсем другая работа. Я прогнал nodriver на той же трёхклассовой тестовой странице, которую использует остальная часть этого репозитория бенчмарков, чтобы цифры совпадали с другими инструментами. На странице есть три сущности: A — статическая ссылка, маркер которой буквально присутствует в байтах ответа; B — узел, созданный встроенным скриптом во время парсинга, где и маркер, и URL собираются из фрагментов, так что увидеть их можно только выполнив JavaScript; и C — узел, внедрённый через 800 мс после события load, собранный тем же способом. Класс C — самый сложный: чтение на событии load его не увидит.
| Стек | Чтение по умолчанию | С явным ожиданием |
|---|---|---|
| nodriver 0.50.3 | 2 из 3 (A + B, пропускает C) | 3 из 3 |
| Botasaurus 4.0.92 | 2 из 3 | 3 из 3 |
| Playwright 1.56.0 | 2 из 3 | 3 из 3 |
| Puppeteer 24.16.0 | 2 из 3 | 3 из 3 |
nodriver приходит ровно туда же, куда и тяжеловесы. browser.get() с последующим tab.get_content() — это снимок на момент load: JavaScript он исполняет корректно, что доказывает класс B, которого в байтах страницы нет, но всё, что внедряется после load, он пропускает. Добавьте tab.select("#delayed-injected") — и получаете все три. Та же самая ловушка, то же самое исправление, что у Playwright и Puppeteer. Стабильно в трёх повторах и трёх отдельных полных запусках набора тестов, без флейков.
Я также менял задержку внедрения, чтобы понять, где чтение по умолчанию начинает отставать. nodriver перестаёт видеть класс C, когда внедрение происходит через 100 мс и больше после load — ровно та же граница, что и у обоих стандартных контролов. (Botasaurus здесь выбивается из ряда, и это действительно интересное отличие двух антидетект-библиотек: его get() по умолчанию ждёт полного завершения загрузки страницы, поэтому стандартное чтение всё ещё ловит внедрение на 300 мс. За это он платит примерно 250 мс на каждую навигацию.)
Само ожидание тоже имеет особенность, которую стоит заложить в бюджет. В этом прогоне tab.select() у nodriver попадал в грубые полусекундные циклы опроса, а не точно следовал задержке внедрения:
| Задержка класса C | 0 ms | 100 ms | 400 ms | 800 ms | 1500 ms |
|---|---|---|---|---|---|
nodriver select() | 124–152 ms | 1132–1141 | 1128–1129 | 2132–2177 | 2138–2150 |
Puppeteer waitForSelector | 113–129 ms | 203–216 | 512–516 | 911–919 | 1608–1611 |
На этом тесте внедрение через 100 мс приводило примерно к 1,1 секунды ожидания для select(). Внутренний цикл реализован как await self, а затем await self.sleep(0.5) после промаха; суммарный цикл в измерении оказался около одной секунды, но await self не следует считать универсальным фиксированным сном. Все тестовые отложенные узлы были найдены. У Puppeteer ожидатель отслеживал задержки заметно точнее. На множестве последовательных ожиданий разница могла бы накапливаться, хотя в этом обзоре не измерялась страница на тридцать селекторов.
Запуск — это ещё одно место, где «асинхронный и лёгкий» сталкивается с реальностью. Подъём браузера у nodriver оказался в той же зоне, что и у Botasaurus и Puppeteer, и заметно медленнее Playwright:
| Стек | Запуск браузера, по всем прогонам |
|---|---|
| nodriver 0.50.3 | 910–1583 ms |
| Botasaurus 4.0.92 | 986–1151 ms |
| Puppeteer 24.16.0 | 969–1008 ms |
| Playwright 1.56.0 | 282–365 ms |
После запуска nodriver быстрее всех четырёх на этапе навигации и чтения — 119–129 мс. Лёгкий на руле, обычный при запуске.
Установка и размер: действительно сильная сторона
Вот где nodriver заслуживает ярлык «сфокусированный», и это проверяемый, безобидный факт установки, а не что-то про scraping. Обычный pip install nodriver разворачивается так:
| Факт установки | nodriver 0.50.3 |
|---|---|
| Заявленных прямых runtime-зависимостей | три — websockets, mss, deprecated |
| Транзитивная зависимость | wrapt (через deprecated) |
Измеренный размер site-packages | около 17,2 МБ в 6 dist-info-каталогах, включая pip в среде |
| Доля самого nodriver | 3,7 МБ |
| numpy / lxml | нет ни того, ни другого |
| Бинарник браузера при установке | не скачивается |
Для категории, которая обычно тянет за собой целый рендеринговый стек, это очень лёгкая установка.
Важно различать прямые и транзитивные пакеты — это влияет на сопровождение. В метаданных nodriver запрашивает mss, websockets и deprecated; wrapt появляется потому, что он нужен deprecated. Шестой каталог dist-info в среде на 17,2 МБ — это pip, который уже был частью виртуального окружения. Поэтому «17,2 МБ в шести дистрибутивах» описывает измеренную среду, а «четыре добавленных runtime-пакета» — то, к чему пришла установка. Это связанные, но не взаимозаменяемые метрики.
Контраст, который делает цифру осмысленной: на той же машине родственный фреймворк Botasaurus весит 122,3 МБ в 44 пакетах — примерно в семь раз больше по размеру на диске. Это разница между компактным CDP-драйвером и фреймворком «из коробки», и она двусторонняя. nodriver даёт тонкое, читаемое дерево зависимостей, которое реально проверить; Botasaurus предлагает больше готового функционала, но платит за это местом на диске и поверхностью зависимостей. Абстрактно «лучшего» тут нет — всё зависит от того, нужен ли вам драйвер или фреймворк, — но если вам важна маленькая и понятная установка, nodriver для своего класса удивительно аккуратен.
Малый размер на диске не означает маленький импорт. На копии под Python 3.14 с исправленным байтом import nodriver занимал примерно 158 мс (медиана по свежим subprocess-import, примерно 151–199 мс). Непатченный импорт в 3.14 всё равно падает, поэтому времени для него не приводится. Все 57 модулей CDP грузятся заранее, и после импорта резидентная память составляла 31,5–31,9 МБ ещё до появления процесса Chrome. Поднятый браузер добавил бы намного больше, и в это измерение он не включался.
К этим цифрам нужно прикрепить две оговорки. Они сняты на одной машине — macOS arm64 — и показатели размера/импорта измерялись в Python 3.14 на копии, где один байт был перекодирован, потому что непропатченный пакет в этом интерпретаторе вообще не импортируется. На поддерживаемом Python патч не нужен: я подтвердил чистый непропатченный импорт на 3.12.13, и именно оттуда взяты все браузерные измерения. И на рантайме всё равно нужен настоящий бинарник Chrome, Chromium, Edge или Brave — nodriver управляет уже существующим браузером, а не поставляет свой, поэтому эта часть затрат лежит вне 17 МБ; как видно из раздела о раскрытии данных, браузер, который установлен у вас, и есть та версия, которую увидит трафик.
Лицензия — вот настоящее решение о внедрении

Большинство обзоров бесплатного инструмента считают фразу «это open source» концом разговора о лицензии. Для nodriver это только начало, потому что лицензия здесь — AGPL-3.0 — что подтверждается и в LICENSE.txt внутри wheel, и в spdx_id репозитория. Это сильная copyleft-лицензия сетевого типа, и она принципиально отличается от того, что требуют соседи.
Те инструменты, с которыми чаще всего сравнивают nodriver, обычно имеют разрешительные лицензии — и именно поэтому контраст получается наглядным:
| Инструмент | Лицензия | Если вы запускаете изменённую копию как сетевой сервис |
|---|---|---|
| nodriver | AGPL-3.0 | Раздел 13 может требовать предложить удалённым пользователям исходный код изменённой версии |
| Playwright | Apache-2.0 | нет аналогичной сетевой copyleft-обязанности |
| Puppeteer | Apache-2.0 | то же самое |
| Botasaurus | MIT | то же самое |
Здесь важен объём действия. Раздел 13 AGPL относится к изменённой версии программного обеспечения, используемой для удалённого сетевого взаимодействия. В этом обзоре не определяется, входит ли в охват связанный вокруг сервисный код, и не разбираются пограничные случаи внутреннего использования или корпоративного контура. Если вы модифицируете nodriver для хостируемого продукта, лицензию и архитектуру нужно проверять вместе с юристом. Это технический флаг для внедрения, а не юридическая консультация.
Я не высказываюсь, хорошо это или плохо — copyleft вполне легитимен, и многие серьёзные проекты используют его. Я лишь отмечаю, что «nodriver свободен и открыт» — это правда, но неполная. Обязательство реально, оно отличается от разрешительного стандарта в этой части экосистемы, и его нужно учитывать в решении, а не сводить всё к слову «бесплатный». (Кстати, PyPI вообще не показывает лицензионный классификатор; текст AGPL лежит в wheel, а SPDX-id — в репозитории, так что не стоит рассчитывать, что пакетный индекс сам подскажет это за вас.)
Метаданные на дату измерения
Снимок репозитория и пакета на момент времени, прямо из GitHub API и PyPI:
| Факт | Значение на 14 июля 2026 |
|---|---|
| Stars | 4 511 |
| Forks | 422 |
| Open issues | 14 |
| Создан | февраль 2024 |
| Последний push | май 2026 |
| Последний релиз PyPI | 0.50.3 |
| Wheel | чистый Python py3-none-any |
requires-python | >=3.9 |
| Классификаторы Python | 3.7–3.13 |
Сигналы по сопровождению смешанные: в репозиторий делали push в мае 2026, а последний измеренный пакет всё ещё содержал проблему с импортом в Python 3.14, и к дате исследования предложенное исправление ещё не было выпущено. Stars и число открытых задач сами по себе не отвечают, соответствует ли такой темп вашему порогу поддержки.
Плюсы и минусы
Плюсы:
- Компактная и понятная установка: три заявленные зависимости плюс транзитивный
wrapt, около 17,2 МБ в шестиdist-info-каталогах в измеренной среде (один из них —pip), без numpy/lxml и без скачивания браузера при установке. - Действительно CDP-native: поставляет собственные 57 модулей привязок к DevTools Protocol и общается по протоколу напрямую, без бинарника chromedriver/Selenium в цепочке.
- Широкая поверхность поиска элементов у
Tab(62 публичных метода) с нативными XPath, CSS и текстовым поиском — без необходимости уходить в сыройevaluate()ради XPath. - Асинхронный по дизайну, с новым профилем на каждый запуск и чистым
Configдля основных параметров (headless, путь к исполняемому файлу, аргументы, язык, порты). - Достаёт контент со страницы с JavaScript так же уверенно, как крупные игроки: 2 из 3 классов контента при чтении по умолчанию и 3 из 3 с явным ожиданием — как у стандартных Playwright и Puppeteer на том же тесте, стабильно в трёх прогонах. На этапе навигации и чтения был самым быстрым из четырёх: 119–129 мс.
- По умолчанию отдаёт
navigator.webdriver = falseтам, где стандартные контролы возвращаютtrue, причём без патчинга свойства — дескриптор остаётся нативным геттером браузера. - Чёткая архитектурная идентичность как CDP-native преемника undetected-chromedriver.
Минусы:
- Не импортируется на Python 3.14 «из коробки» — один байт вне UTF-8 (
cdp/network.py) вызываетSyntaxErrorуже на этапе импорта. Воспроизведено по открытому issue #35, в 0.50.3 проблема всё ещё не исправлена. Либо фиксируйте интерпретатор на ≤3.13 (чисто проверено на 3.12.13), либо перекодируйте файл. - AGPL-3.0 — это реальный фактор для тех, кто планирует запускать изменённую копию как сетевой сервис; лицензия жёстче, чем Apache/MIT у соседей.
- Малый размер установки не означает маленький импорт: примерно 158 мс cold start и около 31,5 МБ резидентной памяти до запуска браузера, потому что все 57 CDP-модулей грузятся заранее.
tab.select()опрашивает с полусекундной задержкой, поэтому короткие ожидания округляются вверх — 100 мс в этом сценарии превращались примерно в 1,1 секунды, тогда как ожидатель Puppeteer стоил около 210 мс. Всегда корректно, но на множестве мелких ожиданий это заметно.- В headless-режиме строка user-agent по умолчанию всё ещё содержит
HeadlessChrome, точно так же, как у стандартных контролов; базовая конфигурация не скрывает самый очевидный идентифицирующий признак. - На рантайме по-прежнему нужен реальный бинарник Chrome/Chromium/Edge/Brave; лёгкая установка через pip — лишь половина истории зависимостей, а версия установленного у вас Chrome и есть то, что будет раскрыто трафиком.
- Эффективность против любых антибот-систем здесь не проверялась — вся stealth-предпосылка намеренно осталась без теста.
Что я не тестировал и поэтому не могу комментировать: память на вкладку, задержки round-trip CDP, обработку профилей, пропускную способность на масштабе, Linux или Windows, конкретно Python 3.13 и — главное — реальную эффективность против антиботов на живом сервисе. Все браузеры в этом обзоре общались только с тестовой страницей на 127.0.0.1. Все цифры сняты на одной машине (macOS arm64); браузерные показатели получены на Python 3.12.13 без патча, а более старые цифры по размеру и импорту — на Python 3.14 с копией, где один байт был исправлен.
Я также не прогонял матрицу обновлений браузера, поэтому совместимость с будущими версиями Chrome остаётся задачей эксплуатации, а не результатом этого обзора.
Кому он подойдёт, а кому лучше пройти мимо
nodriver подойдёт, если вам нужен лёгкий асинхронный CDP-native драйвер для живого Chromium, и вы готовы сами управлять браузером, обновлениями и рантаймом. Компактное дерево зависимостей проще аудитировать, а CDP-first дизайн хорошо подходит для управления на уровне протокола. Пригодность для контейнеров осталась непроверенной: в этом обзоре не тестировались Linux-образы, установка браузера, системные библиотеки, sandboxing или очистка процессов.
Двум группам стоит посмотреть в другую сторону. Если у вас Python 3.14 и вы не готовы фиксировать интерпретатор или патчить vendored-файл, подождите выхода исправления — сегодня это жёсткий стоп. А если AGPL-3.0 мешает тому, как вы собираетесь выпускать продукт — например, хостируемому сервису с закрытыми изменениями — одной лицензии уже достаточно, чтобы рассмотреть более permissive-альтернативу до начала разработки. Это не упрёк коду; это ограничения, которые лучше узнать сейчас, чем на этапе compliance-review.
И ещё — лучше пропустить его, если вам на самом деле нужны данные, а не браузер, которым вы управляете вручную. nodriver даёт вам скриптуемый Tab и 62 метода; превратить отрендеренную страницу в чистые структурированные записи — это всё ещё ваша задача. Это уже другая работа, и именно на этом стыке появляется управляемый сервис.
Альтернативы и место Thunderbit
Сначала честная рамка: nodriver бесплатный, под AGPL и self-hosted. Вы сами запускаете браузер, сами следите за обновлениями, сами отвечаете за рантайм и все его сбои. Для разработчика, которому нужен именно такой контроль, ни один managed-сервис не может конкурировать по цене с библиотекой, которая у вас уже есть.
В мире open source сравнивайте по задаче, а не по логотипу. Если вы выбираете между драйверами для реального браузера, наше сравнение Playwright и Puppeteer покрывает двух очевидных тяжеловесов, рядом с которыми стоит nodriver. Scrapling — ближайший сосед на Python со stealth-уклоном, если именно это вас интересует. Если вам нужен LLM-ready output, а не прямое управление браузером, Crawl4AI рендерит страницы и возвращает Markdown, а Scrapy по-прежнему остаётся базовым фреймворком для крупных browserless-обходов. Если вы сравниваете несколько вариантов сразу, обзор open-source scraper-решений ставит категории рядом.
Раскрытие: эту статью публикует Thunderbit. Thunderbit — это управляемый сервис извлечения данных, так что сравнение здесь построено по задаче, а не по архитектуре. nodriver даёт вам слой управления браузером, который вы хостите и программируете сами; Thunderbit берёт на себя рендеринг и возвращает контент страницы или записи в виде схемы как сервис. Используйте nodriver, когда вам принципиально нужен контроль браузера на уровне протокола и self-hosting. Рассматривайте управляемый extractor, когда важнее итоговая структура данных и передача операционной нагрузки, чем владение браузером. Актуальные детали по лимитам, кредитам и пакетам смотрите на странице цен, а не внутри бенчмарка библиотеки.
Компромисс в том, где находится работа: nodriver оставляет управление браузером и сопровождение рантайма на вашей стороне без поминутной сервисной платы; managed API забирает этот слой на себя и берёт деньги за вызовы.
Попробовать Thunderbit для извлечения веб-данных
Итог
Берите nodriver, если вам нужен компактный, асинхронный, CDP-native драйвер для Chromium, вы уже подтвердили его на своей версии Python (у меня чисто импортировался на 3.12.13) и разобрались с тем, как вам подходит AGPL-3.0. В разрешённой среде рантайм-граф добавил четыре пакета, измеренное окружение занимало около 17 МБ, а библиотека поставляет 57 модулей CDP, поверхность Tab с 62 методами и нативный XPath. На этом тестовом стенде она возвращала 2 из 3 классов контента по умолчанию и 3 из 3 с ожиданием, как и стандартные Playwright и Puppeteer.
Но оговаривайте ограничения честно. На Python 3.14 она вообще не импортируется, пока вы не исправите один байт вне UTF-8 — это задокументированная, всё ещё открытая проблема, не загадка, но жёсткий стоп в день, когда вы на неё наткнётесь. Лицензия AGPL-3.0 — это реальное решение для тех, кто запускает изменённую копию как сервис, а не формальность. Маленькая установка не даёт маленького импорта, потому что все CDP-модули загружаются сразу, а полусекундный polling у select() делает короткие ожидания примерно секундными. По тому вопросу, на который я действительно смог ответить — что библиотека раскрывает по умолчанию, — картина уже уже, чем обещает маркетинг: от стандартного Puppeteer отличается один булев флаг, headless user-agent всё ещё говорит HeadlessChrome, а все остальные измеренные свойства у четырёх стеков совпали. И вся антидетект-предпосылка, из-за которой многие вообще находят nodriver, — это то, что я сознательно не тестировал. Я инвентаризировал библиотеку и гонял её по странице на своей машине, а не сталкивал с живой защитой, и мне кажется честнее сказать это прямо, чем обещать обход, за который я не могу поручиться. На вопросы, на которые я мог ответить, nodriver выглядит как хорошо сделанный, необычно компактный драйвер с двумя острыми краями — ограничением по версии Python и copyleft-лицензией — которые лучше увидеть заранее.
Попробовать Thunderbit для извлечения веб-данных Get Started Free
FAQ
Почему import nodriver ломается в Python 3.14?
Потому что в cdp/network.py есть байт ±, не закодированный в UTF-8, и при этом нет объявления кодировки исходника. Python 3.14.2 отвергает файл и обрывает транзитивный импорт; Python 3.12.13 импортирует тот же самый файл без патча. В этом обзоре не изолировалось точное изменение интерпретатора, и Python 3.13 не тестировался. Источник проблемы — issue #35 nodriver и pull request #36. Используйте проверенную вами версию или перекодируйте файл в UTF-8.
Что даёт "CDP-native, no webdriver", и сколько стоит установка?
nodriver поставляет 57 модулей доменов DevTools Protocol и общается по CDP через WebSocket вместо вызова chromedriver через Selenium. Tab предоставляет 62 метода, включая нативный XPath. Метаданные пакета заявляют три runtime-зависимости (websockets, mss, deprecated); при разрешении добавляется wrapt. В измеренной среде было около 17,2 МБ в шести каталогах dist-info, включая pip, без numpy, lxml и скачанного бинарника браузера. Два нюанса: импорт патченной копии занимал около 158 мс, потому что все 57 CDP-модулей грузятся заранее, и браузер семейства Chrome всё равно нужен отдельно.
Важна ли для моего проекта лицензия AGPL-3.0? Зависит от того, как вы распространяете решение. AGPL-3.0 — это лицензия сетевого copyleft: если вы запускаете изменённую версию nodriver как сервис для других пользователей, вы обязаны предложить им исходники этой изменённой версии. Для личного скрипта или внутреннего инструмента, который вы никуда не публикуете, это обычно не проблема. Для хостируемого коммерческого продукта, построенного на модифицированном nodriver, это уже серьёзный вопрос для compliance-ответственного — и лицензия заметно строже, чем Apache-2.0 и MIT у сопоставимых инструментов.
Что nodriver раскрывает о себе — и означает ли это, что он обходит Cloudflare?
Первая часть измерена; вторая — нет, и это принципиально. На странице, которую я отдал с 127.0.0.1, при управлении тем же Chrome, что и у контролов, navigator.webdriver возвращается как false, тогда как стандартные Playwright и Puppeteer показывают true. Это значение задаётся на этапе запуска браузера, а не через патч свойства — дескриптор всё ещё нативный геттер Chrome. Почти всё остальное совпало с контролями: тот же platform, пять плагинов, двенадцать ядер, 16 GB памяти устройства, та же форма window.chrome, нет противоречия у Permissions API и нет следов вида cdc_ в document или window. В headless user-agent всё ещё пишет HeadlessChrome/151.0.0.0, как и у обоих контролей, — это для вас не скрывается. Но всё это не говорит о том, работает ли что-то против реального антибот-сервиса. Я не отправлял nodriver на живой сайт, не обращался к антибот-сервису и не трогал CAPTCHA; это было исключено по замыслу. Таблица раскрытия выше показывает только то, что объявляет стек. Она ничего не говорит о том, кто это слушает и что с этим делает.
Корректно ли nodriver обрабатывает JavaScript-рендеренный контент?
Да, с обычной оговоркой про момент чтения. На тесте с тремя классами контента комбинация browser.get() + tab.get_content() по умолчанию возвращала 2 из 3 — JavaScript она выполняет правильно (синхронно внедрённый класс присутствует нигде в сырых байтах и всё равно был получен), но читает на событии load, поэтому пропускает всё, что появляется позже. Если добавить tab.select("#delayed-injected"), получается 3 из 3. На той же странице результат идентичен стандартным Playwright и Puppeteer. Учтите один нюанс: select() опрашивает с полусекундной паузой, поэтому ожидание в 100 мс оборачивается примерно в 1,1 секунды.


