Keywords
nodriver レビュー, Web スクレイピング, オープンソース, ベンチマーク
Content
nodriver は、undetected-chromedriver の作者 ultrafunkamsterdam による Python 製ブラウザ自動化ライブラリで、同プロジェクトの後継を名乗っています。設計上の大きな違いは、Selenium と chromedriver からの脱却にあります。nodriver は WebDriver のバイナリを介さず、Chrome DevTools Protocol(CDP)で Chromium と直接やり取りし、API は async ベースです。Playwright と Puppeteer は別の比較対象です。これらも WebDriver 派生ではなく、プロトコル駆動のブラウザ制御ツールです。違いとして重要なのは、API 設計、パッケージ構成、ブラウザの用意のされ方、そして互換性ポリシーです。
このレビューでは、nodriver 0.50.3 を実運用の防御突破テストではなく、パッケージとしての実態確認という観点で調べています。インストール済みパッケージ、import の挙動、API の広さ、ディスク使用量、ライセンスを確認し、ブラウザ起動は 127.0.0.1 で配信したページに対してのみ実施しました。ライブの対象サイト、アンチボットサービス、CAPTCHA は一切使っていません。結果は、パッケージの動作と既定のブラウザ公開情報を示すものであり、検知回避の有効性を証明するものではありません。
この範囲の中で、特に目立った点は 3 つです。まず、Python 3.14 ではライブラリがまったく import できません。たった 1 バイトの異物のせいで、何も呼び出す前にパッケージ全体が止まります。次に、このカテゴリのドライバーとしては驚くほど小さいこと。宣言された直接依存は 3 つ、解決された間接依存は 1 つだけで、測定環境では約 17 MB でした。第三に、私自身のページで見た限り、stock Playwright と stock Puppeteer と比べて nodriver が目に見えて違うのは boolean 1 つだけです。それでも headless の user-agent は 3 つとも HeadlessChrome のままでした。残りの“驚き”はライセンス面にあります。
Python 3.14 で import できない問題
まず最初にぶつかるのはここです。まだ自分のコードが動き出す前に、落ちます。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! とあります)。コメント #: JSON (±Inf). の ± に由来する 0xb1 という UTF-8 ではない 1 バイトが含まれているのに、ソースの文字コード宣言がありません。このモジュールは nodriver/__init__ → cdp/__init__ → network の順で読み込まれるため、パース失敗がそのまま import を止めます。パッケージ全体を調べたところ、他に非 UTF-8 のソースは見つかりませんでした。
境界条件の説明は慎重にする必要があります。Python 3.14.2 はこのファイルを拒否しますが、Python 3.12.13 では未修正のまま import できます。該当環境の network.py はバイト単位で同一で、SHA-256 は ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6 です。つまり、ソースファイル自体の違いが原因ではありません。このレビューでは、CPython のトークナイザ変更のどれが原因かまでは切り分けておらず、Python 3.13 も未検証です。ここで言えるのは、測定した 2 点の結果だけで、3.12 以前がすべて同じだとは断定していません。
この 2 点から Python 3.13 については結論を出せません。
これは新発見というより再現確認です。同じ Python 3.14 の traceback は nodriver issue #35 に記録されており、修正案は pull request #36 にあります。0.50.3 にはまだこのバイトが残っています。PyPI の classifier は Python 3.13 までで、3.14 対応はうたっていません。
回避策はバグと同じくらい小さいです。検証済みの Python を使うか、pull request #36 が提案しているように該当ファイルを UTF-8 へ再エンコードしてください。再エンコード後は、3.14 でもパッケージの import と introspection が通り、別の障害は出ません。下のクリーンな 3.12.13 の結果は、未修正パスの一例です。このレビューでは Python 3.13 は未検証です。
普段から最新の Python を使うチームは少なくありません。そういう環境では、これは小さいとはいえ実在する壁です。存在を知っておけば、書いた覚えのないファイルの SyntaxError で午後を無駄にせずに済みます。
nodriver の中身は何か
「CDP ネイティブ、webdriver なし」という一文は、wheel の中身を見るまでは宣伝文句に聞こえます。nodriver には DevTools Protocol のバインディングが丸ごと同梱されており、nodriver.cdp パッケージには 57 個のプロトコル・ドメインモジュールがあります。accessibility、dom、network、page、fetch、runtime、target、storage、input、emulation などが並びます。このモジュール数が、「CDP を直接叩く、Selenium なし」という主張の実体です。WebDriver を話す chromedriver 実行ファイルを呼び出す代わりに、nodriver は CDP の各ドメインに対応する Python オブジェクトを生成し、自身が WebSocket 経由でプロトコルを話します。cdp/network.py は、その 57 個の自動生成モジュールの 1 つで、問題のバイトがあるのも手作業で編集されない生成コードだからです。
そのプロトコル層の上に、より扱いやすいオブジェクトモデルが載っています。Tab オブジェクトは 62 個の public method を持ち、探索機能もかなり広いです。find() / find_all() によるテキスト検索、select() / select_all() による CSS 検索、そしてネイティブな 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 ネイティブで、実行ごとに新しいプロファイルを使う、というものです。これは API 設計についての観察であって、結果の主張ではありません。
これらのカウントは、パッケージを import して Python 標準の introspection、つまり inspect、モジュール走査、属性カウントで取得しました。サイトには一切触れていません。再実行したいなら、数値はパッケージそのものから出てきます。感覚論ではありません。
自分自身について何を明かすのか

