تجريف الويب باستخدام متصفح مقطوع الرأس: برنامج تعليمي لمحرك العرائس
نشرت: 2022-03-11في هذه المقالة ، سنرى مدى سهولة إجراء تجريف الويب (أتمتة الويب) بطريقة غير تقليدية إلى حد ما باستخدام متصفح بدون رأس .
ما هو متصفح مقطوعة الرأس ولماذا هو مطلوب؟
شهدت السنوات القليلة الماضية تطور الويب من مواقع ويب مبسطة تم إنشاؤها باستخدام HTML و CSS. يوجد الآن المزيد من تطبيقات الويب التفاعلية ذات واجهات المستخدم الجميلة ، والتي غالبًا ما يتم إنشاؤها باستخدام أطر مثل Angular أو React. بعبارة أخرى ، تحكم JavaScript في الوقت الحاضر الويب ، بما في ذلك كل شيء تقريبًا تتفاعل معه على مواقع الويب.
لأغراضنا ، تعد JavaScript لغة من جانب العميل. يقوم الخادم بإرجاع ملفات JavaScript أو البرامج النصية التي تم إدخالها في استجابة HTML ، ويقوم المستعرض بمعالجتها. الآن ، هذه مشكلة إذا كنا نقوم بنوع من تجريف الويب أو أتمتة الويب لأن المحتوى الذي نرغب في رؤيته أو كشطه يتم عرضه فعليًا بواسطة كود JavaScript ولا يمكن الوصول إليه من استجابة HTML الأولية التي يسلمها الخادم.
كما ذكرنا أعلاه ، تعرف المتصفحات كيفية معالجة JavaScript وتقديم صفحات ويب جميلة. الآن ، ماذا لو تمكنا من الاستفادة من هذه الوظيفة لتلبية احتياجات التجريف الخاصة بنا وكان لدينا طريقة للتحكم في المتصفحات برمجيًا؟ هذا هو بالضبط المكان الذي تدخل فيه أتمتة المتصفح بدون رأس!
مقطوعة الرأس؟ عفوا؟ نعم ، هذا يعني فقط عدم وجود واجهة مستخدم رسومية (GUI). بدلاً من التفاعل مع العناصر المرئية بالطريقة المعتادة - على سبيل المثال باستخدام الماوس أو جهاز يعمل باللمس - يمكنك أتمتة حالات الاستخدام بواجهة سطر أوامر (CLI).
كروم مقطوع الرأس ومحرك الدمى
هناك العديد من أدوات تجريف الويب التي يمكن استخدامها للتصفح بدون رأس ، مثل Zombie.js أو Firefox بدون رأس باستخدام السيلينيوم. لكننا اليوم سنستكشف Chrome مقطوعة الرأس عبر Puppeteer ، حيث إنه لاعب أحدث نسبيًا ، تم إصداره في بداية عام 2018. ملاحظة المحرر: من الجدير بالذكر متصفح Intoli's Remote Browser ، وهو لاعب جديد آخر ، ولكن يجب أن يكون موضوعًا لآخر شرط.
ما هو محرك العرائس بالضبط؟ إنها مكتبة Node.js توفر واجهة برمجة تطبيقات عالية المستوى للتحكم في Chrome بدون رأس أو Chromium أو للتفاعل مع بروتوكول DevTools. يتم صيانته بواسطة فريق Chrome DevTools ومجتمع رائع مفتوح المصدر.
يكفي الحديث - دعنا ننتقل إلى الكود ونستكشف عالم كيفية أتمتة تجريف الويب باستخدام تصفح Puppeteer بدون رأس!
اعداد البيئة
بادئ ذي بدء ، ستحتاج إلى تثبيت Node.js 8+ على جهازك. يمكنك تثبيته هنا ، أو إذا كنت من محبي CLI مثلي وترغب في العمل على Ubuntu ، فاتبع هذه الأوامر:
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs
ستحتاج أيضًا إلى بعض الحزم التي قد تكون أو لا تكون متوفرة على نظامك. فقط لتكون آمنًا ، حاول تثبيت ما يلي:
sudo apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3
إعداد الكروم مقطوعة الرأس ومحرك الدمى
أوصي بتثبيت برنامج Puppeteer باستخدام npm
، حيث سيتضمن أيضًا إصدار Chromium الثابت المحدث والمضمون للعمل مع المكتبة.
قم بتشغيل هذا الأمر في الدليل الجذر لمشروعك:
npm i puppeteer --save
ملاحظة: قد يستغرق هذا بعض الوقت حيث سيحتاج محرك الدمى إلى تنزيل Chromium وتثبيته في الخلفية.
حسنًا ، الآن بعد أن تم ضبطنا وتكويننا جميعًا ، دع المرح يبدأ!
استخدام Puppeteer API لكشط الويب الآلي
لنبدأ البرنامج التعليمي لمحرك العرائس بمثال أساسي. سنكتب برنامجًا نصيًا يجعل متصفحنا بدون رأس يأخذ لقطة شاشة لموقع من اختيارنا.
أنشئ ملفًا جديدًا في دليل المشروع باسم screenshot.js
وافتحه في محرر الكود المفضل لديك.
أولاً ، دعنا نستورد مكتبة Puppeteer في البرنامج النصي الخاص بك:
const puppeteer = require('puppeteer');
بعد ذلك ، لنأخذ عنوان URL من وسيطات سطر الأوامر:
const url = process.argv[2]; if (!url) { throw "Please provide a URL as the first argument"; }
الآن ، نحتاج إلى أن نضع في اعتبارنا أن Puppeteer هي مكتبة قائمة على الوعد: فهي تقوم بإجراء مكالمات غير متزامنة إلى مثيل Chrome بدون رأس تحت الغطاء. دعنا نحافظ على الكود نظيفًا باستخدام غير متزامن / انتظار. لذلك ، نحتاج إلى تحديد وظيفة غير async
أولاً ووضع كل كود محرك الدمى هناك:
async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();
إجمالاً ، الكود النهائي يبدو كالتالي:
const puppeteer = require('puppeteer'); const url = process.argv[2]; if (!url) { throw "Please provide URL as a first argument"; } async function run () { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); await page.screenshot({path: 'screenshot.png'}); browser.close(); } run();
يمكنك تشغيله عن طريق تنفيذ الأمر التالي في الدليل الجذر لمشروعك:
node screenshot.js https://github.com
انتظر لحظة ، وازدهار! أنشأ متصفحنا بدون رأس ملفًا باسم screenshot.png
ويمكنك رؤية صفحة GitHub الرئيسية المعروضة فيه. رائع ، لدينا مكشطة ويب Chrome تعمل!
دعنا نتوقف لمدة دقيقة ونستكشف ما يحدث في دالة run()
أعلاه.
أولاً ، أطلقنا مثيل متصفح جديد بدون رأس ، ثم نفتح صفحة جديدة (علامة تبويب) وانتقل إلى عنوان URL المقدم في وسيطة سطر الأوامر. أخيرًا ، نستخدم طريقة Puppeteer المدمجة لأخذ لقطة شاشة ، ونحتاج فقط إلى توفير المسار حيث يجب حفظها. نحتاج أيضًا إلى التأكد من إغلاق المتصفح بدون رأس بعد أن ننتهي من التشغيل الآلي الخاص بنا.
الآن بعد أن غطينا الأساسيات ، دعنا ننتقل إلى شيء أكثر تعقيدًا بعض الشيء.
مثال ثانٍ لكشط محرك العرائس
بالنسبة للجزء التالي من البرنامج التعليمي لمحرك العرائس ، فلنفترض أننا نريد حذف أحدث المقالات من Hacker News.
أنشئ ملفًا جديدًا باسم ycombinator-scraper.js
والصقه في مقتطف الشفرة التالي:
const puppeteer = require('puppeteer'); function run () { return new Promise(async (resolve, reject) => { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let urls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }) browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run().then(console.log).catch(console.error);
حسنًا ، هناك المزيد مما يحدث هنا مقارنةً بالمثال السابق.
أول شيء قد تلاحظه هو أن الدالة run()
ترجع الآن وعدًا ، لذا انتقلت البادئة غير async
إلى تعريف وظيفة الوعد.
لقد قمنا أيضًا بلف جميع التعليمات البرمجية الخاصة بنا في كتلة try-catch حتى نتمكن من التعامل مع أي أخطاء تتسبب في رفض وعدنا.
وأخيرًا ، نستخدم طريقة محرك الدمى المضمنة والتي تسمى evaluate()
. تتيح لنا هذه الطريقة تشغيل كود JavaScript مخصص كما لو كنا ننفذها في وحدة تحكم DevTools. أي شيء يتم إرجاعه من هذه الوظيفة يتم حله بالوعد. هذه الطريقة مفيدة للغاية عندما يتعلق الأمر بكشط المعلومات أو تنفيذ إجراءات مخصصة.
الكود الذي تم تمريره إلى طريقة evaluate()
هو JavaScript أساسي جدًا يقوم ببناء مجموعة من الكائنات ، لكل منها url
وحقول text
تمثل عناوين URL الخاصة بالقصة التي نراها على https://news.ycombinator.com/.
يبدو إخراج البرنامج النصي على هذا النحو (ولكن مع 30 إدخالًا ، في الأصل):
[ { url: 'https://www.nature.com/articles/d41586-018-05469-3', text: 'Bias detectives: the researchers striving to make algorithms fair' }, { url: 'https://mino-games.workable.com/jobs/415887', text: 'Mino Games Is Hiring Programmers in Montreal' }, { url: 'http://srobb.net/pf.html', text: 'A Beginner\'s Guide to Firewalling with pf' }, // ... { url: 'https://tools.ietf.org/html/rfc8439', text: 'ChaCha20 and Poly1305 for IETF Protocols' } ]
أنيق جدا ، أود أن أقول!

حسنًا ، دعنا نمضي قدمًا. لم يكن لدينا سوى 30 عنصرًا تم إرجاعها ، بينما يتوفر الكثير منها - وهي موجودة فقط في صفحات أخرى. نحتاج إلى النقر فوق الزر "المزيد" لتحميل الصفحة التالية من النتائج.
دعنا نعدل البرنامج النصي الخاص بنا قليلاً لإضافة دعم ترقيم الصفحات:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);
لنراجع ما فعلناه هنا:
- أضفنا وسيطة واحدة تسمى
pagesToScrape
إلى دالةrun()
الرئيسية الخاصة بنا. سنستخدم هذا لتحديد عدد الصفحات التي سيقوم البرنامج النصي الخاص بنا بكشطها. - يوجد متغير آخر جديد يسمى
currentPage
والذي يمثل رقم صفحة النتائج التي نبحث عنها حاليًا. تم ضبطه على1
في البداية. قمنا أيضًا بلف وظيفةevaluate()
الخاصة بنا في حلقةwhile
، بحيث تستمر في العمل طالما أنcurrentPage
أقل من أو تساويpagesToScrape
. - أضفنا الحظر للانتقال إلى صفحة جديدة وانتظار تحميل الصفحة قبل إعادة تشغيل حلقة
while
.
ستلاحظ أننا استخدمنا طريقة page.click()
لجعل المتصفح بدون رأس ينقر فوق الزر "المزيد". استخدمنا أيضًا طريقة waitForSelector()
للتأكد من إيقاف منطقنا مؤقتًا حتى يتم تحميل محتويات الصفحة.
كلاهما من أساليب Puppeteer API عالية المستوى جاهزة للاستخدام خارج الصندوق.
إحدى المشكلات التي قد تواجهها على الأرجح أثناء استخدام محرك العرائس هي انتظار تحميل الصفحة. تحتوي Hacker News على بنية بسيطة نسبيًا وكان من السهل جدًا انتظار اكتمال تحميل الصفحة. لحالات الاستخدام الأكثر تعقيدًا ، يقدم Puppeteer مجموعة واسعة من الوظائف المضمنة ، والتي يمكنك استكشافها في وثائق API على GitHub.
كل هذا رائع جدًا ، لكن برنامجنا التعليمي Puppeteer لم يشمل التحسين حتى الآن. دعونا نرى كيف يمكننا جعل محرك العرائس يعمل بشكل أسرع.
تحسين سيناريو محرك العرائس لدينا
الفكرة العامة هي عدم السماح للمتصفح مقطوعة الرأس بالقيام بأي عمل إضافي. قد يشمل ذلك تحميل الصور وتطبيق قواعد CSS وإطلاق طلبات XHR وما إلى ذلك.
كما هو الحال مع الأدوات الأخرى ، يعتمد تحسين محرك العرائس على حالة الاستخدام الدقيقة ، لذا ضع في اعتبارك أن بعض هذه الأفكار قد لا تكون مناسبة لمشروعك. على سبيل المثال ، إذا تجنبنا تحميل الصور في مثالنا الأول ، فقد لا تبدو لقطة الشاشة الخاصة بنا بالطريقة التي نريدها.
على أي حال ، يمكن تحقيق هذه التحسينات إما عن طريق تخزين الأصول مؤقتًا عند الطلب الأول ، أو إلغاء طلبات HTTP تمامًا كما بدأها موقع الويب.
دعونا نرى كيف يعمل التخزين المؤقت أولاً.
يجب أن تدرك أنه عند تشغيل مثيل متصفح جديد بدون رأس ، يقوم Puppeteer بإنشاء دليل مؤقت لملفه الشخصي. تتم إزالته عند إغلاق المتصفح ولا يكون متاحًا للاستخدام عند تشغيل مثيل جديد - وبالتالي لن يمكن الوصول إلى جميع الصور و CSS وملفات تعريف الارتباط والعناصر الأخرى المخزنة بعد الآن.
يمكننا إجبار محرك العرائس على استخدام مسار مخصص لتخزين البيانات مثل ملفات تعريف الارتباط وذاكرة التخزين المؤقت ، والتي ستتم إعادة استخدامها في كل مرة نقوم بتشغيلها مرة أخرى - حتى تنتهي صلاحيتها أو يتم حذفها يدويًا.
const browser = await puppeteer.launch({ userDataDir: './data', });
يجب أن يمنحنا هذا قفزة جيدة في الأداء ، حيث سيتم تخزين الكثير من CSS والصور في دليل البيانات عند الطلب الأول ، ولن يحتاج Chrome إلى تنزيلها مرارًا وتكرارًا.
ومع ذلك ، سيستمر استخدام هذه الأصول عند عرض الصفحة. في احتياجات الكشط الخاصة بنا لمقالات إخبارية Y Combinator ، لا داعي للقلق حقًا بشأن أي عناصر مرئية ، بما في ذلك الصور. نحن نهتم فقط بمخرجات HTML العارية ، لذلك دعونا نحاول حظر كل طلب.
لحسن الحظ ، يعد Puppeteer رائعًا للعمل معه ، في هذه الحالة ، لأنه يأتي مع دعم للخطافات المخصصة. يمكننا توفير معترض عند كل طلب وإلغاء الطلبات التي لا نحتاجها حقًا.
يمكن تعريف المعترض بالطريقة التالية:
await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } });
كما ترى ، لدينا سيطرة كاملة على الطلبات التي يتم البدء فيها. يمكننا كتابة منطق مخصص للسماح بطلبات معينة أو إحباطها بناءً على نوع resourceType
الخاص بهم. لدينا أيضًا إمكانية الوصول إلى الكثير من البيانات الأخرى مثل request.url
حتى نتمكن من حظر عناوين URL محددة فقط إذا أردنا ذلك.
في المثال أعلاه ، نسمح فقط للطلبات التي تحتوي على نوع مورد "document"
بالمرور عبر عامل التصفية الخاص بنا ، مما يعني أننا سنحظر جميع الصور و CSS وكل شيء آخر إلى جانب استجابة HTML الأصلية.
هذا هو الكود النهائي لدينا:
const puppeteer = require('puppeteer'); function run (pagesToScrape) { return new Promise(async (resolve, reject) => { try { if (!pagesToScrape) { pagesToScrape = 1; } const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setRequestInterception(true); page.on('request', (request) => { if (request.resourceType() === 'document') { request.continue(); } else { request.abort(); } }); await page.goto("https://news.ycombinator.com/"); let currentPage = 1; let urls = []; while (currentPage <= pagesToScrape) { await page.waitForSelector('a.storylink'); let newUrls = await page.evaluate(() => { let results = []; let items = document.querySelectorAll('a.storylink'); items.forEach((item) => { results.push({ url: item.getAttribute('href'), text: item.innerText, }); }); return results; }); urls = urls.concat(newUrls); if (currentPage < pagesToScrape) { await Promise.all([ await page.waitForSelector('a.morelink'), await page.click('a.morelink'), await page.waitForSelector('a.storylink') ]) } currentPage++; } browser.close(); return resolve(urls); } catch (e) { return reject(e); } }) } run(5).then(console.log).catch(console.error);
ابق آمنًا مع حدود السعر
المتصفحات مقطوعة الرأس هي أدوات قوية للغاية. إنهم قادرون على أداء أي نوع من مهام أتمتة الويب تقريبًا ، ويجعل محرك العرائس هذا الأمر أسهل. على الرغم من كل الاحتمالات ، يجب علينا الامتثال لشروط الخدمة الخاصة بالموقع الإلكتروني للتأكد من عدم إساءة استخدام النظام.
نظرًا لأن هذا الجانب أكثر ارتباطًا بالعمارة ، فلن أغطي هذا بعمق في هذا البرنامج التعليمي Puppeteer. ومع ذلك ، فإن الطريقة الأساسية لإبطاء نص برمجي Puppeteer هي إضافة أمر سكون إليه:
js await page.waitFor(5000);
ستجبر هذه العبارة البرنامج النصي على السكون لمدة خمس ثوانٍ (5000 مللي ثانية). يمكنك وضع هذا في أي مكان قبل browser.close()
.
تمامًا مثل الحد من استخدامك لخدمات الطرف الثالث ، هناك الكثير من الطرق الأخرى الأكثر قوة للتحكم في استخدامك لمحرك الدمى. أحد الأمثلة على ذلك هو بناء نظام طابور مع عدد محدود من العمال. في كل مرة تريد فيها استخدام محرك الدمى ، ستقوم بدفع مهمة جديدة إلى قائمة الانتظار ، ولكن لن يكون هناك سوى عدد محدود من العمال القادرين على العمل على المهام الموجودة فيها. هذه ممارسة شائعة إلى حد ما عند التعامل مع حدود معدل واجهة برمجة التطبيقات لجهة خارجية ويمكن تطبيقها على تجريف بيانات الويب لمحرك الدمى أيضًا.
مكان محرّك الدمى في شبكة الويب سريعة الحركة
في هذا البرنامج التعليمي لمحرك العرائس ، لقد أوضحت وظائفه الأساسية كأداة تجريف على الويب. ومع ذلك ، فإنه يحتوي على حالات استخدام أوسع بكثير ، بما في ذلك اختبار المتصفح بدون رأس ، وإنشاء ملفات PDF ، ومراقبة الأداء ، من بين أشياء أخرى كثيرة.
تقنيات الويب تتقدم بسرعة. تعتمد بعض مواقع الويب بشكل كبير على عرض JavaScript بحيث يصبح من المستحيل تقريبًا تنفيذ طلبات HTTP البسيطة للتخلص منها أو إجراء نوع من الأتمتة. لحسن الحظ ، أصبح الوصول إلى المتصفحات بدون رأس أكثر وأكثر للتعامل مع جميع احتياجات الأتمتة لدينا ، وذلك بفضل مشاريع مثل Puppeteer والفرق الرائعة التي تقف وراءها!