סקירת nodriver: דרייבר CDP קטן שלא מצליח להיטען ב-Python 3.14

עודכן לאחרונה ב-August 18, 2026
סקירת nodriver: דרייבר CDP קטן שלא מצליח להיטען ב-Python 3.14
סיכום AI
nodriver היא ספריית אוטומציית דפדפן ב-Python מבית ultrafunkamsterdam, היוצר של undetected-chromedriver, והיא מוצגת כיורשת של הפרויקט הזה. ההבדל האדריכלי שלה הוא במיוחד מול Selenium ו-chromedriver: nodriver מדברת עם Chromium דרך Chrome DevTools Protocol ‏(CDP), בלי בינארי של WebDriver באמצע, והממשק שלה אסינכרוני. Playwright ו-Puppeteer שייכים לקבוצת השוואה אחרת. גם הן בקרות דפדפן מבוססות פרוטוקול, לא צאצאיות של WebDriver; ההבדלים המעשיים ביניהן הם בעיצוב ה-API, באריזה, באספקת הדפדפן ובמדיניות התאימות. הסקירה הזו בוחנת את nodriver ‏0.50.3 במקום לבדוק אותה מול הגנות חיות.

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() בשלב האינטראוספקציה הזה. בדיקות הדפדפן שלמטה נעשו בריצה נפרדת.

System diagram: Direct CDP Control Path

הספרייה כן כוללת API שמכוון ל-anti-detection — מתודות שקיומן אומת, אבל לא בחנתי את ההתנהגות שלהן מול יעד כלשהו. אני מציין זאת פעם אחת וממשיך הלאה, כי ההשפעה מול שירות אמיתי היא בדיוק הדבר שבחרתי לא לבדוק. בניסוח נייטרלי: השמות ב-nodriver מאופקים יותר מאשר אצל חלק מהמתחרות הממותגות כ-stealth. הסיפור שלה הוא ארכיטקטוני — CDP-native, פרופיל חדש לכל ריצה — ולא קיר של שמות מתודות בסגנון detect_and_bypass. קחו זאת כהערה על עיצוב API, לא כטענה על תוצאות.

כל הספירות האלו הגיעו מ-import של החבילה ומשימוש ב-introspection של Python עצמו — inspect, הליכה על מודולים, וספירת מאפיינים. לא נגעתי באף אתר. אם תרצו להריץ שוב, המספרים מגיעים ישירות מהחבילה; הם לא תחושות בטן.

מה היא חושפת על עצמה

Measured results chart: Browser disclosure fields in the tested stacks

הנה שאלה שאפשר לענות עליה בלי להתקרב בכלל להגנה אמיתית: כש-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, שלוש ריצות כל אחת. כל ערך למטה נשאר זהה בכל שלוש הריצות.

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

ההבדל הבולט ביותר הוא אותו בוליאני אחד. לפי הגדרות ההפעלה המחדליות הללו, 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:

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

אותו קלישאה ותיקה שבה 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 לא יכולה לראות אותו.

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

nodriver נוחתת בדיוק במקום שבו נוחתות הגדולות. 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 delay0 ms100 ms400 ms800 ms1500 ms
nodriver select()124–152 ms1132–11411128–11292132–21772138–2150
Puppeteer waitForSelector113–129 ms203–216512–516911–9191608–1611

בתבנית הזו, הזרקה של 100 מ״ש הובילה לכ-1.1 שניות עבור select(). הלולאה המותקנת מבצעת await self ואז await self.sleep(0.5) אחרי פספוס; המחזור המשולב שנמדד כאן הגיע בערך לשנייה אחת, אבל await self לא הוגדר כאן כשינה קבועה אוניברסלית. כל צומת מושהה שנבדק נמצא בסוף. ה-waiter של Puppeteer עקב אחרי הדיליים האלה מקרוב יותר. הרבה waits סידוריים יכולים לצבור את הפער, אף שהסקירה הזו לא ביצעה benchmark לדף ייצור עם שלושים selectors.

גם ה-launch הוא מקום שבו המסגור האסינכרוני והקל פוגש את המציאות. העלאת הדפדפן הציבה את nodriver בשכונה של Botasaurus ו-Puppeteer, והרבה מאחורי Playwright:

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

