نكتب الزاحف لواحد أو اثنين 1.0

يعد متتبع ارتباطات الويب (أو عنكبوت الويب) جزءًا مهمًا من محركات البحث لتتبع ارتباطات صفحات الويب من أجل إدخال معلومات عنها في قواعد البيانات ، وخاصةً لمزيد من الفهرسة. محركات البحث (Google و Yandex و Bing) ، وكذلك منتجات SEO (SEMrush و MOZ و ahrefs) وليس لديها مثل هذا الشيء فقط. وهذا الشيء مثير للاهتمام: سواء من حيث حالات الاستخدام أو الاستخدام ، والتنفيذ الفني.



باستخدام هذه المقالة ، سنبدأ في إنشاء دراجة الزاحف الخاصة بك بشكل متكرر ، وتحليل العديد من الميزات وعثرات الاجتماع. من وظيفة عودية بسيطة إلى خدمة قابلة للتوسعة وقابلة للتوسيع. يجب أن تكون مثيرة للاهتمام!

مقدمة


تكرارا - فهذا يعني أنه في نهاية كل إصدار ، من المتوقع إصدار نسخة جاهزة من "المنتج" مع القيود والميزات والواجهة المتفق عليها.

تم اختيار Node.js و JavaScript كمنصة ولغة ، لأنها بسيطة وغير متزامنة. بالطبع ، بالنسبة للتنمية الصناعية ، يجب أن يعتمد اختيار القاعدة التكنولوجية على متطلبات العمل والتوقعات والموارد. كتظاهرة ونموذج أولي ، فإن هذا النظام الأساسي ليس شيئًا (IMHO) تمامًا.

هذا هو الزاحف الخاص بي. هناك العديد من برامج الزحف هذه ، لكن هذا واحد هو لي.
الزاحف الخاص بي هو أفضل صديق لي.

يعد تنفيذ الزاحف مهمة شائعة إلى حد ما ويمكن العثور عليه حتى في المقابلات الفنية. هناك بالفعل الكثير من الحلول الجاهزة ( Apache Nutch ) والحلول المكتوبة ذاتيا لظروف مختلفة وفي العديد من اللغات. لذلك ، أي تعليقات من تجربة شخصية في التنمية أو الاستخدام هي موضع ترحيب وسوف تكون مثيرة للاهتمام.

بيان المشكلة


ستكون مهمة التنفيذ الأول (الأولي) لبرنامج الزاحف tyap-blooper كما يلي:

One-Two Crawler 1.0
اكتب نصًا زاحفًا يتخطى روابط <a href /> الداخلية لموقع صغير (حتى 100 صفحة). نتيجة لذلك ، قدم قائمة بعناوين URL للصفحات التي تحتوي على الرموز المستلمة وخريطة لربطها. يتم تجاهل قواعد robots.txt وسمة rel = nofollow link.

انتباه! يعد تجاهل قواعد ملف robots.txt فكرة سيئة لأسباب واضحة. سوف نعوض هذا الإغفال في المستقبل. في غضون ذلك ، أضف معلمة الحد التي تحد من عدد الصفحات التي سيتم الزحف إليها حتى لا تتوقف عن DoS وتجربة الموقع التجريبي (من الأفضل استخدام "موقع الهامستر" الشخصي الخاص بك لإجراء التجارب).

التنفيذ


لفارغ الصبر ، وهنا هي مصادر هذا الحل.

  1. عميل HTTP (S)
  2. خيارات الإجابة
  3. استخراج الارتباط
  4. إعداد وصلة وتصفية
  5. تطبيع URL
  6. خوارزمية الوظيفة الرئيسية
  7. عودة النتيجة

1. عميل HTTP (S)


أول ما نحتاج إلى القيام به هو ، في الواقع ، إرسال الطلبات وتلقي الردود عبر HTTP و HTTPS. في node.js يوجد عميلان متطابقان لهذا الغرض. بالطبع ، يمكنك أن تأخذ طلب عميل جاهزًا ، ولكن بالنسبة لمهمتنا فهو أمر ضروري للغاية: نحتاج فقط إلى إرسال طلب GET والحصول على استجابة من الجسم والرؤوس.

واجهة برمجة التطبيقات (API) لكلا العميلين اللذين نحتاجهما متطابقة ، وسننشئ خريطة:

const clients = { 'http:': require('http'), 'https:': require('https') }; 

