nodriver هي مكتبة بايثون لأتمتة المتصفح من ultrafunkamsterdam، وهو نفس المطوّر وراء undetected-chromedriver، وتقدّم نفسها بوصفها الامتداد الطبيعي لهذا المشروع. والفرق المعماري الأساسي هنا هو الابتعاد عن Selenium وchromedriver: إذ تتواصل nodriver مع Chromium عبر Chrome DevTools Protocol (CDP)، من دون أي ملف WebDriver وسيط، كما أن واجهتها تعتمد على النمط غير المتزامن. أما Playwright وPuppeteer فهما من فئة مختلفة في المقارنة؛ فهما أيضًا متحكمان بالمتصفح عبر البروتوكول، لكنهما ليسا امتدادًا لـ WebDriver، والفروق المفيدة بينهما تتعلق بتصميم الواجهة، وطريقة الحزمة، وتوفير المتصفح، وسياسة التوافق.
هذه المراجعة تتناول 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)، لذا لا يمكن تفسير النتيجة باختلاف في ملف المصدر نفسه. هذه المراجعة لم تعزل التغيير المحدد في tokenizer داخل CPython المسؤول عن ذلك، كما أن Python 3.13 لم تُختبر. لذا فهي تعرض نتيجتي الاستيراد المقاستين فقط، ولا تدّعي أن كل إصدار أقدم من 3.14 يتصرف مثل 3.12.
ولا يمكن استنتاج أي حكم بشأن Python 3.13 من هاتين النقطتين فقط.
هذه ليست “اكتشافًا” بقدر ما هي إعادة إنتاج. نفس traceback الخاص بـ Python 3.14 موثق في nodriver issue #35، مع اقتراح إصلاح في 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 من دون أي patch. وكل أرقام قيادة المتصفح اللاحقة في هذه المراجعة جاءت من ذلك التثبيت. أما أرقام الحجم وزمن الاستيراد فاستُخدمت فيها النسخة المعاد ترميزها إلى بايت واحد على 3.14، وهي موسومة على هذا الأساس. لا تزال Python 3.13 غير مختبرة هنا؛ فنتيجة 3.12 واحدة لا تعني تغطية كامل النطاق الأقدم.
إذا كنت تشغّل الإصدارات الحديثة افتراضيًا — وكثير من الفرق يسرع إلى اعتماد Python الجديدة — فهذه عقبة حقيقية، ولو كانت سهلة الإصلاح. من الأفضل أن تعرف بوجودها حتى لا تضيع نصف يوم بسبب SyntaxError في ملف لم تكتبه أصلًا.
ما هي nodriver فعلًا من الداخل
الوصف المختصر (“CDP-native، بلا webdriver”) يبدو أقرب إلى التسويق حتى تنظر إلى ما تحويه الحزمة. فـ nodriver تُضمّن معها مجموعة كاملة من bindings الخاصة ببروتوكول DevTools: إذ تحتوي حزمة nodriver.cdp على 57 وحدة خاصة بمجالات البروتوكول — مثل accessibility وdom وnetwork وpage وfetch وruntime وtarget وstorage وinput وemulation وغيرها. وهذا العدد هو الآلية التي تقف خلف ادعاء “CDP مباشر، بلا Selenium”. فبدلًا من تشغيل chromedriver التنفيذي الذي يتكلم WebDriver ويترجم الطلبات لك، تقوم nodriver بإنشاء كائنات بايثون لمجالات 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 موجّهًا نحو مقاومة الاكتشاف — وقد تأكدت من وجود بعض الطرق، لكنني لم أختبر سلوكها ضد أي هدف. أذكر ذلك مرة واحدة ثم أمضي، لأن أثرها على أي خدمة حقيقية هو بالضبط ما اخترت عدم اختباره. وما أستطيع قوله بشكل محايد: إن nodriver أكثر تحفظًا في التسمية من بعض المنافسين الذين يضعون stealth في الواجهة. قصتها هنا معمارية — CDP-native، وملف شخصي جديد مع كل تشغيل — لا قائمة طويلة من أسماء أساليب على نمط detect_and_bypass. اعتبر هذا ملاحظة عن تصميم الواجهة، لا حكمًا على النتائج.
هذه الأعداد جاءت من استيراد الحزمة واستخدام أدوات الفحص المدمجة في بايثون نفسها — inspect، والتنقل بين الوحدات، وعدّ الخصائص. لم يُمس أي موقع. وإذا أردت إعادة التجربة، فالأرقام تأتي مباشرة من الحزمة نفسها، لا من الانطباعات.
ماذا تعلن عن نفسها

