nodriver היא ספריית אוטומציית דפדפן ב-Python מבית ultrafunkamsterdam, היוצר של undetected-chromedriver, והיא מוצגת כיורשת של הפרויקט הזה. ההבדל הארכיטקטוני שלה בולט במיוחד מול Selenium ו-chromedriver: nodriver מדברת עם Chromium דרך Chrome DevTools Protocol (CDP), בלי בינארי של WebDriver באמצע, והממשק שלה אסינכרוני. Playwright ו-Puppeteer שייכים לקבוצת השוואה אחרת. גם הן שליטה בדפדפן מבוססת פרוטוקול, לא צאצאיות של WebDriver; ההבדלים המעשיים ביניהן הם בעיצוב ה-API, באריזה, באספקת הדפדפן ובמדיניות התאימות.
סקירת nodriver הזו בוחנת את גרסה 0.50.3 בלי לבדוק אותה מול הגנות חיות. מדדתי את החבילה המותקנת, את ה-imports, את שטח ה-API, את טביעת הרגל בדיסק ואת הרישיון, ואז הפעלתי דפדפנים רק מול דפים שהוגשו מ-127.0.0.1. לא היה כאן יעד חי, שירות anti-bot או CAPTCHA. התוצאות מתארות את התנהגות החבילה ואת החשיפה של הדפדפן כברירת מחדל; הן לא מוכיחות יעילות נגד זיהוי.
שלושה דברים בלטו בתוך המסגרת הזו. ראשית, ב-Python 3.14 הספרייה לא נטענת בכלל — בית קצה אחד שגוי מספיק כדי להפיל את כל החבילה עוד לפני שמגיעים לקרוא לפונקציה כלשהי. שנית, עבור דרייבר מסוג כזה היא קטנה בצורה מרשימה: שלוש תלויות ישירות מוצהרות ועוד חבילה טרנזיטיבית אחת שנפתרה, ובסביבות 17 MB בסביבה שנמדדה. שלישית, בדף שבניתי, המאפיין היחיד שבו nodriver שונה באופן גלוי מ-Playwright ו-Puppeteer רגילים הוא בוליאני אחד — ומחרוזת ה-user-agent במצב headless עדיין אומרת HeadlessChrome בשלושתן. שאר ההפתעות נמצאות ברישיון.
שבירת ה-import ב-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, כך שכשל ה-parsing מפיל את ה-import. סריקה של החבילה לא מצאה אף קובץ מקור נוסף שאינו UTF-8.
צריך לדייק מאוד בגבול הגרסה. Python 3.14.2 דוחה את הקובץ, בעוד Python 3.12.13 מייבאת אותו ללא תיקון. קבצי network.py המותקנים זהים ברמת הבייטים בשתי הסביבות (SHA-256 ef755f41800d4efb593736f8b55b331bba68eec373ed62ece433e66e4b491cd6), כך שהבדל במקור עצמו לא מסביר את התוצאה. הסקירה הזו לא בידדה את שינוי הטוקנייזר המדויק ב-CPython שאחראי לכך, ו-Python 3.13 לא נבדק. לכן היא מדווחת על שתי תוצאות המדידה בלבד, ולא טוענת שכל Python מוקדם מתנהג כמו 3.12.
מכאן לא נובעת מסקנה על Python 3.13.
זו שחזור של הבאג, לא גילוי חדש. אותו traceback של Python 3.14 מופיע ב-issue #35 של nodriver, ופתרון מוצע נמצא ב-pull request #36. גרסה 0.50.3 עדיין מכילה את בית הקצה הזה. במטאדאטה של PyPI מופיעים classifiers עד Python 3.13, ואין הצהרה על תמיכה ב-3.14.
הפתרון עוקף קצר כמו הבאג עצמו: להשתמש בגרסת interpreter שנבדקה, או לקודד מחדש את הקובץ הזה ל-UTF-8, כפי שמציע pull request #36. אחרי הקידוד מחדש, החבילה נטענת וניתנת לבדיקה ב-3.14 בלי שחוסם נוסף יופיע. הריצה הנקייה על 3.12.13 למטה מאמתת נתיב אחד ללא תיקון; Python 3.13 לא נבדק בסקירה הזו.
ב-Python 3.12.13, import nodriver מצליח בלי שום תיקון. כל מספרי הדפדפן בהמשך הסקירה מגיעים מההתקנה הזו. מדדי טביעת הרגל וזמני ה-import משתמשים בעותק שתוקן בבייט אחד ב-3.14, ומסומנים בהתאם. Python 3.13 עדיין לא נבדק כאן; תוצאה אחת על 3.12 לא מכסה את כל הטווח המוקדם.
אם אתם עובדים כברירת מחדל עם interpreters חדשים — ודי הרבה צוותים רצים מהר על כל Python חדש — זו חסימה אמיתית, גם אם קלה לתיקון. טוב לדעת על זה מראש כדי לא לבזבז חצי יום על SyntaxError בקובץ שמעולם לא כתבתם.
מה nodriver באמת, מתחת למכסה המנוע
התיאור הקצר (“CDP-native, no webdriver”) נשמע כמו שיווק עד שרואים מה נארז ב-wheel. nodriver כוללת בעצמה סט מלא של bindings ל-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 וחיפוש טקסט על אותו אובייקט זה נוח באמת; ספריות אחרות לפעמים מכריחות לרדת ל-evaluate() בשביל XPath. הבנאי 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 שמכוון ל-anti-detection — מתודות שקיומן אומת, אבל לא בחנתי את ההתנהגות שלהן מול יעד כלשהו. אני מציין זאת פעם אחת וממשיך הלאה, כי ההשפעה מול שירות אמיתי היא בדיוק הדבר שבחרתי לא לבדוק. בניסוח נייטרלי: השמות ב-nodriver מאופקים יותר מאשר אצל חלק מהמתחרות הממותגות כ-stealth. הסיפור שלה הוא ארכיטקטוני — CDP-native, פרופיל חדש לכל ריצה — ולא קיר של שמות מתודות בסגנון detect_and_bypass. קחו זאת כהערה על עיצוב API, לא כטענה על תוצאות.
כל הספירות האלו הגיעו מ-import של החבילה ומשימוש ב-introspection של Python עצמו — inspect, הליכה על מודולים, וספירת מאפיינים. לא נגעתי באף אתר. אם תרצו להריץ שוב, המספרים מגיעים ישירות מהחבילה; הם לא תחושות בטן.
מה היא חושפת על עצמה

