مكتبة chromedp مكتوبة بالكامل بلغة Go، مرخّصة بموجب MIT، وتشغّل متصفح Chrome حقيقيًا عبر بروتوكول Chrome DevTools. تقرأ DOM بعد تنفيذ جافاسكربت الصفحة من داخل برنامج Go، دون الحاجة إلى WebDriver منفصل أو بيئة تشغيل Node. تُدمج وحدة Go داخل التطبيق، لكن النظام القابل للتشغيل يظل بحاجة إلى ملف Chrome تنفيذي خارجي تُدار مدة حياته عبر Go contexts.
التثبيت كان بأمر go get واحد، مع توفير Chrome بنفسي: بناء allocator، اشتقاق context منه، ثم تمرير قائمة actions إلى Run. على هذا الجهاز macOS arm64 مع نسخة headless shell جاهزة على القرص، بلغت القيمة الوسيطة لزمن ظهور أول نتيجة سكربت من عملية جديدة 102 مللي ثانية. هذا رقم أساس محلي، وليس ادعاءً عامًا بأن بدء التشغيل ليس عنق زجاجة أبدًا. وفي fixture يُدرج رابطًا بعد 800 مللي ثانية من التحميل، أنهت استراتيجيتان من أصل أربع القراءة قبل أن يوجد الرابط أصلاً.
المتصفح كان حقيقيًا، والمحتوى كان موجودًا، لكن الكود ببساطة لم ينتظره. هذه الفجوة هي أهم درس تعلّمته من chromedp، وهي ليست خللًا — بل الفرق بين "عرضتُ الصفحة" و"انتظرتُ العنصر الذي أريده فعلاً"، وهو تمييز تختفي حدوده تمامًا في الحديث الشائع عن المتصفحات بلا واجهة. استراتيجية الانتظار، لا المتصفح، هي ما يحدد إن كنت ستحصل على البيانات. كل رقم هنا مصدره fixture محلي أتحكم فيه، مع تسجيل الحقيقة المرجعية قبل كل تشغيل، والملخصات الخام موجودة في مجلد chromedp داخل مستودع القياس المرجعي لدينا.
ما هي chromedp فعليًا
حزمة chromedp مصمَّمة لتكون مقتضبة. لا خادم Selenium، ولا طبقة WebDriver وسيطة، ولا بيئة Node مخفية خلف الكواليس. برنامج Go لديك يفتح اتصال WebSocket مباشرةً مع نسخة Chrome ويتحدث معها عبر CDP، وهو تقريبًا نفس بروتوكول الاتصال الذي يستخدمه Puppeteer، لكن دون جافاسكربت.
بلغ عدد نجوم المستودع 13,212 نجمة و178 issue مفتوحة عند فحصي له في 27 يوليو 2026، برخصة MIT. النسخة التي اختبرتها هي v0.16.0، وهي أحدث tag في المستودع. تجدر الإشارة قبل أن يربكك الأمر: صفحة Releases على GitHub لا تزال تُظهر v0.15.1 (نُشرت في 2026-04-01) كأحدث كائن إصدار، بينما يحل أمر go get github.com/chromedp/chromedp@latest إلى v0.16.0. وحدات Go وكائنات إصدار GitHub تباعدت هنا. الأمر ليس عطلاً، لكنه مزعج عند محاولة معرفة النسخة التي تشغّلها فعلاً.
النموذج الذهني هنا هو Go contexts من الأساس إلى القمة. تبني allocator context (يعرف كيف يشغّل Chrome)، وتشتق منه browser context، ثم تستدعي chromedp.Run(ctx, actions...) مع قائمة Actions. أي context فرعي من browser context هو تبويب جديد. وعند إلغاء context يختفي ما يمثّله. إن كانت لديك خبرة بتزامن Go فهذا سيبدو مألوفًا فورًا، وإن لم تكن كذلك فدليلنا للبدء في web scraping بلغة Go مدخل أهدأ من godoc الخاص بـ chromedp.
حدٌّ يجب وضعه مبكرًا: chromedp يسلّمك DOM مُعروضًا، لا بيانات مُهيكلة. أي شيء تستخرجه من ذلك DOM — حقول، جداول، أسعار — هو كود تكتبه أنت وتصونه. إنها driver، لا إطار عمل scraping متكامل.
الآلية الداخلية
كل شيء في chromedp هو Action، وتنفّذ Run مجموعة منها بالترتيب على هدف واحد. Navigate وClick وEvaluate وOuterHTML وWaitVisible — جميعها تشترك في الواجهة نفسها، وقابلة للتركيب معًا، وكلها في النهاية أوامر CDP بأنواع Go. هذا الاتساق هو أفضل قرار تصميمي في المكتبة، لأنه يجعل الطبقة السهلة والبروتوكول الخام يعيشان في نفس المستوى.
وهذا مهم، لأن الطبقة السهلة رقيقة عن قصد. تُبنى chromedp فوق cdproto، وهي مجموعة مُولَّدة من ربط Go المُصنَّف تغطي كامل سطح DevTools Protocol، ويوثّق godoc الخاص بـ chromedp الطبقتين جنبًا إلى جنب. حين لا يوجد action جاهز، تنزل إلى الاستدعاء المباشر للمجال — network.Enable() وpage.CaptureScreenshot() وruntime.Evaluate() — داخل نفس Run. لا جدار بين "الواجهة المريحة" و"الواجهة الحقيقية"، وهذا لا ينطبق على كل driver متصفح.
أفعال الانتظار هي حيث يعيش الحكم اليومي الفعلي، وعددها أكبر مما يستخدمه معظم الناس:
| أمر الانتظار | ما ينتظره |
|---|---|
WaitReady(sel) | حتى يُلحق العنصر بشجرة DOM |
WaitVisible(sel) | حتى يصبح العنصر مرئيًا فعليًا |
WaitNotPresent(sel) / WaitNotVisible(sel) | العكس، مفيد لمؤشرات التحميل |
Poll(js, res) | تقييم شرط جافاسكربت بشكل دوري حتى يتحقق |
إدارة العمليات هي الجزء الآخر من الآلية الذي يستحق المعرفة، لأنه يحدد إن كان برنامجك سيترك متصفحًا يعمل خلفه أم لا. تُشغّل chromedp متصفح Chrome عبر exec.CommandContext في Go. وإلغاء ذلك context يقتل العملية. هذا التفصيل التنفيذي الوحيد يفسّر كلًا من السلوك الجيد والحافة الحادة التي اصطدمت بها أثناء الاختبار.
الإعداد: ملف Go تنفيذي مع Chrome عليك توفيره بنفسك
أمر go get github.com/chromedp/chromedp حُلّ بسلاسة إلى v0.16.0 دون أي مشاكل، وشجرة الاعتماديات لا تحتوي أي استيراد cgo. لذا فإن عبارة "Go خالصة، بلا اعتماديات خارجية" التي ستراها متكررة صحيحة — بخصوص وحدة Go فقط.
لكنها غير صحيحة بخصوص التشغيل الفعلي. تُشغّل chromedp متصفح Chrome خارجيًا، وبدون وجود Chrome على الجهاز يفشل التشغيل فورًا. كل قياس أجريته زوّد المسار التنفيذي الدقيق عبر chromedp.ExecPath، مشيرًا إلى Chrome for Testing الإصدار 151.0.7922.10 بنسخة headless shell. هذا ليس انتقادًا — فتشغيل متصفح يتطلب متصفحًا — لكن "بلا اعتماديات خارجية" و"عليك شحن Chrome بحجم 155 ميجابايت مع ملفك التنفيذي" قصتا نشر مختلفتان تمامًا، ولا يظهر في README سوى إحداهما.
فخ إعداد ثانٍ كلّفني وقتًا ويستحق معرفته قبل كتابة أي كود. يوثّق issue رقم #1591 أن مُشغِّل go test في Go 1.25+ يُلغي NewExecAllocator أثناء بدء تشغيله؛ بينما نفس الكود يعمل بلا مشاكل كملف تنفيذي مُصرَّف. بنيتُ ملف اختبار تنفيذي بأمر go build واستخدمته في كل قياس بدلاً من تشغيل أي شيء عبر go test. كان إصدار Go هنا 1.26.5 على macOS arm64. إن كانت أول تجربة لك مع chromedp هي ملف اختبار يتعطل أثناء بدء تشغيل Chrome، فهذا هو الـ issue الذي يجب قراءته قبل أن تلوم كودك.
تجربة عملية: أربع طرق لقراءة نفس الصفحة، اثنتان منها فارغتان