هناك سؤال يمكنك الإجابة عنه من دون الاقتراب من أي نظام دفاع فعلي: عندما تقود nodriver المتصفح، ماذا يكشف هذا المتصفح عن نفسه للصفحة التي يعرضها؟ كتبت صفحة تقرأ الأشياء البديهية — navigator.webdriver، وسلسلة user-agent، والنظام الأساسي، واللغات، وعدد الإضافات والمكوّنات العتادية، وشكل window.chrome، وما تقوله Permissions API، وأبعاد النافذة والشاشة — ثم استضفتها على 127.0.0.1 ووجّهت إليها أربع حزم: nodriver، وBotasaurus، وPlaywright القياسي، وPuppeteer القياسي بوصفهما أداتين مرجعيتين. جميعها شغّلت نفس إصدار Chrome (Chrome for Testing 151.0.7922.10)، لذا فأي اختلاف سببه المكتبة لا المتصفح. تم الاختبار في الوضع headless والوضع المرئي، ثلاث مرات لكل حالة. وكل قيمة أدناه بقيت ثابتة عبر المحاولات الثلاث.
| الحزمة | الوضع | navigator.webdriver | token في user-agent | navigator.languages |
|---|---|---|---|---|
| nodriver 0.50.3 | headless | false | HeadlessChrome/151.0.0.0 | ["en-US"] |
| nodriver 0.50.3 | مرئي | false | Chrome/151.0.0.0 | ["en-US"] |
| Botasaurus 4.0.92 | headless / مرئي | false | HeadlessChrome/151 / Chrome/151 | ["en-US"] |
| Playwright 1.56.0 | headless / مرئي | true | HeadlessChrome/151 / Chrome/151 | ["en-US","en"] |
| Puppeteer 24.16.0 | headless / مرئي | true | HeadlessChrome/151 / Chrome/151 | ["en-US"] |
أوضح فرق هنا هو تلك القيمة المنطقية الواحدة. في إعدادات التشغيل الافتراضية هذه، تعلن nodriver عن navigator.webdriver كـ false، بينما يعلن Playwright وPuppeteer القياسيان عن true، سواء في headless أو في الوضع المرئي. هذه النتيجة تثبت فرقًا على مستوى الإعدادات؛ لكنها لا تثبت أن القيمة تنتج فقط من غياب ملف WebDriver binary.
في الحزم الأربع جميعها، ما يزال هذا الحقل هو getter الأصلي في Navigator.prototype — أي function get webdriver() { [native code] } — وليس خاصية مضافة إلى الكائن أو دالة مستبدلة. ولم يُعاد كتابة الخاصية بواسطة JavaScript في الصفحة بعد التحميل. ولم تفحص هذه التجربة وسيطة التشغيل أو مسار المصدر المسؤول عن اختلاف القيمة.
والتفصيل الثاني هو الذي يضع تسويق الأداة في سياقه. في الوضع headless، ما تزال user-agent في nodriver تعلن HeadlessChrome/151.0.0.0 — تمامًا مثل Puppeteer القياسي، وتطابق Playwright القياسي أيضًا. وعند التشغيل المرئي تتحول إلى Chrome/151.0.0.0، وبالطريقة نفسها تمامًا. إذا كنت تفترض أن أداة مقاومة الاكتشاف تُخفي أكثر سلسلة التعريف شهرة في أتمتة المتصفح تلقائيًا، فهذا ليس ما يحدث هنا. عليك أن تضبط ذلك بنفسك.
وكل شيء تقريبًا آخر كان متشابهًا عبر الحزم الأربع، وهذا يستحق قوله بوضوح لأنه يضيق القصة — فكل خاصية أدناه كانت متطابقة بين nodriver وBotasaurus وPlaywright وPuppeteer:
| الخاصية | القيمة المتطابقة في الحزم الأربع |
|---|---|
platform | MacIntel |
vendor | Google Inc. |
| الإضافات | خمسة |
| أنواع MIME | اثنان |
pdfViewerEnabled | true |
| الأنوية المنطقية | اثنتا عشرة |
| الذاكرة الجهازية المبلّغ عنها | 16 GB |
| نقاط اللمس | صفر |
window.chrome | موجود، مع app وcsi وloadTimes ومن دون runtime |
| سلسلة WebGL renderer | متطابقة في الحزم الأربع كلها |
المعروف قديمًا أن 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 لا تأتي مع متصفح مضمّن، لذا فهي تستخدم افتراضيًا أي Chrome موجود أصلًا على جهازك. في جهازي، يعني ذلك أنها اكتشفت تلقائيًا /Applications/Google Chrome.app — وهو Chrome 150.0.7871.187 — وكانت سلسلة user-agent التي كشفتها هي تلك النسخة، لا نسخة مثبتة مسبقًا داخل الحزمة. إذن فإصدار المتصفح الذي يراه العالم هو ببساطة ما هو مثبت على أسطولك أنت.
قل بوضوح ما الذي نراه هنا وما الذي لا نراه. هذه هي نتيجة ما تكشفه حزمة آلية عندما لا يُطلب منها الإخفاء — وهي مفيدة إذا كنت في جانب الدفاع، ومفيدة أيضًا إذا أردت معرفة ما تبثه أدواتك أنت. لكنها ليست مقياسًا لما إذا كان أيٌّ من ذلك مهمًا لأي خدمة بعينها. لم أختبر هذا، ولا ينبغي تفسير أي سطر أعلاه على أنه يَعِد بنتيجة.
هل تستطيع سحب المحتوى من الصفحة فعلًا؟
أن تعلن عن نفسك شيء، وأن تعيد الـ HTML الصحيح شيء آخر. شغّلت nodriver على نفس صفحة الاختبار ذات الفئات الثلاث التي يستخدمها بقية هذا المستودع المعياري، بحيث تتوافق الأرقام مع كل الأدوات الأخرى المقاسة هنا. تحتوي الصفحة على ثلاثة عناصر: A رابط ثابت علامته موجودة حرفيًا في البايتات المرسلة؛ وB عقدة ينشئها سكربت مضمّن أثناء التحليل، وتُجمَّع علامتها ورابطها من أجزاء بحيث لا يظهران إلا عند تنفيذ JavaScript؛ وC عقدة تُحقن بعد 800 مللي ثانية بعد حدث التحميل، وتُركَّب بالطريقة نفسها. الفئة C هي العنصر الخصم هنا — فقراءة عند لحظة التحميل لن تراها.
| الحزمة | قراءة افتراضية | مع انتظار صريح |
|---|---|---|
| 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() يعطي لقطة عند التحميل: فهو ينفذ JavaScript بشكل صحيح — والفئة B تثبت ذلك لأنها غير موجودة أصلًا في البايتات المرسلة — لكنه يفوّت أي شيء يُحقن بعد التحميل. أضف tab.select("#delayed-injected") فتحصل على العناصر الثلاثة كلها. نفس الفخ، ونفس الحل، كما في Playwright وPuppeteer. النتيجة ثابتة عبر ثلاث تكرارات وثلاث تشغيلات منفصلة للمجموعة كلها، من دون أي فشل عارض.
اختبرت أيضًا تأخير الحقن لأحدد عند أي نقطة تتوقف القراءة الافتراضية عن رؤية العنصر. تتوقف nodriver عن رؤية الفئة C عندما يصبح الحقن بعد 100 مللي ثانية أو أكثر من حدث التحميل — وهو نفس الحد تقريبًا في أداتي التحكم القياسيتين. (Botasaurus هي الاستثناء هنا، وهو الفرق المثير فعلًا بين أداتي anti-detect: إذ إن get() فيها ينتظر اكتمال تحميل الصفحة افتراضيًا، لذلك تظل قراءتها الافتراضية تلتقط الحقن عند 300 مللي ثانية. لكنها تدفع نحو 250 مللي ثانية لكل تنقّل مقابل ذلك.)
أما الانتظار نفسه ففيه سلوك يستحق وضعه في الميزانية. في هذا الاختبار، وصل tab.select() في nodriver إلى مقاطع polling خشنة بدل أن يتابع زمن الحقن بدقة:
| تأخير الفئة C | 0 ms | 100 ms | 400 ms | 800 ms | 1500 ms |
|---|---|---|---|---|---|
select() في nodriver | 124–152 ms | 1132–1141 | 1128–1129 | 2132–2177 | 2138–2150 |
waitForSelector في Puppeteer | 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 يتابع هذه التأخيرات بدقة أكبر. ومع كثرة الانتظارات المتسلسلة قد يتضخم الفرق، رغم أن هذه المراجعة لم تختبر صفحة إنتاجية فيها ثلاثون selector.
والإقلاع هو الموضع الآخر الذي تلتقي فيه فكرة “خفيف وغير متزامن” مع الواقع. تشغيل المتصفح وضع 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 وصفها “التركيزي”، وهذا أمر قابل للتحقق في التثبيت بحد ذاته، لا علاقة له بالسحب. هذه هي نتيجة pip install nodriver النظيف:
| حقيقة التثبيت | nodriver 0.50.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 يحتاجه. أما مجلد dist-info السادس في بيئة 17.2 MB فهو pip، وكان موجودًا أصلًا في تلك البيئة الافتراضية. لذا فعبارة “17.2 MB عبر ست توزيعات” تصف البيئة المقاسة، بينما عبارة “أربع حزم تشغيل أضيفت” تصف ما نتج عن التثبيت. هما عدّان مرتبطان، لا قابلان للتبادل.
والفارق الذي يجعل الرقم ذا معنى: على الجهاز نفسه، يزن الإطار الشقيق Botasaurus 122.3 MB عبر 44 حزمة — أي ما يقارب سبعة أضعاف الحجم على القرص. هذا هو الفرق بين برنامج تشغيل CDP متركّز وإطار شامل يأتي بكل شيء مسبقًا، ولهذا الفارق وجهان. فـ nodriver تمنحك شجرة تبعيات رفيعة ومقروءة يمكن تدقيقها فعلًا؛ أما Botasaurus فتعطيك أكثر من الصندوق، لكنها تطلب مقابل ذلك القرص ومساحة التبعيات. لا أحد منهما “أفضل” على الإطلاق — الأمر يتوقف على ما إذا كنت تريد برنامج تشغيل أم إطارًا — لكن إذا كنت تفضّل تثبيتًا صغيرًا وسهل الفحص، فـ nodriver نظيفة بشكل غير معتاد بالنسبة لما تفعله.
لكن الحجم الصغير على القرص لا يعني استيرادًا صغيرًا. ففي نسخة 3.14 التي عُدّلت بايتًا واحدًا، استغرق import nodriver نحو 158 ms (متوسط عبر عمليات استيراد جديدة في subprocess، تقريبًا 151–199 ms). أما استيراد 3.14 غير المرقّع فيفشل، لذا لا يوجد زمن مسجّل له. فالوحدات الـ57 الخاصة بـ CDP تُحمَّل كلها مبكرًا، وكانت الذاكرة المقيمة بعد الاستيراد 31.5–31.9 MB قبل وجود أي عملية Chrome أصلًا. ولو شُغِّل متصفح لاحقًا لزاد ذلك كثيرًا، ولم يُحتسب ضمن قياس الذاكرة هذا.
هناك ملاحظتان ملتصقتان بهذه الأرقام. فهي من جهاز واحد — macOS arm64 — كما أن أرقام الحجم وزمن الاستيراد أُخذت على Python 3.14 باستخدام النسخة المرقعة بايتًا واحدًا، لأن الحزمة غير المرقّعة لا تستورد أصلًا على هذا المفسّر. وعلى Python المدعومة لا تحتاج إلى patch: فقد تأكدت من استيراد نظيف غير مُعدّل على 3.12.13، وهي النسخة التي جاءت منها كل قياسات المتصفح. كما أن Chrome أو Chromium أو Edge أو Brave حقيقي ما يزال مطلوبًا وقت التشغيل — فـ nodriver تقود متصفحًا موجودًا أصلًا، ولا تأتي بمتصفح مرفق، لذا تقع كلفة ذلك خارج 17 MB، وكما أظهر قسم الكشف، فإصدار Chrome المثبّت على جهازك هو الإصدار الذي تُعلن عنه اتصالاتك.
الترخيص هو قرار التبنّي الحقيقي