نعلن عن إحضار دالة بسيطة ، والمعلمة الوحيدة منها هي عنوان URL المطلق لسلسلة موارد الويب المطلوبة. باستخدام وحدة url ، سنقوم بتحليل السلسلة الناتجة إلى كائن URL. يحتوي هذا الكائن على حقل مع البروتوكول (مع نقطتين) ، والذي سنقوم من خلاله باختيار العميل المناسب:

 const url = require('url'); function fetch(dst) { let dstURL = new URL(dst); let client = clients[dstURL.protocol]; if (!client) { throw new Error('Could not select a client for ' + dstURL.protocol); } // ... } 

بعد ذلك ، استخدم العميل المحدد ولف نتيجة دالة الجلب بوعد:

 function fetch(dst) { return new Promise((resolve, reject) => { // ... let req = client.get(dstURL.href, res => { // do something with the response }); req.on('error', err => reject('Failed on the request: ' + err.message)); req.end(); }); } 


الآن يمكننا تلقي استجابة بشكل غير متزامن ، لكن في الوقت الحالي لا نفعل أي شيء معها.

2. خيارات الإجابة


للزحف إلى الموقع ، يكفي معالجة 3 خيارات للإجابة:

  1. حسنا - تم استلام كود الحالة 2xx. من الضروري حفظ نص الاستجابة كنتيجة لمزيد من المعالجة - استخراج روابط جديدة.
  2. REDIRECT - تم استلام كود الحالة 3xx. هذا هو إعادة توجيه إلى صفحة أخرى. في هذه الحالة ، سنحتاج إلى عنوان استجابة الموقع ، حيث سنأخذ رابطًا واحدًا "صادرًا" واحدًا.
  3. NO_DATA - جميع الحالات الأخرى: 4xx / 5xx و 3xx بدون رأس الموقع . لا يوجد مكان للذهاب إلى الزاحف لدينا.

ستعمل وظيفة الجلب على حل الاستجابة المعالجة مع الإشارة إلى نوعها:

 const ft = { 'OK': 1, // code (2xx), content 'REDIRECT': 2, // code (3xx), location 'NO_DATA': 3 // code }; 

تنفيذ إستراتيجية توليد النتيجة في أفضل تقاليد if :

 let code = res.statusCode; let codeGroup = Math.floor(code / 100); // OK if (codeGroup === 2) { let body = []; res.setEncoding('utf8'); res.on('data', chunk => body.push(chunk)); res.on('end', () => resolve({ code, content: body.join(''), type: ft.OK })); } // REDIRECT else if (codeGroup === 3 && res.headers.location) { resolve({ code, location: res.headers.location, type: ft.REDIRECT }); } // NO_DATA (others) else { resolve({ code, type: ft.NO_DATA }); } 

وظيفة الجلب جاهزة للاستخدام: رمز الوظيفة بالكامل .

3. استخراج الروابط


الآن ، اعتمادًا على متغير الإجابة المستلمة ، يجب أن تكون قادرًا على استخراج الارتباطات من بيانات نتائج الجلب لمزيد من الزحف. للقيام بذلك ، نقوم بتعريف دالة الاستخراج ، والتي تأخذ كائن نتيجة كمدخلات وإرجاع مجموعة من الروابط الجديدة.

إذا كان نوع النتيجة REDIRECT ، فسوف تُرجع الدالة صفيفًا بمرجع واحد من حقل الموقع . إذا NO_DATA ، ثم مجموعة فارغة. إذا كان موافق ، فسنحتاج إلى توصيل المحلل اللغوي للمحتوى النصي المعروض للبحث.

بالنسبة إلى مهمة البحث <a href /> ، يمكنك أيضًا كتابة تعبير عادي. لكن هذا الحل لا يتغير على الإطلاق ، لأننا في المستقبل سنهتم على الأقل بالسمات الأخرى ( rel ) للرابط ، كحد أقصى ، سوف نفكر في img والرابط والنص البرمجي والصوت / الفيديو ( المصدر ) وغيرها من الموارد. يعد تحليل نص المستند أكثر وعدًا وأكثر ملاءمةً ، وبناء شجرة من العقد لتجاوز المحددات المعتادة.

سنستخدم مكتبة JSDOM الشائعة للعمل مع DOM في node.js:

 const { JSDOM } = require('jsdom'); let document = new JSDOM(fetched.content).window.document; let elements = document.getElementsByTagName('A'); return Array.from(elements) .map(el => el.getAttribute('href')) .filter(href => typeof href === 'string') .map(href => href.trim()) .filter(Boolean); 

نحصل على جميع العناصر A من الوثيقة ، ومن ثم كل القيم المصفاة لسمة href ، إن لم تكن الخطوط الفارغة.

4. إعداد وتصفية الروابط


نتيجة للمستخرج ، لدينا مجموعة من الروابط (عناوين URL) ومشكلتان: 1) يمكن أن يكون عنوان URL نسبيًا و 2) يمكن أن يؤدي عنوان URL إلى مورد خارجي (نحتاج فقط إلى روابط داخلية الآن).

سيتم مساعدة المشكلة الأولى عن طريق وظيفة url.resolve ، التي تحل عنوان URL للصفحة المقصودة نسبة إلى عنوان URL للصفحة المصدر.

لحل المشكلة الثانية ، نكتب وظيفة أداة بسيطة inScope تقوم بفحص مضيف الصفحة المقصودة مقابل مضيف عنوان URL الأساسي لعملية الزحف الحالية:

 function getLowerHost(dst) { return (new URL(dst)).hostname.toLowerCase(); } function inScope(dst, base) { let dstHost = getLowerHost(dst); let baseHost = getLowerHost(base); let i = dstHost.indexOf(baseHost); // the same domain or has subdomains return i === 0 || dstHost[i - 1] === '.'; } 

تبحث الوظيفة عن سلسلة فرعية ( baseHost ) مع تحديد للحرف السابق إذا تم العثور على السلسلة الفرعية: بما أن wwwexample.com و example.com مجالان مختلفان. نتيجة لذلك ، لا نترك المجال المحدد ، ولكن نتجاوز نطاقاته الفرعية.

نقوم بتحسين وظيفة الاستخراج بإضافة "مطلق" وتصفية الروابط الناتجة:

 function extract(fetched, src, base) { return extractRaw(fetched) .map(href => url.resolve(src, href)) .filter(dst => /^https?\:\/\//i.test(dst)) .filter(dst => inScope(dst, base)); } 

الجلب هنا هو نتيجة دالة الجلب ، src هو عنوان URL للصفحة المصدر ، والقاعدة هي عنوان URL الأساسي لعملية الزحف. في الإخراج ، نحصل على قائمة بالروابط الداخلية المطلقة (عناوين URL) لمزيد من المعالجة. يمكن رؤية رمز الوظيفة بالكامل هنا .

5. تطبيع URL


عند مواجهة أي عنوان URL ، لن تحتاج إلى إرسال طلب آخر للمورد ، نظرًا لأن البيانات قد تم استلامها بالفعل (أو أن اتصالًا آخر لا يزال مفتوحًا وينتظر الرد). ولكن لا يكفي دائمًا مقارنة سلاسل عنوانين URL لفهم ذلك. التطبيع هو الإجراء الضروري لتحديد تكافؤ عناوين URL المختلفة بشكل نحوي.

عملية التطبيع هي مجموعة كاملة من التحويلات المطبقة على عنوان URL المصدر ومكوناته. فيما يلي عدد قليل منها:

  • المخطط والمضيف ليست حساسة لحالة الأحرف ، لذلك يجب تحويلها إلى أقل.
  • يجب أن تكون جميع النسب المئوية (مثل "٪ 3A") كبيرة.
  • يمكن إزالة المنفذ الافتراضي (80 لـ HTTP).
  • الجزء ( # ) غير مرئي أبدًا على الخادم ويمكن أيضًا حذفه.

يمكنك دائمًا استخدام شيء جاهز (على سبيل المثال ، تطبيع عنوان url ) أو كتابة وظيفتك البسيطة التي تغطي أهم الحالات الشائعة:

 function normalize(dst) { let dstUrl = new URL(dst); // ignore userinfo (auth property) let origin = dstUrl.protocol + '//' + dstUrl.hostname; // ignore http(s) standart ports if (dstUrl.port && (!/^https?\:/i.test(dstUrl.protocol) || ![80, 8080, 443].includes(+dstUrl.port))) { origin += ':' + dstUrl.port; } // ignore fragment (hash property) let path = dstUrl.pathname + dstUrl.search; // convert origin to lower case return origin.toLowerCase() // and capitalize letters in escape sequences + path.replace(/%([0-9a-f]{2})/ig, (_, es) => '%' + es.toUpperCase()); } 

فقط في حالة تنسيق كائن URL


نعم ، لا يوجد فرز لمعلمات الاستعلام ، وتجاهل علامات utm ، ومعالجة _escaped_fragment_ وأشياء أخرى ، لا نحتاجها (مطلقًا) على الإطلاق.

بعد ذلك ، سننشئ ذاكرة تخزين مؤقت محلية لعناوين URL التي تم تطبيعها والتي يطلبها إطار الزحف. قبل إرسال الطلب التالي ، نقوم بتطبيع عنوان URL المستلم ، وإذا لم يكن في ذاكرة التخزين المؤقت ، فأضفه ثم أرسل طلبًا جديدًا فقط.

6. خوارزمية الوظيفة الرئيسية


المكونات الأساسية (البدائية) من الحل جاهزة ، لقد حان الوقت للبدء في جمع كل شيء معًا. بادئ ذي بدء ، دعنا نحدد توقيع وظيفة الزحف : عند الإدخال ، عنوان URL للبداية وحد الصفحة. ترجع الدالة وعدًا يوفر حله نتيجة متراكمة ؛ اكتبها إلى ملف الإخراج :

 crawl(start, limit).then(result => { fs.writeFile(output, JSON.stringify(result), 'utf8', err => { if (err) throw err; }); }); 

يمكن وصف أبسط سير عمل متكرر لوظيفة الزحف في الخطوات:

1. تهيئة ذاكرة التخزين المؤقت وكائن النتيجة
2. إذا لم يكن عنوان URL للصفحة المقصودة (عبر التطبيع ) في ذاكرة التخزين المؤقت ، فحينئذٍ
- 2.1. إذا تم الوصول إلى الحد ، END (انتظر النتيجة)
- 2.2. إضافة URL إلى ذاكرة التخزين المؤقت
- 2.3. حفظ الرابط بين المصدر والصفحة المقصودة في النتيجة
- 2.4. إرسال طلب غير متزامن لكل صفحة ( جلب )
- 2.5. إذا كان الطلب ناجحًا ، فعندئذٍ
- - 2.5.1. استخراج روابط جديدة من النتيجة ( استخراج )
- - 2.5.2. لكل رابط جديد ، قم بتنفيذ الخوارزمية 2-3
- 2.6. ELSE بمناسبة الصفحة كخطأ
- 2.7. حفظ بيانات الصفحة للنتيجة
- 2.8. إذا كانت هذه هي الصفحة الأخيرة ، أحضر النتيجة
3. عدا ذلك ، احفظ الرابط بين المصدر والصفحة المقصودة في النتيجة

نعم ، ستخضع هذه الخوارزمية لتغييرات كبيرة في المستقبل. الآن ، يتم استخدام حل تكراري بشكل متعمد على الجبهة ، لذلك في وقت لاحق من الأفضل أن "تشعر" بالفرق في التطبيقات. تبدو قطعة العمل لتنفيذ الوظيفة كما يلي:

 function crawl(start, limit = 100) { // initialize cache & result return new Promise((resolve, reject) => { function curl(src, dst) { // check dst in the cache & pages limit // save the link (src -> dst) to the result fetch(dst).then(fetched => { extract(fetched, dst, start).forEach(ln => curl(dst, ln)); }).finally(() => { // save the page's data to the result // check completion and resolve the result }); } curl(null, start); }); } 

يتم التحقق من تحقيق حد الصفحة من خلال عداد طلب بسيط. سيكون العداد الثاني - عدد الطلبات النشطة في كل مرة - بمثابة اختبار الجاهزية لإعطاء النتيجة (عندما تتحول القيمة إلى الصفر). إذا تعذر على وظيفة الجلب الحصول على الصفحة التالية ، فقم بتعيين كود الحالة على أنها خالية.

يمكنك (اختياريًا) التعرف على رمز التنفيذ هنا ، ولكن قبل ذلك يجب أن تفكر في تنسيق النتيجة التي تم إرجاعها.

7. عاد نتيجة


سنقدم معرف معرف فريدًا مع زيادة بسيطة للصفحات التي تم استطلاعها:

 let id = 0; let cache = {}; // ... let dstNorm = normalize(dst); if (dstNorm in cache === false) { cache[dstNorm] = ++id; // ... } 

للنتيجة ، سننشئ مجموعة من الصفحات التي سنضيف إليها كائنات تحتوي على بيانات على الصفحة: id {number} ، url {string} و code {number | null} (هذا يكفي الآن). نقوم أيضًا بإنشاء مجموعة من الروابط للارتباطات بين الصفحات في شكل كائن: من ( معرف الصفحة المصدر) إلى ( معرف الصفحة المقصودة).

لأغراض إعلامية ، قبل حل النتيجة ، نقوم بتصنيف قائمة الصفحات بترتيب تصاعدي من المعرف (بعد كل شيء ، ستأتي الإجابات بأي ترتيب) ، ونكمل النتيجة بعدد صفحات الفرز الممسوحة ضوئيًا وعلامة الوصول إلى الحد الأقصى المحدد للصفقة :

 resolve({ pages: pages.sort((p1, p2) => p1.id - p2.id), links: links.sort((l1, l2) => l1.from - l2.from || l1.to - l2.to), count, fin: count < limit }); 

مثال للاستخدام


يحتوي برنامج الزاحف النهائي على الملخص التالي:

 node crawl-cli.js --start="<URL>" [--output="<filename>"] [--limit=<int>] 

لاستكمال تسجيل النقاط الرئيسية للعملية ، سنرى هذه الصورة عند بدء التشغيل:

 $ node crawl-cli.js --start="https://google.com" --limit=20 [2019-02-26T19:32:10.087Z] Start crawl "https://google.com" with limit 20 [2019-02-26T19:32:10.089Z] Request (#1) "https://google.com/" [2019-02-26T19:32:10.721Z] Fetched (#1) "https://google.com/" with code 301 [2019-02-26T19:32:10.727Z] Request (#2) "https://www.google.com/" [2019-02-26T19:32:11.583Z] Fetched (#2) "https://www.google.com/" with code 200 [2019-02-26T19:32:11.720Z] Request (#3) "https://play.google.com/?hl=ru&tab=w8" [2019-02-26T19:32:11.721Z] Request (#4) "https://mail.google.com/mail/?tab=wm" [2019-02-26T19:32:11.721Z] Request (#5) "https://drive.google.com/?tab=wo" ... [2019-02-26T19:32:12.929Z] Fetched (#11) "https://www.google.com/advanced_search?hl=ru&authuser=0" with code 200 [2019-02-26T19:32:13.382Z] Fetched (#19) "https://translate.google.com/" with code 200 [2019-02-26T19:32:13.782Z] Fetched (#14) "https://plus.google.com/108954345031389568444" with code 200 [2019-02-26T19:32:14.087Z] Finish crawl "https://google.com" on count 20 [2019-02-26T19:32:14.087Z] Save the result in "result.json" 

وهنا النتيجة بتنسيق JSON:

 { "pages": [ { "id": 1, "url": "https://google.com/", "code": 301 }, { "id": 2, "url": "https://www.google.com/", "code": 200 }, { "id": 3, "url": "https://play.google.com/?hl=ru&tab=w8", "code": 302 }, { "id": 4, "url": "https://mail.google.com/mail/?tab=wm", "code": 302 }, { "id": 5, "url": "https://drive.google.com/?tab=wo", "code": 302 }, // ... { "id": 19, "url": "https://translate.google.com/", "code": 200 }, { "id": 20, "url": "https://calendar.google.com/calendar?tab=wc", "code": 302 } ], "links": [ { "from": 1, "to": 2 }, { "from": 2, "to": 3 }, { "from": 2, "to": 4 }, { "from": 2, "to": 5 }, // ... { "from": 12, "to": 19 }, { "from": 19, "to": 8 } ], "count": 20, "fin": false } 


ما الذي يمكن القيام به مع هذا بالفعل؟ كحد أدنى ، قائمة الصفحات التي يمكنك أن تجد جميع صفحات الموقع المكسورة. والحصول على معلومات حول الارتباط الداخلي ، يمكنك اكتشاف سلاسل طويلة (وحلقات مغلقة) من عمليات إعادة التوجيه أو العثور على أهم الصفحات حسب الكتلة المرجعية.

الإعلان 2.0


لقد حصلنا على متغير من أبسط برامج الزاحف التي تتجاوز صفحات موقع واحد. شفرة المصدر هنا . هناك أيضا مثال واختبارات وحدة لبعض الوظائف.

الآن هذا مرسل غير متجانس للطلبات وستكون الخطوة المعقولة التالية هي تعليمه سلوكًا جيدًا. سيكون حول رأس وكيل المستخدم ، وقواعد robots.txt ، وتوجيه تأخير الزحف ، والمزيد. من وجهة نظر التنفيذ ، هذا أولاً وقبل كل شيء يصطف في طوابير الرسائل ثم يخدم عبء أكبر. إذا ، بالطبع ، هذه المادة ستكون مثيرة للاهتمام!

Source: https://habr.com/ru/post/ar441024/


All Articles