ایجاد یک Web Scraper با استفاده از جاوااسکریپت

گردآوری و تالیف : ارسطو عباسی
تاریخ انتشار : 19 مرداد 1398
دسته بندی ها : جاوا اسکریپت

اگر قصد جمع‌آوری اطلاعات از دنیای وب را داشته باشید، منابع و ابزارهای بسیاری در اختیار شما قرار خواهد گرفت. زبان‌های برنامه‌نویسی مانند پایتون و PHP توانایی بالایی در پیاده‌سازی چنین کاری را دارند. اما برای انجام چنین کاری یک گزینه دیگر نیز وجود دارد که گزینه بسیار نزدیک‌تری به دنیای وب است، جاوااسکریپت!

به لطف نودجی‌اس، جاوااسکریپت به یک زبان قدرتمند برای فرایند Web Scraping تبدیل شده است. نه تنها استفاده از جاوااسکریپت در این فرایند سرعت کار را بالا می‌برد بلکه شما به صورت مستقیم قابلیت دسترسی به DOM را نیز خواهید داشت، این نکته بسیار مهمی است که زبان‌ها و تکنولوژی‌های دیگر از آن بی بهره هستند. 

در این مطلب قصد داریم تا از طریق جاوااسکریپت و با کمک گرفتن از قدرت نودجی‌اس یک Web Scraper را بسازیم. همچنین در ارتباط با یکی از مفهوم‌های بسیار مهم در جهت نوشتن برنامه‌ای برای دریافت اطلاعات صحبت می‌کنیم: Async.

برنامه‌نویسی Asynchronous

دریافت (Fetching = گرفتن) داده و اطلاعات یکی از کارهایی‌ست که برنامه‌نویسان مبتدی برای اولین بار در آن با برنامه‌نویسی غیر همزمان برخورد می‌کنند. به صورت پیشفرض جاوااسکریپت یک زبان همزمان است به این معنا که رویدادها به صورت خط به خط اجرا می‌شوند. هر زمانی که یک تابع فراخوانی می‌شود برنامه تا زمان اجرای آن تابع سراغ خط‌های بعدی نمی‌رود.

اما Fetch کردن داده‌ها به صورت مستقیم با موضوع برنامه‌نویسی غیر همزمان ارتباط دارد. برای پیاده‌سازی یک فرایند Fetching ما نیاز داریم که از توانایی‌های Async/Await در جاوااسکریپت استفاده کنیم. این سینتکس در ES7 معرفی شد. اما قبل از ادامه دادن به این موضوع بیایید با سیستم Callback آشنایی پیدا کنیم و سپس اهمیت موضوع Promiseها را برررسی نماییم.

Callback – Promise – Async/Await

قبل از اضافه کردن قابلیت برنامه‌نویسی غیر همزمانی به جاوااسکریپت، برای پیاده‌سازی شکلی از غیر همزمانی ما توابع را به صورت تو در تو داخل همدیگر می‌نوشتیم. این کار باعث می‌شد که یک Callback Hell یا جهنم Callback به وجود بیاید. در زیر می‌توانید یک مثال از این حالت را مشاهده کنید:

/* Passed-in Callbacks */
doSomething(function(result) {
  doSomethingElse(result, function(newResult) {
    doThirdThing(newResult, function(finalResult) {
      console.log(finalResult);
    }, failureCallback);
  }, failureCallback);
}, failureCallback);

اما در ES6 سینتکسی جدید ارائه شد که راهکاری ساده‌تر برای کدنویسی به صورت غیرهمزمان را فراهم می‌کرد. این حالت با استفاده از Promise و متدهای then و catch صورت می‌گرفت. اگر بخواهیم مثال بالا را با استفاده از این سینتکس ایجاد کنیم باید به صورت زیر عمل نماییم:

/* "Vanilla" Promise Syntax */
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
  console.log(finalResult);
})
.catch(failureCallback);

در نهایت، اکمااسکریپت ۷ با دو کلمه کلیدی async و await ارائه شد، سینتکسی جدید که ظاهر کدهای نوشته شده را به برنامه‌نویسی همزمان بسیار شبیه کرده بود. استفاده از این سینتکس خوانایی کدهای‌تان را بسیار بالا می‌برد. مثال زیر فرایند پیاده‌سازی نمونه‌های قبلی با استفاده از Async/Await است.