الـ fixture خادم محلي على 127.0.0.1 يقدّم ثلاثة أصناف من المحتوى تختلف فقط في توقيت دخولها إلى DOM: رابط <a> ثابت ضمن البايتات المُرسَلة، ورابط <a> يُنشئه <script> مضمّن أثناء التحليل الأولي، ورابط <a> يُنشئه setTimeout بعد عدد قابل للضبط من المللي ثانية بعد حدث التحميل. النصوص والروابط الخاصة بالرابطين اللذين ينشئهما السكربت مُجمَّعة من أجزاء نصية داخل جافاسكربت، فلا يوجد أي نص حرفي متصل في أي مكان من البايتات المُرسَلة. لذا فإن نتيجة "found" تُثبت أن Chrome نفّذ جافاسكربت، لا أن شيئًا ما قرأ HTML فقط.
يُحسَب معدل الاسترجاع بلغة Python مقابل علامات مرجعية مُسجَّلة مسبقًا، وليس داخل probe الخاص بـ Go، حتى لا يستطيع الـ probe الغش بمعرفة الإجابة مسبقًا. شُغِّلت كل استراتيجية ثلاث مرات، وكانت مجموعات النتائج متطابقة في كل المرات الثلاث.
| استراتيجية القراءة | رابط HTML ثابت | أُدرج أثناء التحليل | أُدرج بعد 800 ms من التحميل | الزمن |
|---|---|---|---|---|
Navigate + قراءة فورية بلا انتظار | موجود | موجود | غير موجود | 317 ms |
WaitReady("body") | موجود | موجود | غير موجود | 107 ms |
WaitVisible("#delayed-injected") | موجود | موجود | موجود | 912 ms |
| Poll حتى تظهر العلامة | موجود | موجود | موجود | 972 ms |
صفّان يعودان بروابط اثنين من ثلاثة. القراءة الساذجة تفوّت لأن Navigate يعود عند حدث التحميل، والرابط الثالث لم يوجد بعد. أما WaitReady("body") فيفوّت لسبب أدق وأسوأ عمليًا: عنصر body يُلحق بـ DOM عند التحميل، فيتحقق شرط الانتظار فورًا وتشعر أنك فعلت الصواب. عاد فعليًا خلال 107 ms، أسرع من مسار عدم الانتظار، لكنه أعطاك نفس الصفحة الناقصة.
للتأكد من الآلية بدلاً من افتراضها، غيّرت مهلة الإدراج تدريجيًا وأعدت تشغيل الطرفين (recall-summary.json):
| مهلة الإدراج بعد التحميل | القراءة بلا انتظار تراه | WaitVisible يراه | زمن WaitVisible |
|---|---|---|---|
| 0 ms | نعم (سباق توقيت) | نعم | 109 ms |
| 100 ms | لا | نعم | 208 ms |
| 400 ms | لا | نعم | 519 ms |
| 800 ms | لا | نعم | 911 ms |
| 1500 ms | لا | نعم | 1625 ms |
يتتبع زمن WaitVisible مهلة الإدراج في هذا الـ fixture — من 100 إلى 208، ومن 400 إلى 519، ومن 800 إلى 911، ومن 1500 إلى 1625 — وهذا دليل أنه انتظر فعليًا حتى ظهور العنصر ولم يقرأ مبكرًا. صف 0 ms هو حالة الحد الفاصل: قد يُطلَق setTimeout(…, 0) قبل القراءة الفورية، فيلتقطه مسار عدم الانتظار أحيانًا. أما عند 100 ms فما فوق في هذا الاختبار، فقد فوّت مسار عدم الانتظار الرابط في كل تشغيلة.
في بيئة الإنتاج، يمكن لنفس خطأ التوقيت أن ينتج HTML صالحًا مع صفر صفوف مُستخرجة وخروج بكود 0 ما لم يتحقق pipeline من عدد النتائج. هذا نمط فشل معقول تدعمه سلوكيات الـ fixture، وليس حادثة قِيست هنا فعليًا. العرض ليس سوى نصف المتطلب؛ فالقراءة يجب أن تنتظر شرطًا على مستوى التطبيق مرتبطًا بالبيانات المطلوبة.
WaitReady وWaitVisible ليسا مُرتَّبين بالأفضلية — بل يجيبان عن سؤالين مختلفين
الصياغة الشائعة تقول إن WaitVisible "أكثر موثوقية" من WaitReady. هذه الصياغة غير دقيقة إلى درجة تسبب ضررًا. في صفحة بها عنصر مُلحَق بـ DOM لكن بنمط display: none، يفترق الأداءان بوضوح (waitsem-summary.json، ثلاث تشغيلات متطابقة):
| العنصر الهدف | الفعل | النتيجة | الزمن |
|---|---|---|---|
مُلحَق، display:none | WaitReady | يعود | ~6 ms |
مُلحَق، display:none | WaitVisible | ينتهي بمهلة، context deadline exceeded | 4000 ms |
| عنصر مرئي | WaitVisible، استعلام افتراضي | يعود | 4–12 ms |
| عنصر مرئي | WaitVisible، ByID | يعود | 1–2 ms |
| عنصر مرئي | WaitVisible، ByQuery | يعود | 1 ms |
WaitReady يعني الإلحاق فقط. WaitVisible يعني الظهور الفعلي. اسأل السؤال الخطأ وستتجاوز محتوى لم يُعرَض أبدًا، أو تنتظر كامل المهلة أمام عنصر لم يكن ليصبح مرئيًا أصلاً. سلوك انتهاء المهلة نظيف بحد ذاته — context deadline exceeded صحيح عند 4 ثوانٍ بالضبط، دون تعليق ودون حالة "زومبي" — وهذا أكثر مما تحققه بعض المحركات الأخرى.
فخ مُبلَّغ عنه لم يظهر في اختباري. يذكر issue رقم #440 أن WaitVisible("#id") يتعلّق مع الاستعلام الافتراضي، ولم يتكرر ذلك على v0.16.0 — عاد كل من الاستعلام الافتراضي وByID وByQuery على العنصر المرئي في كل تشغيلة. عدم التكرار لا يعني الإصلاح؛ فهذا شكل واحد من selector على صفحة واحدة، وهو لا يُغلق الـ issue.
defer cancel() الذي تجاهلته هو ما يمنع السقف من الانهيار
المقياس هنا عدد العمليات، لا القيم المُعادة. استخدم كل تشغيل لدورة الحياة --user-data-dir فريدًا، وعُدّت عمليات متصفح Chrome الفعلية بأمر pgrep مع استبعاد عمليات renderer الفرعية. شُغِّل كل مسار ثلاث مرات (lifecycle-summary.json).
| مسار الخروج (macOS، 3 تشغيلات لكل مسار) | ماذا حدث لـ chrome-headless-shell المُشغَّل | الزمن |
|---|---|---|
| إلغاء context وallocator | يختفي | 13 و13 و12 مللي ثانية |
| إنهاء عملية Go دون إلغاء | يبقى بعد انتهاء برنامجك — صفر عمليات قبل التشغيل، وواحدة بعد الخروج؛ عمليات يتيمة في 3 من 3 تشغيلات | — |
الإلغاء نظيف وسريع، وهو تمامًا ما يعد به exec.CommandContext. (كل عملية يتيمة أُنهيت قسرًا لاحقًا بواسطة أداة الاختبار؛ وأُعيد الجهاز إلى حالة نظيفة.)
هذا سلوك معروف وموثَّق ومحدود بمنصة معيّنة — القياس لي، أما الاكتشاف فليس لي. تناوله متتبع مشكلات chromedp من عدة زوايا: #774 يصف نفس سلوك عدم الخروج على FreeBSD، و#752 يبلّغ عن عمليات Chromium متعلّقة على macOS، بينما يشرح #562 و#1566 الآلية بالتفصيل. ما أضفته أنا هو عدد العمليات وتوقيتها على طرفي المقارنة، وهو ما لا توفره تلك التقارير الوصفية.
الآلية نفسها قصة مرتبطة بـ build tags تستحق المعرفة. في مصدر v0.16.0، يضبط allocate_linux.go قيمة Pdeathsig = SIGKILL على العملية الفرعية، فيحصل Linux على إشارة موت-الأب على مستوى النواة. أما allocate_other.go، وهو ما يُصرَّف على macOS، فيجعل ذلك الاستدعاء بلا أثر. لا توجد إشارة مكافئة على darwin، فلا شيء يقتل Chrome عند خروج برنامجك. في المقابل، يُقرأ نص godoc كوعد عام — الأمر الافتراضي "يرسل SIGKILL إلى أي متصفحات مفتوحة عند خروج برنامج Go" — بينما يعيش تخصيص Linux فقط في مصدر مُعلَّم بـ build tag يجب أن تذهب لقراءته بنفسك. وصف التوثيق بأنه مبالِغ في الوعد أمر منصف؛ أما وصفه بأنه خلل في chromedp فغير دقيق.
النتيجة العملية ثابتة في الحالتين: على macOS، defer cancel() عنصر حامل للحمل. تجاهله وستُسرّب كل تشغيلة عملية متصفح. لم أختبر Linux، لذا لا أعمّم نتيجة العمليات اليتيمة عليه — المصدر يوحي بأن Linux يتصرف بشكل مختلف، والإيحاء ليس قياسًا.
البدء البارد، والتزامن، والتفاصيل المملة التي تحدد نشرك الفعلي