実際の防御に触れずに答えられる問いがあります。nodriver がブラウザを動かしたとき、そのブラウザは閲覧先のページに何を明かすのか、です。そこで私は、navigator.webdriver、user-agent、platform、languages、plugin と hardware の数、window.chrome の形、Permissions API の返答、window と screen の座標など、目につく項目を読むページを作り、127.0.0.1 で配信して、4 つのスタックを向けました。nodriver、Botasaurus、そして control として stock Playwright と stock Puppeteer です。4 つとも同じ Chrome ビルド(Chrome for Testing 151.0.7922.10)を使っているため、違いが出るならライブラリ側の差であってブラウザの差ではありません。headless / headed の各 3 回ずつ実行し、以下の値はすべて 3 回とも一致しました。
| スタック | モード | 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"] |
もっとも分かりやすい差は、この boolean 1 つです。既定の起動設定では、nodriver は navigator.webdriver を false と返し、stock Playwright と stock Puppeteer は headless / headed の両方で true を返しました。このテストが示すのは設定レベルの違いであって、WebDriver バイナリがないことだけでその値になると証明するものではありません。
4 つのスタックすべてで、このプロパティは Navigator.prototype 上のブラウザ本来の getter のままでした。つまり function get webdriver() { [native code] } です。インスタンスの own property になっていたり、関数が差し替えられていたりはしません。ページ JavaScript による load 後の書き換えもありませんでした。起動引数や、値の違いを生んだソースパスまでは特定していません。
2 つ目のポイントは、宣伝の言い回しを補正するものです。headless モードでは、nodriver の user-agent も HeadlessChrome/151.0.0.0 を名乗ります。 stock Puppeteer とも、stock Playwright とも同じです。headed なら Chrome/151.0.0.0 になり、これも同様に一致します。もし anti-detection ライブラリなら、ブラウザ自動化で有名な自己申告文字列を既定で隠すはずだと思っていたなら、それは違います。そこは自分で設定するものです。
他のほとんどは 4 つのスタックで同じでした。これははっきり言っておくべきです。話を狭くしてくれるからです。以下の項目は、nodriver、Botasaurus、Playwright、Puppeteer のすべてで同じ値でした。
| 項目 | 4 つすべてで同一の値 |
|---|---|
platform | MacIntel |
vendor | Google Inc. |
| Plugins | 5 |
| MIME types | 2 |
pdfViewerEnabled | true |
| Logical cores | 12 |
| Reported device memory | 16 GB |
| Touch points | 0 |
window.chrome | あり、app / csi / loadTimes はあるが runtime はない |
| WebGL renderer string | 4 つすべてで同一 |
古典的な Permissions API と Notification.permission の食い違いは、どこにも出ませんでした。4 つとも default と prompt で一致していました。さらに、古い WebDriver 系で知られる cdc_ 風の残骸を document と window で探しましたが、4 つとも空でした。
nodriver が control より“素の自動化ブラウザらしく”見える唯一の箇所は、ウィンドウのジオメトリです。headless の nodriver は、800×600 の画面上で outerWidth / outerHeight が 0×0 でした。headless Playwright は viewport を自分で設定するため 1280×720 になります。Puppeteer は nodriver と同じく 0×0 でした。これは能力差ではなく、既定設定の違いです。必要なら変更できます。
本番投入前に知っておくべきもう 1 点があります。nodriver はブラウザ本体を同梱しないため、箱出しではマシンに既に入っている Chrome を使います。私の環境では /Applications/Google Chrome.app を自動検出し、Chrome 150.0.7871.187 を使っていました。公開された user-agent も固定版ではなく、そのバージョンでした。つまり、あなたの環境がどのブラウザを持っているかが、そのまま外部に見えるブラウザ版になります。
ここで言うべきことと、言うべきでないことを分けておきます。これは、自動化スタックが隠すよう依頼されていないときに何を公開するかの記録です。守る側には有用であり、自分のツールが何を外へ出しているかを知りたい人にも有用です。しかし、特定のサービスに対してそれが有効かどうかの指標ではありません。そこはテストしていませんし、上のどの行も結果を意味するものではありません。
本当にページの内容を取れるのか
自己申告と、正しい HTML を返すことは別問題です。そこで nodriver を、このベンチマークリポジトリの他ツールと同じ 3 種類のコンテンツを持つフィクスチャに対して動かしました。これで他の測定結果と揃います。ページには 3 つの要素があります。A は配信バイト列にそのまま含まれている静的リンク、B は parse 中に inline script で構築されるノードで、マーカーと URL は断片から組み立てられるため JavaScript を実行しないと見えません。C は load イベントの 800 ms 後に注入されるノードで、同じ方法で構築されます。Class C は adversarial な要素で、load イベント時点で読むと見えません。
| スタック | 既定の読み取り | 明示的な待機あり |
|---|---|---|
| nodriver 0.50.3 | 3 つ中 2(A + B、C を見逃す) | 3 つ中 3 |
| Botasaurus 4.0.92 | 3 つ中 2 | 3 つ中 3 |
| Playwright 1.56.0 | 3 つ中 2 | 3 つ中 3 |
| Puppeteer 24.16.0 | 3 つ中 2 | 3 つ中 3 |
nodriver は、重厚なツール群と同じ地点に着地します。browser.get() の直後に tab.get_content() を呼ぶと、それは load 時点のスナップショットです。JavaScript は正しく実行されます。実際、class B は配信バイト列のどこにも存在しないのに取得できました。しかし、load 後に注入されるものは見逃します。tab.select("#delayed-injected") を足せば、3 つすべて取れます。Playwright や Puppeteer と同じ落とし穴、同じ対処です。3 回の反復と、ベンチマーク全体の 3 回の別実行で安定しており、flaky はありませんでした。
どの時点で既定読み取りが諦めるのかを調べるため、注入遅延を振りました。nodriver は load 後 100 ms 以上で注入される class C を見失います。これは stock control 2 つと同じ境界です。(ここで Botasaurus だけが例外で、2 つの anti-detect ライブラリの中で本当に面白い差です。get() が既定で完全ロードまでブロックするため、既定読み取りでも 300 ms の注入を拾います。その代わり、ナビゲーション 1 回あたり約 250 ms 余計にかかります。)
待機処理そのものにも、予算を見ておきたい癖があります。このスイープでは、nodriver の tab.select() は注入遅延を細かく追うのではなく、粗い polling の区切りで動いていました。
| Class-C の遅延 | 0 ms | 100 ms | 400 ms | 800 ms | 1500 ms |
|---|---|---|---|---|---|
nodriver select() | 124–152 ms | 1132–1141 | 1128–1129 | 2132–2177 | 2138–2150 |
Puppeteer waitForSelector | 113–129 ms | 203–216 | 512–516 | 911–919 | 1608–1611 |
このフィクスチャでは、100 ms の注入で select() はおよそ 1.1 秒でした。インストール済みのループは、ミスの後に await self と await self.sleep(0.5) を行います。今回の合成サイクルはおよそ 1 秒付近でしたが、await self が普遍的な固定時間スリープだと断定はしていません。テストした遅延ノードはすべて見つかりました。Puppeteer の waiter は、これらの遅延にもっと忠実に追従していました。多数の連続待機では差が積み上がる可能性がありますが、このレビューでは 30 セレクタの実運用ページまではベンチマークしていません。
起動処理も、async かつ軽量という説明が現実とぶつかる部分です。ブラウザ起動では 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 |
起動後の navigate-and-read は、4 つの中で nodriver が最速で 119–129 ms でした。ハンドルを握ったときは軽快、点火は普通、という感じです。
インストールとフットプリント: 本当に良い部分
ここで nodriver は「フォーカスが絞られている」という評価に値します。これはスクレイピング云々ではなく、検証可能なインストール上の事実です。クリーンな pip install nodriver が解決する内容は次の通りです。
| インストール上の事実 | nodriver 0.50.3 |
|---|---|
| 直接ランタイム依存数 | 3 — websockets、mss、deprecated |
| 解決された間接依存 | wrapt(deprecated 経由) |
測定した site-packages 合計 | 約 17.2 MB、6 個の dist-info ディレクトリ(環境の pip を含む) |
| nodriver 自体の占有分 | 3.7 MB |
| numpy / lxml | どちらもなし |
| インストール時のブラウザバイナリ | 追加ダウンロードなし |
レンダリング系を丸ごと抱え込むことが多いカテゴリとしては、かなり軽いほうです。
直接パッケージと間接パッケージを分けて見るのは、保守の観点で重要です。nodriver のメタデータが要求するのは mss、websockets、deprecated です。wrapt は deprecated が必要とするために入ってきます。17.2 MB の環境で 6 つ目の dist-info は pip で、これはその仮想環境にもともと含まれていました。したがって、「6 配布物で 17.2 MB」は測定環境の説明であり、「追加されたランタイムパッケージは 4 つ」はインストールの解決結果の説明です。関連はありますが、同じ意味ではありません。
この数字の意味が分かる対比を挙げると、同じマシン上で sibling framework の Botasaurus は 44 パッケージで 122.3 MB、つまりディスク占有は 約 7 倍です。これは、フォーカスの絞られた CDP ドライバーと、あれこれ同梱したフレームワークの差です。どちらにも長所と短所があります。nodriver は、監査しやすい薄い依存ツリーを提供します。Botasaurus は、最初からいろいろ揃っていて、その代わりディスクと依存範囲を支払います。抽象的にどちらが「良い」とは言えません。ドライバーが欲しいのか、フレームワークが欲しいのか次第です。ただし、小さくて見通しの良いインストールを重視するなら、nodriver はこの用途にしてはかなり綺麗です。
ディスクが小さいことは、import が軽いことを意味しません。3.14 のバイト修正済みコピーでは、import nodriver に約 158 ms かかりました(新規 subprocess import の中央値で、およそ 151–199 ms)。未修正の 3.14 では import 自体が失敗するため、そこでのタイミングは出していません。57 の CDP ドメインモジュールが eager に読み込まれるため、import 後の常駐メモリは Chrome プロセスが立ち上がる前でも 31.5–31.9 MB でした。起動済みのブラウザはこれにさらに大きく加わりますが、このメモリ測定には含めていません。
これらの数字には 2 つの注意書きをつけておきます。計測は 1 台のマシン、macOS arm64 です。フットプリントと import 時間は、未修正版では 3.14 で import できないため、1 バイト修正済みコピーを使って測っています。対応済みの Python ではパッチは不要です。実際に私は 3.12.13 で未修正版の clean import を確認しており、ブラウザ計測はすべてそこからです。そして、実行時には実際の Chrome、Chromium、Edge、Brave のバイナリが必要です。nodriver は既存ブラウザを制御するのであって、同梱はしません。したがってそのインストールコストは 17 MB の外側にありますし、公開情報の章で見た通り、マシンにある Chrome のバージョンがそのままトラフィック上の公開バージョンになります。
本当に重要なのはライセンスです