/* Async/Await Syntax */
(async () => {
  try {
    const result = await doSomething();
    const newResult = await doSomethingElse(result);
    const finalResult = await doThirdThing(newResult);
    console.log(finalResult); 
  } catch(err) {
    console.log(err);
  }
})();

وبسایت‌های ایستا

در گذشته برای بازیابی اطلاعات از یک وبسایت از XMLHttpRequest استفاده می‌کردیم. حال می‌توانیم از APIهای جاوااسکریپت که برای فرایند Fetching استفاده می‌شود، استفاده کنیم. متد fetch راهکاری است که در جاوااسکریپت ارائه شده، برای استفاده از این متد باید در قسمت ورودی آن یک url را قرار دهید. خروجی این متد یک Promise خواهد بود.

برای استفاده از fetch() در نودجی‌اس نیاز است که یکی از پیاده‌سازی‌های آن را در پروژه وارد کنید. Isomorphic Fetch یکی از انتخاب‌های محبوب برای این کار است. برای نصب آن کافی‌ست دستور npm install isomorphic-fetch es6-promise را در ترمینال وارد کنید. بعد از آن می‌توانید با دستور const fetch = require(‘isomorphic-fetch’) آن را به پروژه اضافه نمایید.

JSON

اگر قصد دریافت داده‌های JSON را دارید پس نیاز است که از متد json() بعد از ارائه پاسخ استفاده کنید. 

(async () => {
  const response = await fetch('https://wordpress.org/wp-json');
  const json = await response.json();
  console.log(JSON.stringify(json));
})()

استفاده کردن از داده‌های JSON و انجام عملیات parsing یکی از روش‌های بهینه برای دریافت اطلاعات از یک آدرس است. اما اگر آن آدرس حاوی اطلاعات JSON نبود چه؟

HTML

برای اغلب وبسایت‌ها شما باید اطلاعات مورد نظرتان را از یک قالب HTML استخراج کنید. برای انجام چنین کاری دو راهکار را در اختیار دارید.

راه اول: استفاده از عبارات با قاعده یا Regular Expressions

اگر خروجی مورد نظر شما چیزی ساده است و با نوشتن عبارات با قاعده آشنا هستید می‌توانید از متد text() استفاده کرده و بعد داده‌های‌تان را با استفاده از متد match استخراج نمایید. برای مثال در اینجا می‌توانید شیوه دریافت اطلاعات مربوط به اولین تگ h1 را در یک صفحه مشاهده کنید:

(async () => {
  const response = await fetch('https://example.com');
  const text = await response.text();
  console.log(text.match(/(?<=\<h1>).*(?=\<\/h1>)/));
})()

راه دوم: استفاده از DOM Parser

اگر راهکاری حرفه‌ای و پیچیده‌تر می‌خواهید باید سراغ یک ابزار DOM Parser بروید. اگر کدهای‌تان را در بخش Front-End می‌نویسید نیازی به استفاده از ماژول‌های دیگر نیست چرا که جاوااسکریپت به صورت محلی از DOM پشتیبانی می‌کند اما در این مثال ما قصد استفاده از نودجی‌اس را داریم به همین خاطر نیاز است که از یک ماژول استفاده کنیم. Jsdom گزینه‌ای مناسب برای انجام این کار است. می‌توانید با وارد کردن npm i jsdom در ترمینال آن را نصب کنید. حال برای افزودن آن به پروژه به صورت زیر عمل نمایید:

const jsdom = require("jsdom");
const { JSDOM } = jsdom;

با استفاده از jsdom می‌توانیم از HTML دریافت شده با استفاده از DOM کوئری بگیریم. برای مثال:

(async () => {
  const response = await fetch('https://example.com');
  const text = await response.text();
  const dom = await new JSDOM(text);
  console.log(dom.window.document.querySelector("h1").textContent);
})()

وبسایت‌های پویا