معظم المراجعات لأداة مجانية تتعامل مع عبارة “مفتوحة المصدر” كأنها نهاية الحديث عن الترخيص. أما بالنسبة إلى nodriver فهي البداية، لأن الترخيص هنا AGPL-3.0 — وقد تأكد ذلك من LICENSE.txt داخل الحزمة ومن spdx_id في المستودع. هذا ترخيص copyleft شبكي قوي، وهو التزام مختلف جوهريًا عمّا يطلبه الجيران في المجال نفسه.
وأغلب الأدوات التي تُقارن بها nodriver عادةً مرخّصة برخص أكثر تساهلًا، وهذا ما يجعل الفارق واضحًا:
| الأداة | الترخيص | إذا شغّلت نسخة معدلة كخدمة شبكية |
|---|---|---|
| nodriver | AGPL-3.0 | قد تفرض المادة 13 على المشغّل أن يوفّر المصدر الموافق لنسخته المعدلة للمستخدمين عن بُعد |
| Playwright | Apache-2.0 | لا توجد مادة مقابلة للـ network copyleft |
| Puppeteer | Apache-2.0 | كما سبق |
| Botasaurus | MIT | كما سبق |
والنطاق هنا مهم. فالمادة 13 من AGPL تتناول النسخة المعدلة من البرنامج المشمول عند استخدامها للتفاعل الشبكي عن بُعد. وهذه المراجعة لا تحسم ما إذا كان كود الخدمة المحيطة المنفصل يدخل ضمن العمل المشمول، ولا تبت في حالات الاستخدام الداخلي أو الحالات الرمادية داخل المؤسسات. فإذا كنت تبني منتجًا مستضافًا يغيّر nodriver، فراجع نص الترخيص والبنية مع المستشار القانوني. هذه إشارة تقنية لا مشورة قانونية.
أنا لا أعلّق على ما إذا كان AGPL جيدًا أو سيئًا — فالـ copyleft خيار مشروع، وكثير من المشاريع الجادة تستخدمه. لكنني ألفت الانتباه إلى أن عبارة “nodriver مجانية ومفتوحة المصدر” صحيحة لكنها غير كاملة. الالتزام موجود فعلًا، وهو مختلف عن القاعدة المتساهلة السائدة في هذه الزاوية من المنظومة، ويجب أن يدخل في القرار بدل أن يذوب داخل كلمة “مجانية”. (وللإنصاف، PyPI لا يضع أي مُصنِّف ترخيص أصلًا؛ نص AGPL موجود في الحزمة، وspdx_id في المستودع، فلا تعتمد على فهرس الحزم ليُظهر لك ذلك.)
البيانات الوصفية، بتاريخها
أرقام المستودع والحزمة في نقطة زمنية محددة، مباشرة من GitHub API وPyPI:
| الحقيقة | القيمة في 14 يوليو 2026 |
|---|---|
| النجوم | 4,511 |
| النسخ (Forks) | 422 |
| القضايا المفتوحة | 14 |
| تاريخ الإنشاء | فبراير 2024 |
| آخر دفع (push) | مايو 2026 |
| أحدث إصدار على PyPI | 0.50.3 |
| الحزمة (wheel) | py3-none-any مكتوبة بالكامل ببايثون |
requires-python | >=3.9 |
| مصنّفات Python | 3.7–3.13 |
إشارات الصيانة الظاهرة مختلطة: فالمستودع تلقّى push في مايو 2026، بينما ما تزال الحزمة الأحدث المقاسة تحتوي على مشكلة استيراد Python 3.14، ولم يكن الإصلاح المقترح قد صدر حتى تاريخ البحث. عدد النجوم وعدد القضايا المفتوحة لا يحسمان ما إذا كان هذا الإيقاع يلبّي سقف الصيانة لديك أم لا.
الإيجابيات والسلبيات
الإيجابيات:
- تثبيت صغير وواضح: ثلاث تبعيات مباشرة معلنة بالإضافة إلى
wraptالانتقالية، وحوالي 17.2 MB عبر ستة مجلداتdist-infoفي البيئة المقاسة (أحدهاpip)، من دون numpy/lxml، ومن دون تنزيل متصفح وقت التثبيت. - CDP-native فعلًا: تضمّن bindings الخاصة ببروتوكول DevTools عبر 57 وحدة، وتتواصل مباشرة عبر البروتوكول، من دون chromedriver أو Selenium في الوسط.
- مساحة واسعة لاكتشاف العناصر عبر
Tab(62 طريقة عامة) مع دعم أولي لـ XPath وCSS والبحث النصي — من دون الحاجة إلى النزول إلىevaluate()من أجل XPath. - غير متزامنة بطبيعتها، مع ملف تعريف جديد في كل تشغيل، وواجهة
Configنظيفة للتحكمات الشائعة (headless، مسار التنفيذ، الوسائط، اللغة، المنافذ). - تسحب المحتوى من صفحة مبنية بجافاسكربت بكفاءة تضاهي الأدوات الثقيلة: 2 من 3 من فئات المحتوى في القراءة الافتراضية، و3 من 3 مع انتظار صريح — وهو نفس ما فعله Playwright وPuppeteer القياسيان على نفس الاختبار، بثبات عبر ثلاث مرات. وكانت عملية التنقّل ثم القراءة الأسرع بين الأربعة عند 119–129 ms.
- تُرجع
navigator.webdriverكـfalseافتراضيًا حيث تُرجعه الأدوات القياسية كـtrue، من دون ترقيع الخاصية — إذ تبقى الصيغة الأصلية getter المدمج في المتصفح. - هوية معمارية واضحة بوصفها الامتداد 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 ms عند الإقلاع البارد و31.5 MB تقريبًا في الذاكرة قبل وجود أي متصفح، لأن كل وحدات CDP الـ57 تُحمّل مبكرًا.
tab.select()تعتمد على polling بتأخير نصف ثانية، لذا فإن الانتظارات القصيرة تُقَرَّب للأعلى — تأخير 100 ms قد يكلّف نحو 1.1 ثانية، بينما يحتاجه Puppeteer نحو 210 ms. صحيح في كل مرة، لكنه يتراكم مع كثرة الانتظارات الصغيرة.- حتى في headless ما تزال الجلسة تعلن
HeadlessChromeفي الـ user-agent افتراضيًا، تمامًا مثل الأدوات القياسية؛ لا شيء في الإعداد الافتراضي يخفي أكثر سلسلة تعريف واضحة. - ما تزال بحاجة إلى Chrome أو Chromium أو Edge أو Brave حقيقي وقت التشغيل؛ التثبيت الخفيف عبر pip ليس سوى نصف القصة، وإصدار Chrome المثبّت على جهازك هو ما تعلنه اتصالاتك.
- الفاعلية ضد أي نظام anti-bot لم تُتحقق هنا — ففرضية التخفي كلها لم تُختبر عمدًا.
ما لم أختبره، وبالتالي لا أستطيع الحديث عنه: الذاكرة لكل تبويب، وزمن round-trip عبر CDP، والتعامل مع الملفات الشخصية، والإنتاجية على نطاق كبير، وLinux أو Windows، وPython 3.13 تحديدًا، والأهم — الفاعلية الواقعية ضد أي خدمة حيّة مضادة للبوتات. كل المتصفحات في هذه المراجعة تحدثت فقط مع صفحة اختبار على 127.0.0.1. وكل الأرقام من جهاز واحد (macOS arm64)؛ وأرقام قيادة المتصفح هي Python 3.12.13 من دون patch، بينما أرقام الحجم وزمن الاستيراد الأقدم كانت على Python 3.14 باستخدام النسخة المرقعة بايتًا واحدًا.
كما أنني لم أشغّل مصفوفة ترقية للمتصفح، لذلك يظل التوافق مع إصدارات Chrome المستقبلية فحصًا تشغيليًا على من يتبناها، لا نتيجة مثبتة في هذه المراجعة.
لمن تناسب، ومن الأفضل أن يتجنبها
تصلح nodriver إذا كنت تريد برنامج تشغيل Chromium خفيفًا، غير متزامن، وCDP-native، وتشعر بالراحة تجاه إدارة المتصفح نفسه والتحديثات وبيئة التشغيل. شجرة التبعيات الصغيرة أسهل تدقيقًا، والتصميم القائم على CDP يناسب التحكم على مستوى البروتوكول. أما ملاءمتها للحاويات فتبقى غير مختبرة: لم أفحص صور Linux، ولا تثبيت المتصفح، ولا المكتبات المشتركة، ولا sandbox، ولا تنظيف العمليات.
وهناك مجموعتان ينبغي أن تنظرَا إلى خيار آخر. إذا كنت على Python 3.14 ولا تريد تثبيت المفسّر على نسخة أقدم أو تعديل ملف مضمَّن، فانتظر حتى يصل الإصلاح — فمشكلة الاستيراد اليوم حاجز توقف فعلي. وإذا كان AGPL-3.0 يسبب مشكلة في طريقة النشر التي تخطط لها — مثل خدمة مستضافة مع تعديلات خاصة — فالترخيص وحده سبب كافٍ للنظر في بديل برخصة متساهلة قبل البناء عليه. لا شيء من هذا هجوم على الكود؛ لكنه قيود من الأفضل معرفتها الآن لا أثناء مراجعة الامتثال.
وتجاوزها أيضًا إذا كانت حاجتك الفعلية هي البيانات لا متصفح تديره يدويًا. فـ nodriver تمنحك tab قابلة للبرمجة و62 طريقة؛ أما تحويل صفحة معروضة إلى سجلات نظيفة ومهيكلة فذلك ما يزال كودك أنت. هذه مهمة مختلفة، وهي نقطة التقاء تأتي عندها واجهة مُدارة.
البدائل، وأين يقع Thunderbit
الإطار الصادق أولًا: nodriver مجانية، وتحت AGPL، وتستضيفها بنفسك. أنت من يدير المتصفح، ويعالج التحديثات، ويتحمل بيئة التشغيل وكل ما يتعطل فيها. وللمطور الذي يريد هذا القدر من التحكم تحديدًا، لا توجد خدمة مُدارة تنافس أداة تملكها بالفعل من ناحية السعر.
ضمن عالم المفتوح المصدر، قارن بحسب المهمة لا بحسب الشعار. إذا كنت تقارن برامج تشغيل متصفح حقيقي، فمقارنة Playwright وPuppeteer تغطي الوظيفتين الواضحتين اللتين تقف nodriver بجانبهما. أما Scrapling فهو الأقرب في المسار الموجّه إلى التخفي إذا كان هذا هو سبب اهتمامك. ولإخراج جاهز للـ LLM بدلًا من التحكم الخام بالمتصفح، فإن Crawl4AI يعرض الصفحات ويعيدها بصيغة Markdown، بينما يبقى Scrapy إطار المرجع للزحف الكبير من دون متصفح. وإذا كنت تقارن بين عدة أدوات دفعة واحدة، فمراجعة open-source scraper roundup تضع الفئات جنبًا إلى جنب.
إفصاح: هذه المقالة منشورة بواسطة Thunderbit. Thunderbit خدمة استخراج مُدارة، لذا فالمقارنة هنا قائمة على المهمة لا على البنية. فـ nodriver تمنحك طبقة تحكم بالمتصفح تستضيفها وتبرمجها بنفسك؛ أما Thunderbit فتعالج العرض وتعيد محتوى الصفحة أو سجلات على شكل schema كخدمة. استخدم nodriver عندما يكون التحكم على مستوى البروتوكول والاستضافة الذاتية مطلوبين. وفكّر في أداة استخراج مُدارة عندما تكون بنية السجل الناتج وتسليم التشغيل أهم من امتلاك المتصفح نفسه. أما تفاصيل نقاط النهاية القابلة للتغيير، والرصيد، وحدود الدُفعات، فمكانها صفحة الأسعار لا داخل benchmark لمكتبة.
جوهر المقايضة هو أين يقع العمل: nodriver تبقي إدارة المتصفح وصيانته على جانبك من دون رسوم خدمة لكل طلب؛ أما واجهة API مُدارة فتتولى تلك الطبقة وتفرض رسومًا على الاستدعاءات.
جرّب Thunderbit لاستخراج بيانات الويب
الخلاصة
استخدم nodriver إذا كنت تريد برنامج تشغيل Chromium صغيرًا، غير متزامن، وCDP-native، وقد تحققت من توافقه مع نسخة Python لديك (هنا استوردت 3.12.13 بنجاح)، وراجعت أثر AGPL-3.0 على طريقة النشر. لقد أضافت شجرة التشغيل التابعة أربع حزم، وبلغت البيئة المقاسة نحو 17 MB، وتأتي المكتبة مع 57 وحدة CDP وعدة 62 طريقة في Tab ودعم أصيل لـ XPath. وعلى هذا الاختبار أعادت 2 من 3 من فئات المحتوى افتراضيًا، و3 من 3 مع انتظار، وهو ما طابق Playwright وPuppeteer القياسيين.
لكن من الأفضل تقدير القيود بصدق. فعلى Python 3.14 لا تعمل أصلًا إلى أن تُصلح بايتًا واحدًا غير UTF-8 — وهي مشكلة موثقة وما تزال مفتوحة، لا لغزًا، لكنها توقف حقيقي يوم تصادفك. والترخيص AGPL-3.0 قرار جوهري لكل من يشغّل نسخة معدلة كخدمة، لا إجراء شكليًا. كما أن التثبيت الصغير لا يشتري استيرادًا صغيرًا، لأن وحدات CDP كلها تُحمّل مبكرًا، وselect() التي تنتظر بنصف ثانية تجعل الانتظارات القصيرة تكلف قرابة ثانية لكل مرة. أما على سؤال الكشف الافتراضي الذي أمكنني الإجابة عنه فعلًا، فالصورة أضيق مما يوحي به التسويق: تختلف قيمة منطقية واحدة عن Puppeteer القياسي، والـ user-agent في headless ما يزال يقول HeadlessChrome, وكل خاصية أخرى قيست كانت متطابقة بين الحزم الأربع. وأما فرضية مقاومة الاكتشاف كلها — وهي السبب الذي يدفع كثيرين أصلًا للعثور على nodriver — فلم أختبرها عمدًا. لقد جردت المكتبة وشغّلتها ضد صفحة على جهازي بدل أن أضعها في مواجهة دفاعات حيّة، وأفضّل أن أقول ذلك بوضوح بدل أن أقدّم لك ادعاء تجاوز لا أستطيع الدفاع عنه. في الأسئلة التي أمكن الإجابة عنها، nodriver أداة جيدة البناء، وخفيفة بشكل غير معتاد، لكنها تحمل حافتين حادتين — جدارًا مرتبطًا بإصدار Python وترخيص copyleft — ويستحسن أن تراهما قادمتين.
جرّب Thunderbit لاستخراج بيانات الويب Get Started Free
الأسئلة الشائعة
لماذا يفشل import nodriver على Python 3.14؟
لأن cdp/network.py يحتوي على بايت ± غير UTF-8 من دون إعلان لترميز المصدر. ترفض Python 3.14.2 الملف وتوقف الاستيراد المتسلسل؛ بينما تستورد Python 3.12.13 الملف نفسه المتطابق بايتًا من دون تعديل. هذه المراجعة لم تعزل التغيير المحدد في المفسّر ولم تختبر Python 3.13. المسار الأصلي هو nodriver issue #35 وpull request #36. استخدم نسخة اختبرتها، أو أعد ترميز الملف إلى UTF-8.
ما الذي تكسبه من "CDP-native، بلا webdriver"، وما تكلفة التثبيت؟
nodriver تضم 57 وحدة من مجالات DevTools وتتحدث CDP عبر WebSocket بدل استدعاء chromedriver عبر Selenium. وتوفّر Tab 62 طريقة، منها XPath الأصلي. وتذكر بيانات الحزمة ثلاث تبعيات وقت التشغيل (websockets وmss وdeprecated); وعند حلّها تُضاف wrapt. استخدمت البيئة المقاسة نحو 17.2 MB عبر ستة مجلدات dist-info، بما في ذلك pip، من دون numpy أو lxml أو تنزيل متصفح. وهناك ملاحظتان: استغرق استيراد النسخة المرقّعة نحو 158 ms لأن كل وحدات CDP الـ57 تُحمّل مبكرًا، وما يزال متصفح من عائلة Chrome مطلوبًا منفصلًا.
هل يهم ترخيص AGPL-3.0 لمشروعي؟ الأمر يتوقف على طريقة النشر. AGPL-3.0 هو ترخيص copyleft شبكي: إذا شغّلت نسخة معدلة من nodriver كخدمة يستخدمها الآخرون، فقد يتوجب عليك إتاحة مصدر النسخة المعدلة لهم. أما إذا كان لديك سكربت شخصي أو أداة داخلية لا تُعرَض للخارج، فلا مشكلة تُذكر. لكن إذا كنت تبني منتجًا تجاريًا مستضافًا على nodriver معدلة، فهذه مسألة حقيقية يجب طرحها على من يتولى الامتثال — وهو ترخيص أشد من Apache-2.0 وMIT المستخدمين في الأدوات المقارنة.
ماذا تكشف nodriver عن نفسها — وهل يعني ذلك أنها تتغلب على Cloudflare؟
النصف الأول مقاس، أما الثاني فليس كذلك، والفرق مهم. على صفحة استضفتها من 127.0.0.1، مع تشغيل نفس إصدار Chrome المستخدم من قبل الأدوات المرجعية: تعود navigator.webdriver بقيمة false، بينما يبلّغ Playwright القياسي وPuppeteer القياسي عن true. هذه القيمة تُضبط وقت تشغيل المتصفح، لا عبر ترقيع الخاصية — إذ يبقى الـ descriptor getter أصليًا من Chrome نفسه. وما عدا تلك القيمة المنطقية، كانت معظم الأمور مطابقة للأدوات المرجعية: نفس سلسلة platform، وخمس إضافات، و12 نواة، و16 GB من الذاكرة المبلّغ عنها، ونفس شكل window.chrome, ولا تناقض في Permissions API، ولا بقايا cdc_ في document أو window. وما تزال الجلسات headless تعلن HeadlessChrome/151.0.0.0 في user-agent، تمامًا مثل الأداتين المرجعيتين — وهذا ليس شيئًا مخفيًا لك. لكن ما لا تخبرك به هذه النتائج هو ما إذا كان أي من ذلك فعالًا ضد خدمة حقيقية مضادة للبوتات. لم أوجّه nodriver إلى موقع حي، ولم أتصل بخدمة anti-bot، ولم أتعامل مع CAPTCHA؛ وذلك خارج النطاق عمدًا. جدول الكشف أعلاه يصف ما تعلنه الحزمة، ولا يقول شيئًا عمّن يستمع، أو ما الذي يفعله بذلك.
هل تتعامل nodriver مع المحتوى المعروض بجافاسكربت بشكل صحيح؟
نعم، مع التحفّظ المعتاد بشأن متى تقرأ. على صفحة اختبار فيها ثلاث فئات محتوى، أعادت browser.get() متبوعة بـ tab.get_content() الافتراضي 2 من 3 — فهي تنفذ JavaScript بشكل صحيح (الفئة المحقونة تزامنيًا غير موجودة في البايتات المرسلة ومع ذلك عادت)، لكنها تقرأ عند حدث التحميل، لذا تفوّت أي شيء يُحقن بعده. بإضافة tab.select("#delayed-injected") تحصل على 3 من 3. وهذا مطابق لـ Playwright وPuppeteer القياسيين على الصفحة نفسها. واحسب حسابًا لملاحظة واحدة: select() تعتمد على polling بتأخير نصف ثانية، لذا فإن انتظار 100 ms يكلف نحو 1.1 ثانية.