無料ツールのレビューでは、「オープンソースだから」でライセンス話が終わってしまいがちです。nodriver では、そこが始まりです。ライセンスは AGPL-3.0 で、wheel の LICENSE.txt とリポジトリの spdx_id の両方で確認しました。これは強い network copyleft ライセンスで、周辺ツールが求める条件とはかなり違います。
nodriver を比較対象として見る人が多い相手は permissive なライセンスなので、その差ははっきりしています。
| ツール | ライセンス | 修正済みコピー をネットワークサービスとして運用する場合 |
|---|---|---|
| nodriver | AGPL-3.0 | Section 13 により、修正版の対応ソースをリモート利用者に提供する義務が生じる場合があります |
| Playwright | Apache-2.0 | 同等の network copyleft 条項はなし |
| Puppeteer | Apache-2.0 | 同上 |
| Botasaurus | MIT | 同上 |
ここでは対象範囲が重要です。AGPL の Section 13 は、対象プログラムを修正した版をネットワーク越しに使わせるケースを扱います。このレビューでは、周辺の別サービスコードがライセンス対象に含まれるかどうか、あるいは社内利用や企業境界の境目がどうかまでは判断しません。ホスト型プロダクトで nodriver を修正するなら、ライセンス本文と構成を法務と一緒に確認してください。これは技術上の導入フラグであって、法的助言ではありません。
AGPL が良いとか悪いとかを論じたいわけではありません。copyleft は正当な選択ですし、真面目なプロジェクトでも普通に使われます。ここで言いたいのは、「nodriver は無料でオープンソース」であることは真実だが、それだけでは不十分だということです。その義務は本物で、このエコシステムの permissive 標準とは異なり、決定材料に入れるべきものです。「無料」に丸め込んではいけません。(ちなみに PyPI 側には license classifier がありません。AGPL の本文は wheel にあり、SPDX ID は repo にあるので、パッケージインデックスに頼って見つけるのは危険です。)
メタデータの時点情報
GitHub API と PyPI から取得した、時点付きの repo / package 情報です。
| 項目 | 2026 年 7 月 14 日時点の値 |
|---|---|
| Stars | 4,511 |
| Forks | 422 |
| Open issues | 14 |
| 作成時期 | 2024 年 2 月 |
| 最終 push | 2026 年 5 月 |
| 最新 PyPI リリース | 0.50.3 |
| Wheel | 純 Python の py3-none-any |
requires-python | >=3.9 |
| Python classifier | 3.7–3.13 |
見える保守シグナルはまちまちです。repo は 2026 年 5 月に push されていますが、私が測定した最新パッケージには Python 3.14 の import 問題がまだ残っており、修正案は研究時点では未リリースでした。star 数や open issue 数だけでは、この更新ペースが自分の許容ラインかどうかは決まりません。
長所と短所
長所:
- 小さくて見通しの良いインストール。直接依存 3 つ + 間接依存
wrapt、測定環境では 6 個のdist-infoで約 17.2 MB(1 つはpip)。numpy/lxml なし、インストール時のブラウザダウンロードなし。 - 本当に CDP ネイティブ。57 モジュールの DevTools Protocol バインディングを同梱し、chromedriver / Selenium バイナリを介さず直接プロトコル通信する。
Tabの要素探索面が広い(public method 62 個)。XPath、CSS、テキスト検索が最初から使え、XPath のために生のevaluate()に降りなくてよい。- async 設計で、毎回 fresh profile。headless、実行ファイルパス、引数、言語、ポートなどの基本設定を扱う
Configも素直。 - 3 種類のコンテンツクラスを持つページからの取得は重厚なツール群と同等。既定読み取りで 3 つ中 2、待機ありで 3 つ中 3。stock Playwright / Puppeteer と同じ結果で、3 回の実行でも安定。navigate-and-read は 119–129 ms で 4 つ中最速。
- 既定で
navigator.webdriverをfalseと返し、stock コントロールがtrueを返すのと対照的。しかもプロパティ自体は書き換えず、descriptor はブラウザ本来の native getter のまま。 - undetected-chromedriver の CDP ネイティブ後継として、アーキテクチャ上の輪郭が明確。
短所:
- そのままでは Python 3.14 で import できない。
cdp/network.pyという 1 バイトの非 UTF-8 ファイルが import 時にSyntaxErrorを起こす。issue #35 でも再現され、0.50.3 では未修正。3.13 以下に固定するか、ファイルを再エンコードする必要がある。 - AGPL-3.0 は、修正済みコピーをネットワークサービスとして運用する人にとって現実的な導入判断材料。Apache / MIT 系より厳しい。
- ディスク使用量が小さいからといって import も軽いわけではない。57 の CDP モジュールを全部前もって読み込むため、cold start は約 158 ms、常駐メモリは約 31.5 MB。
tab.select()は 0.5 秒バックオフの polling なので、短い待機は丸め込まれる。100 ms の待機が約 1.1 秒になる。正確ではあるが、小さな待機を大量に積むと効いてくる。- headless では user-agent に
HeadlessChromeをそのまま出す。stock コントロールと同じで、既定設定ではもっとも分かりやすい自己申告文字列は隠れない。 - 実行時には本物の Chrome / Chromium / Edge / Brave バイナリがまだ必要。pip インストールが軽いのは半分だけで、どの Chrome が入っているかが、そのまま公開バージョンになる。
- いかなるアンチボット対策に対して有効かは、このレビューでは未検証。stealth 前提そのものを意図的に試していません。
ここでテストしていないため、コメントできないもの: タブごとのメモリ、CDP の往復遅延、プロファイル管理、大規模スループット、Linux / Windows、Python 3.13 固有の挙動、そして最重要の、実運用のライブサービスに対するアンチボット回避性能です。このレビューで使ったブラウザはすべて 127.0.0.1 のフィクスチャにしか接続していません。数値はすべて 1 台のマシン(macOS arm64)での結果で、ブラウザ性能の計測は Python 3.12.13 の未修正版、古いフットプリントと import 時間の計測は Python 3.14 の 1 バイト修正版で行っています。
また、ブラウザ更新マトリクスも実行していないため、将来の Chrome リリースとの互換性は、このレビューではなく導入側の運用確認事項です。
どんな人向けか、どんな人は避けるべきか
nodriver が合うのは、軽量で async、CDP ネイティブな Chromium ドライバーが欲しく、ブラウザ本体、更新、ランタイムを自分で持てる人です。依存ツリーが小さいので監査しやすく、CDP ファーストの設計はプロトコルレベルの制御に向いています。コンテナ向けかどうかは未検証です。このレビューでは Linux イメージ、ブラウザのインストール、共有ライブラリ、サンドボックス、プロセスの後始末は試していません。
次の 2 つの層は、別の場所を見たほうがいいです。Python 3.14 を使っていて、インタープリタ固定や vendor ファイルのパッチを受け入れたくないなら、修正リリースを待つべきです。今の import ブロックは、今日の時点では完全な停止要因です。また、配信形態が AGPL-3.0 に合わないなら、たとえば private な修正を含むホスト型サービスを作る場合などは、これ一本で permissive ライセンスの代替を先に検討する理由になります。これはコードへの否定ではありません。どちらも、コンプライアンスレビューで初めて知るより、今知っておくほうがいい制約です。
さらに、あなたが本当に欲しいのが データ であって、手で操作するブラウザではないなら、nodriver は見送りでよいでしょう。nodriver が渡すのは script で操作できるタブと 62 個のメソッドであり、描画済みページをきれいで構造化されたレコードに変える作業は、まだあなた自身のコードです。それは別の仕事であり、そこに managed API の出番があります。
代替案と Thunderbit の位置づけ
まず正直な前提から。nodriver は無料で、AGPL で、self-hosted です。ブラウザを自分で動かし、更新を自分で管理し、ランタイムとその中の障害責任も自分で持ちます。まさにその制御を求める開発者にとっては、既に持っているライブラリより安い managed service はありません。
オープンソース内では、ロゴではなく仕事で比較してください。本物のブラウザドライバーを比較するなら、Playwright と Puppeteer の比較 が、形として nodriver の並ぶ 2 大ツールを整理しています。stealth 志向という理由で探しているなら、Python 圏で最も近いのは Scrapling のレビュー です。生のブラウザ制御より LLM 向け出力が欲しいなら、Crawl4AI のレビュー はページをレンダリングして Markdown で返してくれますし、Scrapy のレビュー は browserless の大規模クロールで今も基準的存在です。これらをまとめて比較するなら、オープンソーススクレイパー総覧 がカテゴリごとに並べてくれます。
開示: この記事は Thunderbit が公開しています。Thunderbit は managed extraction service なので、比較軸はアーキテクチャではなくタスクです。nodriver は自分でホストしてプログラムするブラウザ制御層を提供します。Thunderbit はレンダリングを処理し、ページ内容や schema 形のレコードをサービスとして返します。プロトコルレベルのブラウザ制御と self-hosting が必須なら nodriver を使ってください。ブラウザを所有することより、出力レコードと運用の引き渡しが重要なら managed extractor を検討してください。変更可能なエンドポイント、クレジット、バッチ上限の詳細は、ライブラリのベンチマークではなく pricing page に置くべきです。
トレードオフは、どこで仕事が発生するかです。nodriver はブラウザ管理とランタイム保守をあなた側に残しつつ、リクエストごとのサービス料は取りません。managed API はその層を肩代わりし、呼び出しに対して課金します。
結論
小さくて、async で、CDP ネイティブな Chromium ドライバーが欲しく、しかも自分の Python バージョンで動作確認済みなら、nodriver を使う価値があります(この環境では 3.12.13 で clean import を確認しました)。AGPL-3.0 をどう配布するかも確認しておいてください。解決されたランタイムグラフは 4 パッケージ追加で、測定環境は約 17 MB、ライブラリは 57 の CDP ドメインモジュールと 62 メソッドの Tab 面、そして native XPath を備えています。このフィクスチャでは、既定で 3 種類中 2 つ、待機ありで 3 つ中 3 つを返し、stock Playwright と Puppeteer と同じでした。
ただし、注意点は過小評価しないでください。Python 3.14 では、1 つの非 UTF-8 バイトを直すまで import できません。これは既知で未解決の問題であり、謎ではありませんが、引っかかった当日には完全停止です。ライセンスは AGPL-3.0 なので、修正済みコピーをサービスとして運用するなら本当に判断が必要です。小さいインストールは小さい import を意味せず、57 の CDP モジュールが最初に全部読み込まれるため、select() の 0.5 秒ポーリングは短い待機を 1 秒前後に膨らませます。既定の公開情報について実際に答えられた範囲では、宣伝ほど広くはありません。stock Puppeteer と違うのは boolean 1 つだけで、headless の user-agent は依然として HeadlessChrome のまま、ほかに測定したすべての項目は 4 つのスタックで同一でした。そして、多くの人が nodriver を見つける理由でもある anti-detection の本筋は、意図的にテストしていません。ライブラリを調べ、自分のマシン上のページで動かしただけで、ライブ防御と戦わせてはいません。根拠のない bypass 断言を渡すより、そのことを明確に言うほうが誠実だと思います。答えられた範囲では、nodriver はよくできた、かなり軽量なドライバーです。ただし、Python バージョンの壁と copyleft ライセンスという 2 つの鋭い端があるので、そこは先に見えている必要があります。
Web データ抽出に Thunderbit を試す Get Started Free
FAQ
なぜ Python 3.14 で import nodriver が失敗するのですか?
cdp/network.py に、ソースの文字コード宣言なしで非 UTF-8 の ± バイトが含まれているからです。Python 3.14.2 はこのファイルを拒否し、transitive import を止めます。Python 3.12.13 では、バイト列が同一のこのファイルを未修正版のまま import できます。このレビューでは正確なインタープリタ側変更は特定しておらず、Python 3.13 も試していません。上流の情報は nodriver issue #35 と pull request #36 です。検証済みのバージョンを使うか、ファイルを UTF-8 に再エンコードしてください。
「CDP ネイティブ、webdriver なし」で何が得られて、インストールコストは何ですか?
nodriver は 57 の DevTools Protocol ドメインモジュールを同梱し、Selenium 経由で chromedriver を呼ぶのではなく、WebSocket で CDP を直接話します。Tab には native XPath を含む 62 の method があります。パッケージメタデータが要求するランタイム要件は 3 つ(websockets、mss、deprecated)で、解決すると wrapt が追加されます。測定環境では、pip を含む 6 個の dist-info で約 17.2 MB でした。numpy、lxml、ダウンロード済みブラウザバイナリはありません。注意点は 2 つです。57 の CDP モジュールを eager に読み込むため、修正版コピーでも import は約 158 ms かかります。また、Chrome 系のブラウザは別途必要です。
AGPL-3.0 は自分のプロジェクトに関係ありますか? 配布形態次第です。AGPL-3.0 は network copyleft ライセンスです。もし修正した nodriver をサービスとして公開し、他人が使える形で運用するなら、その修正版ソースを提供する義務が生じます。個人用スクリプトや、外部公開しない社内ツールなら、ほぼ問題になりません。修正済み nodriver を基盤にしたホスト型商用サービスなら、コンプライアンスの責任者に必ず確認すべき論点です。Apache-2.0 や MIT の同種ツールより厳格です。
nodriver は自分自身について何を見せますか? それは Cloudflare に勝てるという意味ですか?
前半は測定済み、後半は未測定です。この違いが重要です。127.0.0.1 で配信したページに、control と同じ Chrome ビルドを向けたところ、navigator.webdriver は false でした。stock Playwright と stock Puppeteer はどちらも true です。この値はプロパティをパッチするのではなく、browser launch 時点で設定されています。descriptor は依然として Chrome 本来の native getter のままです。それ以外は、ほぼすべて control と同じでした。platform 文字列、5 つの plugin、12 コア、16 GB の device memory、window.chrome の形、Permissions API の矛盾のなさ、document や window に cdc_ 風の残骸がないことなどです。headless の user-agent は HeadlessChrome/151.0.0.0 のままで、control と同じです。これはあなたの代わりに隠してはくれません。これらから分からないのは、実際のアンチボットサービスに対して有効かどうかです。私は nodriver をライブサイトに向けていませんし、アンチボットサービスにも接触していません。CAPTCHA も触っていません。それは設計上のスコープ外でした。上の公開情報テーブルが示すのは、スタックが何を名乗るかだけです。相手が誰で、どう反応するかは示しません。
nodriver は JavaScript で描画されたコンテンツを正しく扱えますか?
はい。ただし、読むタイミングの問題はあります。3 種類のコンテンツクラスを持つフィクスチャで、既定の browser.get() + tab.get_content() は 3 つ中 2 つを返しました。JavaScript は正しく実行されています(同期注入の class は配信バイト列のどこにもなく、それでも取得できました)が、読み取りは load イベント時点なので、その後に注入されたものは見逃します。tab.select("#delayed-injected") を足すと 3 つ中 3 つになりました。これは同じページでの stock Playwright と stock Puppeteer と同じです。1 つだけ癖を見込んでください。select() は 0.5 秒バックオフで polling するので、100 ms の待機は約 1.1 秒になります。