بلغت القيمة الوسيطة 102 ms من عملية جديدة وallocator وcontext وتصفح localhost حتى أول Evaluate، عبر خمس عمليات، بمدى من 98 إلى 111 ms (coldstart-summary.json). في هذا الـ fixture على macOS arm64 مع نسخة headless shell جاهزة على القرص، كان زمن البدء صغيرًا مقارنة بانتظار المحتوى المتأخر. لم تُقَس الحاويات، ولا أنظمة الملفات الباردة، ولا CI، ولا بيئات serverless، ولا التصفح في الإنتاج.
بخصوص التزامن، تمنحك chromedp شكلين — متصفح واحد بعدة child contexts (تبويبات)، أو عدة متصفحات مستقلة. أربع عمليات تصفح، بثلاث تشغيلات لكل منها (concurrency-summary.json):
| النمط | الزمن الكلي (p50) | المدى | أقصى عدد عمليات Chrome |
|---|---|---|---|
| متصفح مشترك، 4 child contexts | 214 ms | 209–219 | 1 |
| 4 متصفحات مستقلة | 264 ms | 261–278 | 4 |
النتيجة المقيسة هي عدد العمليات: عملية متصفح Chrome واحدة مقابل أربع لهذه العمليات المحلية البسيطة الأربع. مديا الزمن الكلي لم يتقاطعا لكنهما يبقيان مؤشرًا اتجاهيًا لا قياس أداء إنتاجية فعليًا. لم تُقَس RSS ولا PSS، لذا لا يُثبت هذا الاختبار أي توفير في الذاكرة.
اختبار مسار الأخطاء الصغير هنا ليس مفصَّلًا بما يكفي لدعم أي ادعاء متانة: لا تحدد المسودة إن كان الذي كشف كل حالة هو رمز HTTP أو خطأ تصفح أو حدث أو منطق أداة الاختبار نفسها. اعتبر التعامل مع الأخطاء 500 والروابط الميتة أمرًا غير موثَّق حتى تُنشَر نتيجة API الدقيقة والملف الخام.
أشياء لم تُختبَر، وتخرج بالتالي عن نطاق هذه الأرقام: سلوك دورة الحياة على Linux، والتزامن بأكثر من N=4 أو مع عمل حقيقي لكل صفحة، وفروقات الذاكرة (عددت العمليات لا RSS)، واعتراض الشبكة والتقاط الطلبات، وتقارير مهلة WaitReady المفتوحة في #168 و#1593 — تصف هذه التقارير مهلات متقطعة، بينما ما قسته أنا هو دلالة الانتظار، وهو سؤال مختلف. جهاز واحد، وبنية Chrome واحدة.
ليست chromedp الـ driver الوحيدة المكتوبة بلغة Go لـ CDP في هذا الاختبار: مرّت rod عبر نفس الـ fixture وأداة الاختبار والجهاز وبنية Chrome في نفس الجلسة، ولها مراجعة خاصة بها.
الإيجابيات والسلبيات
الإيجابيات:
- وصول حقيقي إلى CDP — تعمل أفعال الراحة واستدعاءات
cdprotoالخام معًا داخل نفسRun، فلا تصطدم أبدًا بسقف للواجهة. - خط أساس محلي لدورة البدء البارد بقيمة وسيطة 102 ms ومدى 98–111 ms على fixture اختبار macOS.
- الإلغاء يُنهي Chrome خلال ~13 ms، وباستمرار في كل تشغيلة.
- تشترك child contexts في عملية متصفح واحدة لعدد N من التبويبات (عملية واحدة مقابل 4 للمتصفحات المنفصلة).
- حتمية في الاختبار — كانت مجموعات الاسترجاع ودلالات الانتظار ونتائج دورة الحياة متطابقة عبر ثلاث تكرارات لكل منها.
- تعامل نظيف مع انتهاء المهلة: أعاد
WaitVisibleعلى شرط غير قابل للتحققcontext deadline exceededصحيحًا عند 4 ثوانٍ بالضبط بدلاً من التعليق. - وحدة Go خالصة (بلا cgo)، مرخّصة بموجب MIT؛ لكن لا تزال بحاجة إلى ملف Chrome تنفيذي خارجي وقت التشغيل.
السلبيات:
- تتطلب Chrome خارجيًا وقت التشغيل؛ سمعة "بلا اعتماديات" تخص وحدة Go فقط.
WaitReady("body")فخ يبدو صحيحًا لكنه يفوّت بصمت المحتوى الذي يظهر بعد التحميل — عاد خلال 107 ms بصفحة ناقصة.- مسار
Navigate+ قراءة ساذجة يفوّت أي شيء يُدرَج بعد ~100 ms أو أكثر من التحميل، بشكل حتمي ودون أي خطأ. - على macOS، الخروج دون
cancel()يترك المتصفح يتيمًا (3 من 3 تشغيلات). سلوك معروف ومحدود بمنصة معيّنة، لكن يسهل الوقوع فيه. - صياغة godoc عن SIGKILL عند الخروج تُقرأ كأنها عامة، بينما الآلية مقتصرة على مصدر مُعلَّم لـ Linux فقط.
- يمكن لـ
go testفي Go 1.25+ أن يُلغي بدء تشغيل allocator (#1591)؛ ابنِ ملفًا تنفيذيًا بدلاً من ذلك. - تُعيد DOM لا بيانات مُهيكلة — كل حقل تريده هو كود تحليل تملكه أنت وتصونه.
- أحدث tag (v0.16.0) متقدم على أحدث كائن Release على GitHub (v0.15.1)، ما يجعل التحقق من الإصدار مربكًا قليلاً.
لمن تناسب chromedp، ومن يجب أن يتجاوزها
إن كانت خدمتك مكتوبة بلغة Go أصلاً وتحتاج متصفحًا حقيقيًا داخلها، فإن chromedp تكاد تكون الخيار البديهي. لا عملية Node تحتاج إشرافًا، ولا خادم WebDriver يجب إبقاؤه حيًا؛ ملف تنفيذي واحد مع Chrome تشحنه أو تثبّته. يتطابق نموذج context مع بدائيات تزامن Go بشكل مباشر لدرجة أن دورة حياة المتصفح تُدار بنفس انضباط defer الذي يحكم بقية كودك. وإن احتجت شيئًا لا تغطيه الواجهة السهلة — أحداث شبكة CDP، أو نقاط ربط دقيقة بدورة حياة الصفحة، أو حيل على مستوى البروتوكول — فتنزل إلى cdproto دون مغادرة المكتبة.
كما تناسب جيدًا حين تريد تحكمًا صريحًا في الانتظار. أفعال الانتظار بدائية لا استدلالية؛ تفعل بالضبط ما تقوله، وهذه ميزة بمجرد أن تتقبّل أن اختيار الفعل الصحيح صار مسؤوليتك.
تجاوزها إن كان فريقك لا يكتب بلغة Go — الالتزام باللغة هو التكلفة الحقيقية، لا المكتبة نفسها. تجاوزها إن أردت ergonomics تنتظر تلقائيًا وتخمّن بالنيابة عنك، لأن chromedp لن تخمّن؛ ستفعل بالضبط ما طلبته وتُعيد ما كان في الصفحة في تلك اللحظة فقط. تجاوزها، أو خصّص لها ميزانية جدّية، إن كنت تحتاج فعليًا سجلات مُهيكلة لا DOM: فكل حقل هو selector تكتبه وتختبره وتُصلحه عند تغيّر الموقع. وإن كان كل ما تريده هو "أحضر لي البيانات من هذه الـ 500 رابط"، فإن بناء تنسيق متصفحات كامل بلغة Go آلية كبيرة مقابل هذا الطلب. يشرح دليلنا لأتمتة المتصفح متى تستحق هذه الآلية جهدها ومتى لا تستحقه.
البدائل، وأين يقع منتجنا من هذه الخريطة
ضمن فئة drivers المتصفح، تُعد rod driver آخر مكتوبًا بلغة Go لـ CDP، بينما Playwright وPuppeteer خياران من جهة Node تناولناهما في مقارنتنا بين Playwright وPuppeteer. عقود الانتظار في كل منهما غير قابلة للتبادل. ينتظر Playwright تلقائيًا حتى تصبح إمكانية التفاعل متاحة قبل كثير من الأفعال؛ لكن ذلك لا يخبره متى انتهت بيانات التطبيق من الوصول بعد الفعل. تمنحك chromedp بدائيات انتظار أدنى مستوى وتترك كلًا من إمكانية التفاعل وجاهزية مستوى التطبيق للمستدعي. وإن كنت تستعرض المجال الأوسع، فإن استعراضنا لأدوات scraping مفتوحة المصدر اختبرناها يغطي الزواحف الثابتة ومكتبات الاستخراج على الجانب الآخر من هذا الخط.
مراجعة ذات صلة: مراجعة Browserless.
خدمة الاستخراج المُدارة فئة مختلفة تمامًا. تستبدل التحكم بالمتصفح والـ selectors بتفويض العرض وتشكيل schema للخارج. قد يكون هذا مفيدًا حين يكون المُخرَج المطلوب سجلات مُهيكلة لا DOM، بينما تناسب chromedp أكثر حين يجب أن يبقى المتصفح تحت سيطرة خدمتك المكتوبة بـ Go. نحن نبني Thunderbit، إحدى هذه الخدمات، لكننا لم نُشغّلها على هذا الـ fixture؛ لذا لا يدعم هذا الاختبار أي تكافؤ أو مقارنة زمن استجابة أو جودة استخراج أو تكلفة مع chromedp.
جرّب Thunderbit لاستخراج بيانات الويب
الخلاصة
هل يجب أن تستخدم chromedp؟ نعم، إن كنت تكتب بلغة Go وتريد متصفحًا حقيقيًا تحت سيطرتك. في هذا الـ fixture المحلي، وصلت إلى أول نتيجة سكربت خلال قيمة وسيطة 102 ms، وأنهت Chrome خلال نحو 13 ms بعد الإلغاء، واستخدمت عملية متصفح واحدة لأربع تبويبات متزامنة. هذه ملاحظات محدودة النطاق، لا وعود أداء عالمية؛ الجاذبية الدائمة هي الوصول المباشر إلى CDP من Go حين تنفد طبقة الراحة.
لكن قيّم الادعاءات بحجمها الحقيقي، لأن السمعة تبالغ في أمرين. "Go خالصة، بلا اعتماديات" تصف الوحدة فقط؛ فأنت وقت التشغيل تشحن وتُدير ملف Chrome تنفيذيًا. و"استخدم متصفحًا بلا واجهة وستحصل على المحتوى الديناميكي" صحيح فقط حين يكون انتظارك مرتبطًا بالعنصر الذي تريده — فكل من القراءة الساذجة وWaitReady("body") أعطاني صفحة ناقصة المحتوى الذي أُدرِج بعد 800 ms من التحميل، بصمت، في كل مرة. وعلى macOS، defer cancel() ليس خيار أسلوب؛ تجاهله وستُسرّب متصفحًا في كل تشغيلة، وهو سلوك منصة معروف لكنه يبقى مشكلتك أنت. اضبط هذه الأمور الثلاثة بشكل صحيح وستكون chromedp من بين أكثر drivers المتصفح التي قِستها قابلية للتنبؤ. أخطئ فيها وستفشل بصمت، وهذا أسوأ شكل فشل لأداة scraping.
جرّب Thunderbit لاستخراج بيانات الويب Get Started Free
الأسئلة الشائعة
هل تستطيع chromedp فعلاً رؤية المحتوى الذي تُنشئه جافاسكربت؟
نعم — لكن فقط مع انتظار مرتبط بالعنصر الذي تريده. في fixture أُدرِج فيه رابط بعد 800 ms من حدث التحميل، فوّته Navigate مع قراءة فورية، وفوّته WaitReady("body") أيضًا، بينما استرجعه كل من WaitVisible على ذلك العنصر واستطلاع جافاسكربت، في ثلاث من ثلاث تشغيلات لكل منهما. وعند تغيير المهلة تدريجيًا، فوّتت القراءة بلا انتظار العنصر في كل إعداد من 100 ms فأعلى. العرض ضروري؛ والانتظار الصحيح هو ما يجعله كافيًا.
ما الفرق بين WaitReady وWaitVisible؟
ينتظر WaitReady حتى يُلحق العنصر بـ DOM. وينتظر WaitVisible حتى يصبح مرئيًا فعليًا. في عنصر مُلحَق لكن بنمط display: none، عاد WaitReady خلال نحو 6 ms، بينما انتظر WaitVisible حتى مهلة context البالغة 4 ثوانٍ وعاد بـ context deadline exceeded نظيف. لا أحدهما "أكثر موثوقية" من الآخر — بل يجيبان عن سؤالين مختلفين، والفخ الحقيقي هو اختيار السؤال الخطأ.
هل أحتاج فعلاً إلى defer cancel() مع chromedp؟
على macOS، نعم. أنهى إلغاء context وallocator عملية Chrome المُنشَأة خلال 12–13 ms في كل تشغيلة؛ بينما ترك إنهاء عملية Go دون إلغاء عملية متصفح يتيمة في كل التشغيلات الثلاث. هذا سلوك معروف ومحدود بمنصة معيّنة — يوثّق متتبع مشكلات chromedp نفس نمط عدم الخروج على أنظمة أخرى غير Linux، وإشارة قتل موت-الأب التي تعالج ذلك تعيش فقط في مصدر مُعلَّم لـ Linux. لم أختبر Linux، لذا اعتبر نتيجة العمليات اليتيمة مقتصرة على macOS.
هل تحتاج chromedp إلى تثبيت Chrome بشكل منفصل؟
نعم. وحدة Go نفسها Go خالصة بلا cgo، لكنها تُشغّل متصفحًا خارجيًا وتفشل فورًا دون وجوده. زوّدت الاختبار بنسخة Chrome for Testing 151.0.7922.10 بصيغة headless shell صراحةً عبر chromedp.ExecPath. والميزة أن تكلفة البدء صغيرة: بلغت القيمة الوسيطة لدورة بدء بارد كاملة حتى أول نتيجة سكربت 102 ms عبر خمس عمليات جديدة، بمدى 98–111 ms.
هل يُفضَّل مشاركة متصفح واحد بين التبويبات أم تشغيل متصفحات منفصلة؟ فضّل child contexts حين يكون هدفك تقليل عدد عمليات المتصفح. استخدمت أربع عمليات تصفح لـ localhost عبر متصفح واحد عملية Chrome واحدة؛ بينما استخدمت أربعة متصفحات منفصلة أربع عمليات. كما فضّل الزمن الكلي الإعداد المشترك (214 ms مقابل 264 كقيمة وسيطة)، لكن أربع صفحات محلية بسيطة ليست قياس أداء إنتاجية حقيقيًا. لم تُقَس الذاكرة. تبقى المتصفحات المنفصلة الخيار الصحيح حين تحتاج عزل جلسات أقوى، أو وكلاء proxy مختلفين، أو نطاق تأثير أصغر عند الأعطال؛ وهذه المفاضلات خارج نطاق هذا الاختبار.