אחרי שהכול רץ, ה-navigate-and-read של nodriver הוא המהיר ביותר מבין הארבעה, עם 119–129 מ״ש. קלות בהגה, רגילה בהנעה.

התקנה וטביעת רגל: החלק הטוב באמת

כאן nodriver מרוויחה את התווית “ממוקדת”, וזה נתון התקנה נעים ומאומת, לא משהו שקשור לסקרייפינג. כך נפתר pip install nodriver נקי:

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

עבור קטגוריה שבדרך כלל סוחבת איתה 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 שיש לכם מותקנת היא זו שהטראפיק שלכם יכריז עליה.

הרישיון הוא החלטת האימוץ האמיתית

System diagram: The license is the real adoption decision

רוב הסקירות על כלי חינמי מתייחסות ל“זה open source” כאל סוף השיחה על רישוי. אצל nodriver זה רק ההתחלה, כי הרישיון הוא AGPL-3.0 — מאומת גם ב-LICENSE.txt של ה-wheel וגם ב-spdx_id של המאגר. זה רישיון copyleft חזק ברשת, וזה מחויבות שונה מהותית ממה שהשכנים מבקשים.

העמיתים שרוב האנשים משווים אליהם את nodriver הם רישיונות permissive, ודווקא לכן הניגוד חד:

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

ההיקף חשוב. סעיף 13 של AGPL עוסק בגרסה ששונתה של התוכנה המכוסה כשהיא משמשת לאינטראקציה מרוחקת דרך הרשת. הסקירה הזו לא מכריעה האם קוד שירות נלווה ומופרד נכלל בעבודה המכוסה, וגם לא פותרת מקרי קצה של שימוש פנימי או גבולות תאגידיים. אם מוצר מתארח משנה את nodriver, יש לבדוק את טקסט הרישיון ואת הארכיטקטורה עם ייעוץ משפטי. זה דגל אימוץ טכני, לא ייעוץ משפטי.

אני לא אומר כאן אם AGPL טוב או רע — copyleft היא בחירה לגיטימית והרבה פרויקטים רציניים משתמשים בה. אני כן מדגיש ש“nodriver היא חופשית ופתוחה” הוא משפט נכון אך לא שלם. החובה אמיתית, היא שונה מהברירת המחדל הפרמיסיבית בפינה הזו של האקוסיסטם, והיא צריכה להיכנס להחלטה ולא להיטמע לתוך “חינם”. (PyPI, למען הסר ספק, לא מציג בכלל classifier של רישיון; טקסט ה-AGPL נמצא ב-wheel ו-spdx_id נמצא במאגר, אז אל תסמכו על אינדקס החבילות שיציף לכם את זה.)

המטאדאטה, לפי תאריך

מספרים נקודתיים מהמאגר ומהחבילה, ישר מ-GitHub API ומ-PyPI:

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

אותות התחזוקה הניתנים לצפייה הם מעורבים: המאגר נדחף במאי 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 שניות.

Ke
Ke
CTO ב-Thunderbit | מדען נתונים בכיר ומומחה ב-ML עם כמעט עשור של ניסיון בלמידת מכונה ובמדע הנתונים, קה שן הוא בוגר אוניברסיטת קולומביה ולשעבר מדען נתונים בכיר ב-Walmart Labs. עם מומחיות עמוקה ומוכרת בקרב עמיתים ב-Python, R, Java וסטטיסטיקה, הוא משתף תובנות מוכחות-בקרב על המעבר מאלגוריתמי AI מורכבים מתיאוריה לארכיטקטורה ברמת production.
תוכן עניינים
Thunderbit · סוכן נתוני אינטרנט מבוסס AI

חלץ נתונים מכל דף בתוך קליק אחד

למעלה מ-250,000 משתמשים סומכים עליו
יש תוכנית חינמית
מדף אינטרנט לגיליון נתונים
תארו מה אתם צריכים — סוכן ה-AI של Thunderbit מחלץ את זה ומייצא ל-Excel, Google Sheets, Airtable או Notion. חינמי להתחלה.
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week