הנה שאלה שאפשר לענות עליה בלי להתקרב בכלל להגנה אמיתית: כש-nodriver מפעילה דפדפן, מה אותו דפדפן מספר על עצמו לדף שהוא מביט בו? בניתי דף שקורא את הדברים הברורים — navigator.webdriver, ה-user-agent, הפלטפורמה, השפות, מספרי plugins ויכולות חומרה, הצורה של window.chrome, מה אומרת Permissions API, וגיאומטריית החלון והמסך — הגשתי אותו מ-127.0.0.1, והפניתי אליו ארבעה סטאקים: nodriver, Botasaurus, ו-Playwright ו-Puppeteer רגילים כ-controls. כל הארבעה הפעילו את אותו build של Chrome (Chrome for Testing 151.0.7922.10), כך שכל הבדל מקורו בספרייה ולא בדפדפן. headless ו-headed, שלוש ריצות כל אחת. כל ערך למטה נשאר זהה בכל שלוש הריצות.
| Stack | Mode | navigator.webdriver | User-agent token | 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. הבדיקה מציינת הבדל ברמת התצורה; היא לא מוכיחה שהערך הזה נובע רק מהיעדר binary של WebDriver.
בכל ארבעת הסטאקים המאפיין הוא עדיין ה-getter המקורי של הדפדפן על Navigator.prototype — function get webdriver() { [native code] } — ולא property על המופע עצמו ולא פונקציה מוחלפת. JavaScript של הדף לא שיכתב את המאפיין אחרי הטעינה. הבדיקה לא מיפתה את דגלי ההפעלה או את נתיב המקור שאחראים לערך השונה.
הפרט השני הוא זה שמסייג את השיווק. במצב headless, ה-user-agent של nodriver עדיין מכריז HeadlessChrome/151.0.0.0 — בדיוק כמו Puppeteer רגיל, ובדיוק כמו Playwright רגיל. במצב headed הוא הופך ל-Chrome/151.0.0.0, שוב באותו אופן. אם הנחתם שספריית anti-detection מסווה כברירת מחדל את מחרוזת הזיהוי המפורסמת ביותר באוטומציית דפדפנים, זה לא קורה. את זה תצטרכו להגדיר בעצמכם.
כמעט כל השאר היה זהה בין ארבעת הסטאקים, וחשוב לומר זאת במפורש כי זה מצמצם את התמונה — כל מאפיין למטה נקרא אותו דבר ב-nodriver, Botasaurus, Playwright ו-Puppeteer:
| Property | Value, identical on all four stacks |
|---|---|
platform | MacIntel |
vendor | Google Inc. |
| Plugins | five |
| MIME types | two |
pdfViewerEnabled | true |
| Logical cores | twelve |
| Reported device memory | 16 GB |
| Touch points | zero |
window.chrome | present, with app/csi/loadTimes and no runtime |
| WebGL renderer string | identical across all four |
אותו קלישאה ותיקה שבה Permissions API ו-Notification.permission סותרים זה את זה לא הופיעה כאן — כל ארבעת הסטאקים דיווחו default ו-prompt בהתאם. סרקתי גם את document ואת window אחר שאריות בסגנון cdc_ שהיו מוכרות בסטאקים ישנים של WebDriver: ריק בכל הארבעה.
המקום היחיד שבו nodriver נראית יותר כמו דפדפן אוטומטי חשוף מאשר כמו שליטה מחושבת הוא גיאומטריית החלון. ב-headless, nodriver מדווחת outerWidth/outerHeight של 0×0 על מסך 800×600; Playwright ב-headless מדווחת 1280×720 כי היא מגדירה viewport עבורך. Puppeteer נמצאת יחד עם nodriver ב-0×0. זה הבדל של ברירת מחדל, לא של יכולת, והוא שלך לשנות.
עוד דבר שכדאי לדעת לפני פריסה: nodriver לא מגיעה עם דפדפן, אז out of the box היא משתמשת בכל Chrome שכבר מותקן על המכונה. אצלי זה אומר שהיא זיהתה אוטומטית את /Applications/Google Chrome.app — Chrome 150.0.7871.187 — וה-user-agent שנחשף היה של אותה גרסה, לא של גרסה נעולה מראש. גרסת הדפדפן שנחשפת אצלכם היא פשוט מה שמותקן בפועל אצלכם.
צריך לקרוא לזה כמו שזה ולא יותר. זו רשומה של מה שסטאק אוטומטי חושף כשאף אחד לא ביקש ממנו להסתיר — שימושית אם אתם בצד המגן, שימושית גם אם אתם רוצים לדעת מה הכלי שלכם משדר. זו לא מדידה של האם זה חשוב לשירות מסוים. לא בדקתי את זה, ואף שורה למעלה לא אמורה להתפרש כהוכחת תוצאה.
האם היא באמת מצליחה להוציא תוכן מדף?
להכריז על עצמך זה דבר אחד; להחזיר את ה-HTML הנכון זו כבר העבודה. הרצתי את nodriver מול אותה תבנית של שלוש קטגוריות תוכן שבה משתמש שאר ה-benchmark הזה, כדי שהמספרים יהיו באותו קנה מידה כמו כל הכלים האחרים שנמדדו כאן. הדף מכיל שלושה דברים: A, קישור סטטי שהסמן שלו הוא ליטרל בבייטים המוגשים; B, צומת שנבנה על ידי inline script במהלך ה-parse, כשהסמן וה-URL שלו מורכבים משברים כך שרק הרצת ה-JavaScript מגלה אותם; ו-C, צומת שמוזרק 800 מ״ש אחרי אירוע ה-load, ונבנה באותו אופן. Class C הוא האדברסרי — קריאה בזמן ה-load לא יכולה לראות אותו.
| Stack | Default read | With an explicit wait |
|---|---|---|
| nodriver 0.50.3 | 2 of 3 (A + B, misses C) | 3 of 3 |
| Botasaurus 4.0.92 | 2 of 3 | 3 of 3 |
| Playwright 1.56.0 | 2 of 3 | 3 of 3 |
| Puppeteer 24.16.0 | 2 of 3 | 3 of 3 |
nodriver נוחתת בדיוק במקום שבו נוחתות הגדולות. browser.get() ואחריו tab.get_content() הוא snapshot בזמן ה-load: הוא מרנדר JavaScript כמו שצריך — class B מוכיח זאת, כי class B לא קיים בשום מקום בבייטים המוגשים — אבל הוא מפספס כל דבר שמוזרק אחרי ה-load. הוסיפו tab.select("#delayed-injected") וקיבלתם את שלושתם. אותה מלכודת, אותו תיקון, כמו ב-Playwright ו-Puppeteer. יציב לאורך שלוש חזרות ושלוש ריצות נפרדות של כל הסוויטה, בלי flakiness.
סרקתי את דיליי ההזרקה כדי למצוא איפה הקריאה המחדלית מוותרת. nodriver מפסיקה לראות את class C ברגע שההזרקה מגיעה 100 מ״ש או יותר אחרי ה-load — אותו גבול כמו בשני ה-controls הרגילים. (Botasaurus היא החריגה כאן, וזו ההבדלה הממשית המעניינת בין שתי ספריות ה-anti-detect: ה-get() שלה ממתין כברירת מחדל לטעינה מלאה של הדף, ולכן הקריאה המחדלית שלה עדיין תופסת הזרקה ב-300 מ״ש. המחיר הוא בערך 250 מ״ש לכל ניווט.)
ל-hwait עצמו יש תכונה ששווה לתקצב. בסריקה הזו, tab.select() של nodriver נחתה בקפיצות פולינג גסות במקום לעקוב מקרוב אחרי דיליי ההזרקה:
| Class-C delay | 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 לא הוגדר כאן כשינה קבועה אוניברסלית. כל צומת מושהה שנבדק נמצא בסוף. ה-waiter של Puppeteer עקב אחרי הדיליים האלה מקרוב יותר. הרבה waits סידוריים יכולים לצבור את הפער, אף שהסקירה הזו לא ביצעה benchmark לדף ייצור עם שלושים selectors.
גם ה-launch הוא מקום שבו המסגור האסינכרוני והקל פוגש את המציאות. העלאת הדפדפן הציבה את nodriver בשכונה של Botasaurus ו-Puppeteer, והרבה מאחורי Playwright:
| Stack | Browser launch, across runs |
|---|---|
| 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 של nodriver הוא המהיר ביותר מבין הארבעה, עם 119–129 מ״ש. קלות בהגה, רגילה בהנעה.
התקנה וטביעת רגל: החלק הטוב באמת
כאן nodriver מרוויחה את התווית “ממוקדת”, וזה נתון התקנה נעים ומאומת, לא משהו שקשור לסקרייפינג. כך נפתר pip install nodriver נקי:
| Install fact | nodriver 0.50.3 |
|---|---|
| Declared direct runtime dependencies | three — websockets, mss, deprecated |
| Resolved transitive dependency | wrapt (via deprecated) |
Measured site-packages total | about 17.2 MB across 6 dist-info directories, including the environment's pip |
| nodriver's own share of that | 3.7 MB |
| numpy / lxml | neither |
| Browser binary at install time | none pulled down |
עבור קטגוריה שבדרך כלל סוחבת איתה stack של rendering, זו גישה קלה.
ההבחנה בין חבילות ישירות לחבילות טרנזיטיביות חשובה לתחזוקה. המטאדאטה של nodriver מבקשת mss, websockets ו-deprecated; wrapt מגיעה כי deprecated דורשת אותה. התיקייה השישית של dist-info בסביבה של 17.2 MB היא pip, שכבר הייתה חלק מה-virtual environment. לכן “17.2 MB על פני שש הפצות” מתאר את הסביבה שנמדדה, בעוד “ארבע חבילות ריצה נוספות” מתאר מה ההתקנה פתחה בפועל. אלו ספירות קשורות, לא זהות.
הניגוד שממחיש למה המספר הזה חשוב: על אותה מכונה, מסגרת האחות Botasaurus שוקלת 122.3 MB על פני 44 חבילות — בערך פי שבעה בטביעת הרגל בדיסק. זה ההבדל בין דרייבר CDP ממוקד לבין framework כולל-כול, וזה פועל לשני הכיוונים. nodriver נותנת לכם עץ תלות דק וקריא שאפשר באמת לבקר; Botasaurus נותנת יותר out of the box וגובה על זה בדיסק ובשטח תלות. אף אחת מהן לא “טובה” אבסולוטית — זה תלוי אם אתם רוצים דרייבר או framework — אבל אם חשוב לכם install קטן וקל לבדיקה, nodriver נקייה באופן חריג למה שהיא עושה.
טביעת רגל קטנה בדיסק לא אומרת import קטן. בעותק של Python 3.14 שתוקן בבייט אחד, import nodriver לקח בערך 158 מ״ש (חציון על פני imports טריים בתהליכי subprocess, בערך 151–199 מ״ש). ה-import הלא מתוקן ב-3.14 נכשל, כך שלא מדווח עליו זמן. כל 57 מודולי ה-CDP נטענים ברגע ה-import, והזיכרון התפוס אחרי ה-import היה 31.5–31.9 MB לפני שמופיע תהליך Chrome כלשהו. דפדפן שהושק היה מוסיף הרבה יותר, והוא לא נכלל במדידת הזיכרון הזו.
שני סייגים צמודים למספרים האלה. הם ממכונה אחת — macOS arm64 — והמדדים של footprint וזמני import נמדדו על Python 3.14 מול העותק שתוקן בבייט אחד, כי החבילה הלא מתוקנת פשוט לא נטענת ב-interpreter הזה. על Python נתמך לא צריך את התיקון: אישרתי import נקי ולא מתוקן ב-3.12.13, ומשם מגיעות כל מדידות הדפדפן. וגם ברגע הריצה עדיין נדרש בינארי אמיתי של Chrome, Chromium, Edge או Brave — nodriver מפעילה דפדפן קיים, היא לא מביאה אחד איתה, כך שעלות ההתקנה הזו נמצאת מחוץ ל-17 MB, וכפי שהראתה תחילת החשיפה, גרסת Chrome שיש לכם מותקנת היא זו שהטראפיק שלכם יכריז עליה.
הרישיון הוא החלטת האימוץ האמיתית