اگر قصد دریافت اطلاعات از یک وبسایت پویا و زنده را داشته باشید چه؟ منظورمان وبسایتی است که به صورت بلادرنگ بروزرسانی می‌شود، مانند یک شبکه اجتماعی. استفاده از fetch نمی‌تواند کارمان را راه بیاندازد به همین دلیل باید سراغ گزینه دیگری برویم.

بهترین ماژول نودجی‌اس برای انجام چنین کاری puppeteer است. این ماژول قابلیت‌هایی مانند automatic page navigation و یا screen capture را دارد. 

برای نصب این ماژول تنها کافی‌ست دستور npm i puppeteer را در ترمینال وارد کنید. حال برای اینکه یک شروع سریع داشته باشیم کدهای زیر را در یک فایل js قرار می‌دهیم:

const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
  headless: false,
});
const page = await browser.newPage();
await page.setRequestInterception(true);
await page.goto('http://www.example.com/');

برای مشاهده جزئیات کار ابتدا قابلیت headless را خاموش کرده‌ایم. سپس در یک صفحه جدید setRequestInterception را فراخوانی کرده‌ایم که البته این مورد اجباری نیست اما استفاده از آن به ما قابلیت به کارگیری متدهای abort، continue و respond را می‌دهد. در نهایت با استفاده از دستور goto آدرس مورد نظر را وارد کرده‌ایم.

در این ماژول نیز قابلیت استفاده از امکانات DOM را در اختیار داریم.

ورود

اگر قصد وارد شدن به یک وبسایت را داشته باشیم می‌توانیم از طریق متدهای type و click این کار را انجام دهیم. برای اجرا کردن این قابلیت‌ها نیز ماژول از قدرت DOM بهره می‌برد:

await page.type('#username', 'UsernameGoesHere');
await page.type('#password', 'PasswordGoesHere');
await page.click('button');
await page.waitForNavigation();

اسکرول بی‌نهایت

در شبکه‌های اجتماعی برای نمایش اطلاعات از اسکرول بی‌نهایت استفاده می‌شود. حال برای آنکه بتوانیم اسکرول بی‌نهایت را مدیریت کنیم و روی حساب‌ها اسکرول انجام دهیم می‌توانیم به صورت زیر عمل نماییم:

for (let j = 0; j < 5; j++) {
  await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
  await page.waitFor(1000);
}

البته از انجایی که زمان‌های بارگذاری می‌تواند متفاوت باشد، تعداد خروجی‌ها نیز در هر بار متفاوت است. می‌توان برای حل این مشکل میزان اسکرول را به یک مقدار معین محدود کنید.

بهینه‌سازی

راه‌هایی برای اعمال بهینه‌سازی در کدها وجود دارد که باعث سریع‌تر شدن جریان دسترسی به خروجی‌ها می‌شود. برای مثال اگر قصد بارگذاری نکردن تصاویر و فونت‌ها را در یک وبسایت داشته باشیم می‌توانیم به صورت زیر عمل نماییم:

await page.setRequestInterception(true);
page.on('request', (req) => {
 if (req.resourceType() == 'font' || req.resourceType() == 'image'){
   req.abort();
 }
 else {
   req.continue();
 }
});

می‌توانید قابلیت بارگذاری CSS را نیز درست به شیوه بالا غیرفعال کنید.

در پایان

در این مطلب سعی شد تا شما را با شیوه پیاده‌سازی یک Web Scraper در جاوااسکریپت آشنا کنیم. استفاده از چنین روشی می‌تواند بسیار کاربردی‌تر و آسان‌تر از روش‌های دیگر باشد. همچنین اگر قصد یادگیری کامل مفاهیمی مانند DOM، Async/Aawit و.. را دارید به شما پیشنهاد می‌کنم دوره‌های آموزشی زیر را دنبال کنید:

منبع

مقالات پیشنهادی

  • آیکون های فروشگاهی و بازاریابی

    در این پست لذت بخش من میخوام به شما یک مجموعه از آیکون های زیبا و ضررویه بازاریابی و فروشگاهی رو معرفی کنم که شامل +100 آیکون Swificons با 3 نوع مختلف...

    حسام موسوی