רוב הסקירות על כלי חינמי מתייחסות ל“זה open source” כאל סוף השיחה על רישוי. אצל nodriver זה רק ההתחלה, כי הרישיון הוא AGPL-3.0 — מאומת גם ב-LICENSE.txt של ה-wheel וגם ב-spdx_id של המאגר. זה רישיון copyleft חזק ברשת, וזה מחויבות שונה מהותית ממה שהשכנים מבקשים.
העמיתים שרוב האנשים משווים אליהם את nodriver הם רישיונות permissive, ודווקא לכן הניגוד חד:
| Tool | License | If you run a modified copy as a network service |
|---|---|---|
| nodriver | AGPL-3.0 | Section 13 can require an operator to offer the corresponding source of its modified version to remote users |
| Playwright | Apache-2.0 | no equivalent network-copyleft clause |
| Puppeteer | Apache-2.0 | as above |
| Botasaurus | MIT | as above |
ההיקף חשוב. סעיף 13 של AGPL עוסק בגרסה ששונתה של התוכנה המכוסה כשהיא משמשת לאינטראקציה מרוחקת דרך הרשת. הסקירה הזו לא מכריעה האם קוד שירות נלווה ומופרד נכלל בעבודה המכוסה, וגם לא פותרת מקרי קצה של שימוש פנימי או גבולות תאגידיים. אם מוצר מתארח משנה את nodriver, יש לבדוק את טקסט הרישיון ואת הארכיטקטורה עם ייעוץ משפטי. זה דגל אימוץ טכני, לא ייעוץ משפטי.
אני לא אומר כאן אם AGPL טוב או רע — copyleft היא בחירה לגיטימית והרבה פרויקטים רציניים משתמשים בה. אני כן מדגיש ש“nodriver היא חופשית ופתוחה” הוא משפט נכון אך לא שלם. החובה אמיתית, היא שונה מהברירת המחדל הפרמיסיבית בפינה הזו של האקוסיסטם, והיא צריכה להיכנס להחלטה ולא להיטמע לתוך “חינם”. (PyPI, למען הסר ספק, לא מציג בכלל classifier של רישיון; טקסט ה-AGPL נמצא ב-wheel ו-spdx_id נמצא במאגר, אז אל תסמכו על אינדקס החבילות שיציף לכם את זה.)
המטאדאטה, לפי תאריך
מספרים נקודתיים מהמאגר ומהחבילה, ישר מ-GitHub API ומ-PyPI:
| Fact | Value on July 14, 2026 |
|---|---|
| Stars | 4,511 |
| Forks | 422 |
| Open issues | 14 |
| Created | February 2024 |
| Last pushed | May 2026 |
| Latest PyPI release | 0.50.3 |
| Wheel | pure-Python py3-none-any |
requires-python | >=3.9 |
| Python classifiers | 3.7–3.13 |
אותות התחזוקה הניתנים לצפייה הם מעורבים: המאגר נדחף במאי 2026, בעוד שהחבילה האחרונה שנמדדה עדיין הכילה את בעיית ה-import ב-Python 3.14, ותיקון מוצע עדיין לא שוחרר בתאריך המחקר. כמות stars ומספר issues פתוחים לא מכריעים אם הקצב הזה מתאים לסף התחזוקה שלכם.
יתרונות וחסרונות
יתרונות:
- התקנה קטנה וברורה: שלוש תלויות ישירות מוצהרות ועוד
wraptטרנזיטיבית, כ-17.2 MB על פני שש תיקיותdist-infoבסביבה שנמדדה (אחת מהן היאpip), בלי numpy/lxml ובלי הורדת דפדפן בזמן ההתקנה. - CDP-native באמת: כוללת 57 מודולי bindings לפרוטוקול DevTools משלה ומדברת בפרוטוקול ישירות, בלי chromedriver/Selenium בינארי באמצע.
- שטח רחב של מציאת אלמנטים ב-
Tab(62 מתודות ציבוריות) עם XPath, CSS וחיפוש טקסט כיכולות מובנות — בלי לרדת ל-evaluate()גולמי בשביל XPath. - אסינכרונית מטבעה, פרופיל חדש לכל ריצה, ו-
Configנקי עבור הכיוונונים הנפוצים (headless, נתיב לבינארי, ארגומנטים, שפה, פורטים). - מושכת תוכן מדף שנבנה ב-JavaScript בצורה טובה כמו הגדולות: 2 מתוך 3 קטגוריות תוכן בקריאה מחדלית, ו-3 מתוך 3 עם wait מפורש — זהה ל-Playwright ול-Puppeteer הרגילות על אותה תבנית, ויציב לאורך שלוש ריצות. ה-navigate-and-read היה המהיר מבין הארבעה, עם 119–129 מ״ש.
- מדווחת
navigator.webdriverכ-falseכברירת מחדל במקוםtrueכמו ה-controls הרגילים, בלי לשכתב את המאפיין — ה-descriptor נשאר ה-getter המקורי של הדפדפן. - זהות ארכיטקטונית ברורה כיורשת CDP-native של undetected-chromedriver.
חסרונות:
- לא נטענת ב-Python 3.14 out of the box — קובץ אחד לא UTF-8 (
cdp/network.py) זורקSyntaxErrorבזמן import. שוחזר מול issue #35 הפתוח, ועדיין לא תוקן ב-0.50.3. נעלו ל-≤3.13 (נבדק נקי ב-3.12.13) או קודדו מחדש את הקובץ. - AGPL-3.0 היא שיקול אמיתי לכל מי שמריץ עותק שונה כשירות רשת — מחמירה יותר מהעמיתים Apache/MIT.
- טביעת הרגל הקטנה בדיסק לא אומרת import קטן: כ-158 מ״ש cold-start וכ-31.5 MB resident לפני ש-Chrome כלשהו קיים, כי כל 57 מודולי ה-CDP נטענים מראש.
tab.select()עושה polling עם back-off של חצי שנייה, כך שהמתנות קצרות מעוגלות למעלה — המתנה של 100 מ״ש עולה בערך 1.1 שניות, בעוד ה-waiter של Puppeteer עולה בערך 210 מ״ש. זה נכון בכל פעם, אבל מצטבר מול הרבה waits קטנים.- ריצות headless עדיין מכריזות
HeadlessChromeב-user-agent כברירת מחדל, בדיוק כמו ה-controls הרגילים; שום דבר בתצורת ברירת המחדל לא מסתיר את מחרוזת הזיהוי הברורה ביותר. - עדיין צריך בינארי אמיתי של Chrome/Chromium/Edge/Brave בזמן הריצה; התקנת pip קלה היא רק חצי מהסיפור, וכל Chrome שמותקן אצלכם הוא זה שגרסתו נחשפת בטראפיק.
- היעילות מול כל מערכת anti-bot לא נבדקה כאן — כל הנחת ה-stealth לא אומתה בכוונה.
מה שלא בדקתי, ולכן אי אפשר להסיק עליו: זיכרון לכל tab, זמן round-trip ב-CDP, טיפול בפרופילים, throughput בקנה מידה, Linux או Windows, במיוחד Python 3.13, ו — העיקר — יעילות אמיתית נגד anti-bot בשירות חי כלשהו. כל הדפדפנים בסקירה הזו דיברו רק עם fixture ב-127.0.0.1. כל המספרים הם ממכונה אחת (macOS arm64); מספרי הפעלת הדפדפן הם ב-Python 3.12.13 בלי תיקון, ומספרי ה-footprint וזמני ה-import הישנים הם ב-Python 3.14 על העותק המתוקן בבייט אחד.
גם לא הרצתי מטריצת שדרוג דפדפנים, כך שתאימות מול גרסאות Chrome עתידיות נשארת בדיקת תפעול של המאמצים, לא תוצאה של הסקירה הזו.
למי זה מתאים, ולמי עדיף להימנע
nodriver מתאימה אם אתם רוצים דרייבר קל, אסינכרוני, CDP-native ל-Chromium חי, ומוכנים להחזיק בעצמכם את הדפדפן, העדכונים וה-runtime. עץ התלויות הקטן קל יותר לבקרה, והעיצוב שמבוסס CDP מתאים לשליטה ברמת הפרוטוקול. התאמה לקונטיינרים לא נבדקה: הסקירה הזו לא הפעילה images של Linux, התקנת דפדפן, ספריות משותפות, sandboxing או ניקוי תהליכים.
שתי קבוצות צריכות לחפש במקום אחר. אם אתם על Python 3.14 ולא מוכנים לנעול את ה-interpreter או לתקן קובץ vendored, חכו עד שהפתרון ישוחרר — שבירת ה-import היא חסימה קשה היום. ואם AGPL-3.0 היא בעיה עבור אופן ההפצה שלכם — שירות מתארח עם שינויים פרטיים — הרישיון לבדו הוא סיבה מספיק טובה לשקול חלופה ברישיון פרמיסיבי לפני שבונים על זה. אף אחת מהנקודות הללו אינה ביקורת על הקוד; שתיהן אילוצים שכדאי לגלות עכשיו ולא במהלך סקירת תאימות.
כדאי גם לדלג אם מה שאתם באמת צריכים הוא נתונים, לא דפדפן שמפעילים ידנית. nodriver נותנת לכם tab לתכנות ו-62 מתודות; להפוך דף מרונדר לרשומות נקיות ומובנות זה עדיין הקוד שלכם לכתוב. זו עבודה אחרת, ושם נכנס API מנוהל.
חלופות, ואיפה Thunderbit נכנסת
המסגור הכנה קודם כול: nodriver היא חינמית, AGPL ומאוחסנת אצלכם. אתם מפעילים את הדפדפן, מנהלים את העדכונים, ובעלי ה-runtime וכל כשל שבו. למפתח שרוצה בדיוק את רמת השליטה הזו, שום שירות מנוהל לא מתחרה במחיר עם ספרייה שכבר יש לכם.
בתוך open source, השוו לפי עבודה ולא לפי לוגו. אם אתם בוחנים דרייברים לדפדפן אמיתי, ההשוואה שלנו בין Playwright ל-Puppeteer מכסה את שתי הכבדות הברורות ש-nodriver עומדת לידן בצורתה. Scrapling היא השכנה הקרובה ביותר ב-Python על ציר ה-stealth אם זו הסיבה שלכם להסתכל. עבור פלט מוכן ל-LLM במקום שליטה גולמית בדפדפן, Crawl4AI מרנדרת דפים ומחזירה Markdown, ו-Scrapy נשארת מסגרת ה-reference לסריקות גדולות בלי דפדפן. אם אתם משווים כמה מהכלים האלה יחד, הסיכום על open-source scrapers מציב את הקטגוריות זו לצד זו.
גילוי נאות: המאמר הזה מתפרסם על ידי Thunderbit. Thunderbit היא שירות חילוץ מנוהל, כך שההשוואה כאן היא לפי משימה ולא לפי ארכיטקטורה. nodriver נותנת לכם שכבת שליטה בדפדפן שאתם מארחים ומחכנים; Thunderbit מטפלת ברינדור ומחזירה תוכן דף או רשומות במבנה schema כשירות. השתמשו ב-nodriver כששליטה ברמת פרוטוקול והפעלה עצמאית הן דרישות. שקלו מחלץ מנוהל כשהפלט וההעברה התפעולית חשובים יותר מבעלות על הדפדפן. פרטי endpoint, קרדיטים ומגבלות batch משתנים שייכים ל-דף התמחור, לא בתוך benchmark של ספרייה.
הפשרה היא איפה העבודה יושבת: nodriver משאירה אצלכם את ניהול הדפדפן ואת תחזוקת ה-runtime בלי עמלת שירות לכל בקשה; API מנוהל לוקח על עצמו את השכבה הזו וגובה על הקריאות.
נסו את Thunderbit לחילוץ נתוני רשת
מסקנה
בחרו ב-nodriver אם אתם רוצים דרייבר Chromium קטן, אסינכרוני ו-CDP-native, אם אימתתם שהוא עובד על גרסת Python שלכם (אצלי 3.12.13 נטענה נקי), ואם בדקתם את AGPL-3.0 מול אופן ההפצה שלכם. גרף ה-runtime שנפתר הוסיף ארבע חבילות, הסביבה שנמדדה תפסה כ-17 MB, והספרייה מביאה 57 מודולי CDP עם Tab של 62 מתודות ו-XPath מקורי. על התבנית הזו היא החזירה 2 מתוך 3 קטגוריות תוכן כברירת מחדל ו-3 מתוך 3 עם wait, בדיוק כמו Playwright ו-Puppeteer הרגילות.
אבל צריך לכייל את הסייגים בכנות. ב-Python 3.14 היא פשוט לא נטענת עד שתתקנו בית קצה אחד שאינו UTF-8 — באג מתועד שעדיין פתוח, לא תעלומה, אבל חסימה קשה ביום שתפגשו אותה. הרישיון הוא AGPL-3.0, וזה שיקול אמיתי לכל מי שמריץ עותק שונה כשירות, לא פורמליות. ההתקנה הקטנה לא קונה import קטן, כי כל מודולי ה-CDP נטענים מראש, ו-select() עם polling של חצי שנייה הופך המתנות קצרות לכאלה שעולות בערך שנייה כל אחת. ובשאלת החשיפה המחדלית שכן יכולתי לענות עליה, התמונה צרה יותר ממה שהשיווק מרמז: בוליאני אחד שונה מ-Puppeteer רגילה, ה-user-agent ב-headless עדיין אומר HeadlessChrome, וכל מאפיין אחר שמדדתי היה זהה בכל ארבעת הסטאקים. וכל הנחת ה-anti-detection — הסיבה שרבים בכלל מגיעים ל-nodriver — היא משהו שבחרתי במפורש לא לבדוק. מיפיתי את הספרייה והפעלתי אותה מול דף במחשב שלי במקום להציב אותה מול הגנות חיות, ואני מעדיף לומר זאת בפה מלא מאשר למסור לכם טענת bypass שאין לי איך לעמוד מאחוריה. בשאלות שכן יכולתי לענות עליהן, nodriver היא דרייבר בנוי היטב, קטן בצורה חריגה, עם שני קצוות חדים — חומת גרסת Python ורישיון copyleft — שכדאי לראות מגיעים.
נסו את Thunderbit לחילוץ נתוני רשת Get Started Free
שאלות נפוצות
למה import nodriver נכשל ב-Python 3.14?
כי cdp/network.py מכיל בית קצה ± שאינו UTF-8 בלי הצהרת קידוד מקור. Python 3.14.2 דוחה את הקובץ ומפסיקה את ה-import הטרנזיטיבי; Python 3.12.13 מייבאת את אותו קובץ, בייט-ל-בייט, בלי תיקון. הסקירה הזו לא בידדה את שינוי ה-interpreter המדויק ולא בדקה Python 3.13. המסלול ב-upstream הוא issue #35 של nodriver ו-pull request #36. השתמשו בגרסה שבדקתם, או קודדו מחדש את הקובץ ל-UTF-8.
מה נותן לכם “CDP-native, no webdriver”, ומה זה עולה בהתקנה?
nodriver כוללת 57 מודולים של תחומי DevTools Protocol ומדברת CDP דרך WebSocket במקום להפעיל chromedriver דרך Selenium. ה-Tab חושף 62 מתודות, כולל XPath מקורי. המטאדאטה של החבילה מגדירה שלוש דרישות ריצה (websockets, mss, deprecated); הפתרון מוסיף את wrapt. הסביבה שנמדדה השתמשה בכ-17.2 MB על פני שש תיקיות dist-info, כולל pip, בלי numpy, בלי lxml ובלי בינארי דפדפן שהורד. שני סייגים: ה-import של העותק המתוקן לקח בערך 158 מ״ש כי כל 57 מודולי ה-CDP נטענים מיד, ועדיין נדרש בנפרד דפדפן ממשפחת Chrome.
האם רישיון AGPL-3.0 חשוב לפרויקט שלי? זה תלוי איך אתם מפיצים. AGPL-3.0 הוא רישיון network-copyleft: אם אתם מריצים גרסה משונה של nodriver כשירות שאחרים משתמשים בו, אתם מחויבים להציע להם את קוד המקור של הגרסה המשונה הזו. עבור סקריפט אישי או כלי פנימי שלעולם לא נחשף, זו לא בעיה. עבור מוצר מסחרי מתארח שבנוי על nodriver מתוקנת, זו שאלה אמיתית שצריך להעלות מול מי שאחראי על התאימות — וזה מחמיר יותר מרישיונות Apache-2.0 ו-MIT של כלים דומים.
מה nodriver חושפת על עצמה — והאם זה אומר שהיא מנצחת את Cloudflare?
החצי הראשון נמדד; השני לא, וההבדל חשוב. בדף שהגשתי מ-127.0.0.1, כשהפעלתי את אותו build של Chrome כמו ה-controls: navigator.webdriver חוזר false, בעוד ש-Playwright ו-Puppeteer רגילות מחזירות true. הערך הזה נקבע בזמן ה-launch של הדפדפן, לא על ידי תיקון המאפיין — ה-descriptor עדיין הוא ה-getter המקורי של Chrome. מעבר לבוליאני הזה, כמעט הכול זהה ל-controls בדיוק: אותה מחרוזת platform, חמישה plugins, שנים־עשר cores, 16 GB זיכרון מכשיר מדווח, אותה צורת window.chrome, בלי סתירה של Permissions API, ובלי שאריות בסגנון cdc_ על document או window. ריצות headless עדיין מכריזות HeadlessChrome/151.0.0.0 ב-user-agent, בדיוק כמו שני ה-controls — זה לא מוסווה בשבילכם. מה שכל זה לא אומר הוא אם זה עובד מול שירות anti-bot אמיתי. לא כיוונתי את nodriver לאתר חי, לא יצרתי קשר עם שירות anti-bot ולא נגעתי ב-CAPTCHA; זה היה מחוץ למסגרת בכוונה. טבלת החשיפה למעלה אומרת מה הסטאק מכריז. היא לא אומרת מי מאזין, או מה הם עושים עם זה.
האם nodriver מטפלת נכון בתוכן שמרונדר ב-JavaScript?
כן, עם הסתייגות רגילה לגבי מתי קוראים. על תבנית עם שלוש קטגוריות תוכן, browser.get() + tab.get_content() מחדליים החזירו 2 מתוך 3 — הוא מבצע JavaScript כמו שצריך (הקטגוריה שמוזרקת בסינכרון לא קיימת בשום מקום בבייטים המוגשים ובכל זאת חזרה), אבל הוא קורא בזמן אירוע ה-load, ולכן מפספס כל מה שמוזרק אחר כך. הוספת tab.select("#delayed-injected") החזירה 3 מתוך 3. זה זהה ל-Playwright ול-Puppeteer הרגילות על אותו דף. קחו בחשבון תכונה אחת: select() עושה polling עם back-off של חצי שנייה, כך שהמתנה של 100 מ״ש עולה בערך 1.1 שניות.


