تکه تکه کردن (Web Scraping) وبسایت Scotch به روش Node.js

گردآوری و تالیف : عرفان کاکایی
تاریخ انتشار : 04 خرداد 1397
دسته بندی ها : نود جی اس

در طی سال های اخیر، تعداد زیادی فناوری و نمونه های طراحی جدید در زمینه وب پدید آمده اند. برخی زبان های برنامه نویسی در حال یافتن محبوبیت بیشتری هستند. احتمال این که مفاهیمی مانند طراحی تعاملی، برنامه های دو پلتفرمه موبایل/کامپیوتر، رندر کننده های سمت سرور، (SSR = Server-side Renderers) ساختار بدون سرور و... را شنیده باشید بالاست.

در حالی که بسیاری توسعه دهندگان وب مدرن می خواهند به سرعت این فناوری ها برسند، یک سری مفاهیم و تکنیک های وب هستند که محبوبیت کمتری دارند ولی بسیار کاربری هستند. یکی از آنان تکه تکه کردن (Web Scraping) است. در این آموزش، نگاهی به Web Scraping و روش هایی که می توانیم این تکنیک را اجرا کنیم خواهیم داشت.

تکه تکه کردن چیست؟

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

تکه تکه کردن ممکن است بسیار ناچیز به نظر برسد، اما تکنیکی است که بسیاری از ربات ها و افراد سرگردان در وب برای استخراج داده ها از آن استفاده می کنند. تکنیک های متفاوتی هستند که می توانند برای تکه تکه کردن به کار گرفته شوند. گرچه، در این آموزش، از تکنیکی که شامل DOM می شود استفاده خواهیم کرد.

جدول محتویات:

  1. پیش نیاز ها
  2. شروع کار
  3. توابع کمکی
  4. آماده شدن برای تکه تکه کردن Scotch
  5. توابع استخراج Scotch
  6. استخراج پروفایل نویسنده Scotch
  7. پایان دادن کار با یک Route
  8. نتیجه گیری

تکه تکه کردن Scotch

در این آموزش، از تکه تکه کردن برای استخراج داده هایی از وبسایت Scotch استفاده خواهیم کرد. Scotch از هیچ API ای برای گرفتن پروفایل یا آموزش ها/پست های نویسندگان استفاده نمی کند. حال، ما می خواهیم یک API ساده برای گرفتن پروفایل یا آموزش ها/پست های نویسندگان Scotch بسازیم.

در اینجا اسکرینشاتی از یک برنامه بر پایه API ای که ما می خواهیم بسازیم را میبینید. می توانید برنامه را در Heroku و سورس کد را در گیت هاب ببینید.

پیش نیاز ها:

سینتکس پیشرفته جاوا اسکریپت و ES6

تکه تکه کردن به طور مجازی می تواند در هر زبان برنامه نویسی ای که parse کردن HTTP، XML یا DOM را پشتیبانی کند، انجام شود. در این آموزش، بر روی تکه تکه کردن با استفاده از محیط جاوا اسکریپت و Node.js تمرکز می کنیم. از این رو، داشتن دانش بالا در زمینه جاوا اسکریپت برای درک کامل این کد لازم است.

همچنین در این آموزش، استفاده زیادی از ES6 می شود. آشنایی کلی با ES6 نیز برای درک کامل این کد لازم است.

همچنین برخی قابلیت های ED7 مانند async function و عامل await را استفاده خواهیم کرد. از این رو، دانش در زمینه توابع ناهمگام (Asynchronous Functions) و کار با Promise ها نیز لازم است.

آشنایی با jQuery

کمی اشنایی با کتابخانه DOM جی کوئری برای درک کامل بخش هایی از این کد نیز لازم است، زیرا در این آموزش از پکیج Cheerio استفاده خواهیم کرد که بر پایه DOM است.

برنامه نویسی تابعی

در این آموزش، از الگو های برنامه نویسی تابعی برای ساخت API مورد نظر استفاده خواهیم کرد. به این صورت، چندین تابع خاص خواهیم نوشت، و همچنین از برخی مفاهیم برنامه نویسی مانند Pure function، Hiher-order functions و Composition استفاده خواهیم کرد. از این رو، اگر داشنشی در زمینه برنامه نویسی تابعی داشته باشید، کارتان راحت تر است.

وابستگی های (Dependency) هسته ای

قبل از شروع، مطمئن شوید که Node، npm، یا yarn را بر روی سیستم خود نصب دارید. از آنجایی که در این آموزش تعداد زیادی سینتکس های ES6/7 را استفاده خواهیم کرد، پیشنهاد می کنم از این نسخه های Node و npm برای پشتیبانی کامل استفاده کنید: Node 8.9.0 یا بالاتر، و npm 5.2.0 یا بالاتر.

این ها پکیج هایی هستند که استفاده خواهیم کرد:

  1. Cheerio: Cherioo سریع، انعطاف پذیر و پیاده سازی بی نظیر هسته jQuey است که به خصوص برای سرور طراحی شده است. این پکیج parse کردن های در DOM را آسان تر می کند.
  2. Axios: Axios یک کلاینت HTTP خوب برای مرورگر و Node.js است. این پکیج، ما را قادر می سازد تا محتویات صفحات را از طریق درخواست های HTTP بگیریم.
  3. Express: Express یک برنامه تحت وب نوشته شده با Node.js است که امکانات قدرتمندی برای برنامه های وب و موبایل فراهم می کند.
  4. Lodash: Lodash یک کتابخانه کاربردی جاوا اسکریپت است، که کار با جاوا اسکریپت را با کوتاه تر کردن کار با آرایه ها، اعداد و متغیر های نوع string و ... آسان تر می کند.

شروع کار:

نصب وابستگی ها (Dependencies)

یک پوشه جدید برای برنامه بسازید و از این کد برای نصب وابستگی های مورد نیاز برای برنامه استفاده کنید:

# Create a new directory
mkdir scotch-scraping

# cd into the new directory
cd scotch-scraping

# Initiate a new package and install app dependencies
npm init -y
npm install express morgan axios cheerio lodash

راه اندازی برنامه تحت سرور با Express

حال با استفاده از Express یک برنامه تحت سرور را راه اندازی می کنیم. یک فایل server.js در پوشه ریشه برنامه خود بسازید و این کد را برای راه اندازی سرور اجرا کنید:

/* server.js */

// Require dependencies
const logger = require('morgan');
const express = require('express');

// Create an Express application
const app = express();

// Configure the app port
const port = process.env.PORT || 3000;
app.set('port', port);

// Load middlewares
app.use(logger('dev'));

// Start the server and listen on the preconfigured port
app.listen(port, () => console.log(`App started on port ${port}.`));


بخش اسکریپت npm را اصلاح کنید.

در آخر، ما بخش scripts فایل package.json را اصلاح می کنیم، تا به این شکل در آید:

"scripts": {
  "start": "node server.js"
}

حال، تمام موارد مورد نیاز برای ساخت برنامه مان را داریم. اگر دستور npm start را در ترمینال خود اجرا کنید، برنامه تحت سرور اگر پورت 3000 در دسترس باشد، اجرا می شود. گرچه، فعلا به هیچ route ای نمی توانیم دسترسی داشته باشیم، زیرا هنوز آنها را به برنامه اضافه نکرده ایم. بیایید یک سری توابع کمکی را که برای تکه تکه کردن نیاز داریم بسازیم.

توابع کمکی:

همانطور که قبلا اشاره شد، یک سری توابع کمکی خواهیم ساخت که در بخش های مختلفی از برنامه مان استفاده خواهیم کرد. یک پوشه app در پوشه ریشه پروژه تان ایجاد کنید. یک فایل جدید به نام helpers.js در پوشه app که ساختید ایجاد کنید و این محتویات را به آن اضافه کنید:

/* app/helpers.js */

const _ = require('lodash');
const axios = require("axios");
const cheerio = require("cheerio");

همانطور که میبینید، به سادگی وابستگی هایی را که برای توابع کمکی مان نیاز خواهیم داشت را می آوریم. حال بیایید توابع کمکی را اضافه کنیم.

توابع کمکی کاربردی

با ساخت یک سری توابع کمکی کاربردی شروع می کنیم. این بخش را به فایل app/helpers.js اضافه کنید:

/* app/helpers.js */

///////////////////////////////////////////////////////////////////////////////
// UTILITY FUNCTIONS
///////////////////////////////////////////////////////////////////////////////

/**
 * Compose function arguments starting from right to left
 * to an overall function and returns the overall function
 */
const compose = (...fns) => arg => {
  return _.flattenDeep(fns).reduceRight((current, fn) => {
    if (_.isFunction(fn)) return fn(current);
    throw new TypeError("compose() expects only functions as parameters.");
  }, arg);
};

/**
 * Compose async function arguments starting from right to left
 * to an overall async function and returns the overall async function
 */
const composeAsync = (...fns) => arg => {
  return _.flattenDeep(fns).reduceRight(async (current, fn) => {
    if (_.isFunction(fn)) return fn(await current);
    throw new TypeError("compose() expects only functions as parameters.");
  }, arg);
};

/**
 * Enforces the scheme of the URL is https
 * and returns the new URL
 */
const enforceHttpsUrl = url =>
  _.isString(url) ? url.replace(/^(https?:)?\/\//, "https://") : null;

/**
 * Strips number of all non-numeric characters
 * and returns the sanitized number
 */
const sanitizeNumber = number =>
  _.isString(number)
    ? number.replace(/[^0-9-.]/g, "")
    : _.isNumber(number) ? number : null;

/**
 * Filters null values from array
 * and returns an array without nulls
 */
const withoutNulls = arr =>
  _.isArray(arr) ? arr.filter(val => !_.isNull(val)) : [];

/**
 * Transforms an array of ({ key: value }) pairs to an object
 * and returns the transformed object
 */
const arrayPairsToObject = arr =>
  arr.reduce((obj, pair) => ({ ...obj, ...pair }), {});

/**
 * A composed function that removes null values from array of ({ key: value }) pairs
 * and returns the transformed object of the array
 */
const fromPairsToObject = compose(arrayPairsToObject, withoutNulls);

حال بیایید توابع را یک به یک بررسی کنیم و ببینیم هر کدام چه کاری می کنند.

Compose() : این یک تابع سطح بالا است، که یک یا چند تابع را به عنوان آرگومان های خود میگیرد و تابع تشکیل شده را بر میگرداند. تابع تشکیل شده تاثیری مشابه انتقال دادن یک تابع یک به یک میان توابع، و از راست به چپ دارد.

اگر هر آرگومان که به compose() رسیده باشد یک تابع نباشد، تابع تشکیل شده یک ارور را بر می گرداند. این کد نشان می دهد compose چگونه کار می کند:

/**
* -------------------------------------------------
* Method 1: Functions in sequence
* -------------------------------------------------
*/
function1( function2( function3(arg) ) );

/**
* -------------------------------------------------
* Method 2: Using compose()
* -------------------------------------------------
* Invoking the composed function has the same effect as (Method 1)
*/
const composedFunction = compose(function1, function2, function3);

composedFunction(arg);

composeAsync(): این تابع درست به مانند تابع compose() کار می کند. تنها فرق موجود، ناهمگام بودن این تابع است. از این رو، برای تشکیل توابعی که رفتار ناهمگام دارند، مانند توابعی که promise ها را بر می گردانند، مناسب است.

EnforceHttpsUrl (): این تابع یک رشته url را به عنوان آرگومان خود می گیرد، و url  را با یک https در اول آن بر می گرداند. البته می تواند به صورت https:// و http:// نیز باشد. اگر url یک رشته نباشد نیز، null (خالی) باز گردانده می شود. مثال:

enforceHttpsUrl('scotch.io'); // returns => 'scotch.io'
enforceHttpsUrl('//scotch.io'); // returns => 'https://scotch.io'
enforceHttpsUrl('http://scotch.io'); // returns => 'https://scotch.io'

sanitizeNumber() : این تابع انتظار دریافت یک عدد یا رشته را به عنوان آرگومان خود دارد. اگر یک عدد به آن ارسال شده باشد، همان عدد را بر می گرداند. گرچه، اگر یک رشته به آن فرستاده شود، موارد غیر عددی را از آن رشته حذف می کند و رشته تصفیه شده را باز می گرداند. مثال:

sanitizeNumber(53.56); // returns => 53.56
sanitizeNumber('-2oo,40'); // returns => '-240'
sanitizeNumber('badnumber.decimal'); // returns => '.'

withoutNulls(): این تابع یک آرایه را به عنوان آرگومان خود میگیرد، و یک آرایه جدید که تنها شامل موارد غیر خالی (not-null) آرایه اصلی می شود را بر می گرداند. مثال:

withoutNulls([ 'String', [], null, {}, null, 54 ]); // returns => ['String', [], {}, 54]

arrayPairsToObject(): این تابع یک آرایه از نوع { key: value } را میگیرد، و یک آبجکت تغییر شکل داده شده را بر می گرداند. مثال:

const pairs = [ { key1: 'value1' }, { key2: 'value2' }, { key3: 'value3' } ];

arrayPairsToObject(pairs); // returns => { key1: 'value1', key2: 'value2', key3: 'value3' }

fromPairsToObject(): این یک تابع تشکیل شده با استفاده از تابع compose() است. این تابع تاثیری مانند اجرای کد زیر دارد:

arrayPairsToObject( withoutNulls(array) );

توابع کمکی Request و Response

این کد را به فایل app/helpers.js اضافه کنید:

/* app/helpers.js */

/**
 * Handles the request(Promise) when it is fulfilled
 * and sends a JSON response to the HTTP response stream(res).
 */
const sendResponse = res => async request => {
  return await request
    .then(data => res.json({ status: "success", data }))
    .catch(({ status: code = 500 }) =>
      res.status(code).json({ status: "failure", code, message: code == 404 ? 'Not found.' : 'Request failed.' })
    );
};

/**
 * Loads the html string returned for the given URL
 * and sends a Cheerio parser instance of the loaded HTML
 */
const fetchHtmlFromUrl = async url => {
  return await axios
    .get(enforceHttpsUrl(url))
    .then(response => cheerio.load(response.data))
    .catch(error => {
      error.status = (error.response && error.response.status) || 500;
      throw error;
    });
};

در اینجا، دو تابع جدید را اضافه کرده ایم: sendResponse() و fetchHtmlFromUrl(). بیایید ببینیم این توابع چه کاری انجام می دهند.

SendResponse(): این یک تابع سطح بالا است که یک آدرس HTTP را به عنوان آرگومان خود میگیرد، و تابع async را بر می گرداند. تابع async باز گرداننده شده، انتظار یک promise یا thenable را به عنوان آرگومان خود دارد.

اگر request promise موفقیت آمیز باشد، یک پاسخ JSON را با استفاده از res.json() می فرستد، که شامل داده های به دست آمده است. حال اگر promise موفقیت آمیز نباشد، یک خطای JSON به همراه یک HTTP صحیح فرستاده می شود. این تابع می تواند به این صورت در یک route استفاده شود:

app.get('/path', (req, res, next) => {
  const request = Promise.resolve([1, 2, 3, 4, 5]);
  sendResponse(res)(request);
});

ایجاد یک درخواست GET به شاخه path این پاسخ JSON را بر می گرداند:

{
  "status": "success",
  "data": [1, 2, 3, 4, 5]
}

FetchHtmlFromUrl(): این تابع، یک تابع async است که یک انتظار یک رشته url را به عنوان آرگومان خود دارد. در ابتدا، از axios.get() برای گرفتن محتویات url (که یک promise را بر می گرداند) استفاده می کند. اگر این promise موفقیت آمیز باشد، از تابع Cheerio.load() برای ساخت نماد parse کننده Cheerio استفاده می کند، و سپس نماد را بر می گرداند. گرچه، اگر این promise موفقیت آمیز نباشد، یک خطا بر می گرداند.

نماد parse کننده Cheerio که با این تابع برگردانده شده است، به ما اجازه می دهد که داده های مورد نظر خود را استخراج کنیم. می توانیم از آن به روش های مشابهی که در jQuey استفاده می کنیم، استفاده کنیم.

تابع کمکی DOM Parsing

حال بیایید یک سری تابع اضاف کنیم که به ما در parse کردن DOM کمک می کنند. این محتویات را به فایل app/helpers.js اضافه کنید:

/* app/helpers.js */

///////////////////////////////////////////////////////////////////////////////
// HTML PARSING HELPER FUNCTIONS
///////////////////////////////////////////////////////////////////////////////

/**
 * Fetches the inner text of the element
 * and returns the trimmed text
 */
const fetchElemInnerText = elem => (elem.text && elem.text().trim()) || null;

/**
 * Fetches the specified attribute from the element
 * and returns the attribute value
 */
const fetchElemAttribute = attribute => elem =>
  (elem.attr && elem.attr(attribute)) || null;

/**
 * Extract an array of values from a collection of elements
 * using the extractor function and returns the array
 * or the return value from calling transform() on array
 */
const extractFromElems = extractor => transform => elems => $ => {
  const results = elems.map((i, element) => extractor($(element))).get();
  return _.isFunction(transform) ? transform(results) : results;
};

/**
 * A composed function that extracts number text from an element,
 * sanitizes the number text and returns the parsed integer
 */
const extractNumber = compose(parseInt, sanitizeNumber, fetchElemInnerText);

/**
 * A composed function that extracts url string from the element's attribute(attr)
 * and returns the url with https scheme
 */
const extractUrlAttribute = attr =>
  compose(enforceHttpsUrl, fetchElemAttribute(attr));

module.exports = {
  compose,
  composeAsync,
  enforceHttpsUrl,
  sanitizeNumber,
  withoutNulls,
  arrayPairsToObject,
  fromPairsToObject,
  sendResponse,
  fetchHtmlFromUrl,
  fetchElemInnerText,
  fetchElemAttribute,
  extractFromElems,
  extractNumber,
  extractUrlAttribute
};

یک سری تابع دیگر را نیز اضافه کردیم. حال ببینیم که این توابع چه کاری انجام می دهند:

FetchElemInnerText(): این تابع یک عنصر را به عنوان آرگومان خود انتظار دارد. این تابع متن داخلی عنصر را با صدا کردن تابع elem.text() استخراج می کند، اسپیس های اضافی اطراف آن را پاک می کند، و متن پایانی را بر می گرداند. مثال:

const $ = cheerio.load('<div class="fullname">  Glad Chinda </div>');
const elem = $('div.fullname');

fetchElemInnerText(elem); // returns => 'Glad Chinda'

fetchElemAttribute(): این یک تابع سطح بالا است که یک صفت را به عنوان آرگومان خود انتظار دارد، و خود یک تابع مشابه را بر می گرداند. تابع باز گردانده شده مقدار صفت داده شده عنصر را با صدا کردن تابع elem.attr(attribute) بر می گرداند. مثال:

const $ = cheerio.load('<div class="username" title="Glad Chinda">@gladchinda</div>');
const elem = $('div.username');

// fetchTitle is a function that expects an element as argument
const fetchTitle = fetchElemAttribute('title');

fetchTitle(elem); // returns => 'Glad Chinda'

extractFromElems(): این یک تابع بزرگ است که یک کار کوچک را انجام می دهد. یک تابع سطح بالا که یک تابع سطح بالای دیگر را بر می گرداند. در اینجا، ما از یک تکنیک برنامه نویسی تابعی به نام currying برای ایجاد یک سری توابع دیگر که هر کدام تنها یک آرگومان دارند، استفاده کرده ایم. لیست آرگومان ها را در زیر مشاهده می کنید:

extractorFunction -> transformFunction -> elementsCollection -> cheerioInstance

تابع extractFromElems() استخراج داده ها از مجموعه ای از عناصر مشابه با استفاده از تابع extractor، و همچنین تغییر شکل دادن داده های استخراج شده با استفاده از تابع transform  را میسر می سازد. تابع extractor یک عنصر را به عنوان یک آرگومان دریافت می کند، در حالی که تابع transform آرایه ای از مقادیر را به عنوان آرگومان های خود دریافت می کند.

فرض کنید مجموعه ای از عناصر را داریم، و هر کدام نام یک شخص را در خود دارند. می خواهیم تمام این نام ها را استخراج کنیم و پس از تبدیل به حروف بزرگ، در یک آرایه قرار دهیم. در اینجا مثالی برای نحوه استفاده از تابع extractFromElems() زده شده است:

const $ = cheerio.load('<div class="people"><span>Glad Chinda</span><span>John Doe</span><span>Brendan Eich</span></div>');

// Get the collection of span elements containing names
const elems = $('div.people span');

// The transform function
const transformUpperCase = values => values.map(val => String(val).toUpperCase());

// The arguments sequence: extractorFn => transformFn => elemsCollection => cheerioInstance($)
// fetchElemInnerText is used as extractor function
const extractNames = extractFromElems(fetchElemInnerText)(transformUpperCase)(elems);

// Finally pass in the cheerioInstance($)
extractNames($); // returns => ['GLAD CHINDA', 'JOHN DOE', 'BRENDAN EICH']

extractNumber(): این یک است که یک عنصر را به عنوان آرگومان خود می گیرد، و تلاش می کند عددی را از محتوای داخلی آن استخراج کند. این کار را با استفاده از توابع parseInt()، sanitizeNumber() و fetchElemInnerText() انجام می دهد و نتیجه ای مانند اجرای دستور زیر دارد:

parseInt( sanitizeNumber( fetchElemInnerText(elem) ) );

extractUrlAttribute(): این یک تابع سطح بالا است که یک صفت را به عنوان آرگومان خود انتظار دارد، و خود یک تابع مشابه را باز می گرداند. تابع باز گردانده شده، تلاش می کند تا URL داده شده را به همراه https باز گرداند. مثالی در زیر زده شده است که نحوه کار آن را نشان می دهد:

// METHOD 1
const fetchAttribute = fetchElemAttribute(attr);
enforceHttpsUrl( fetchAttribute(elem) );

// METHOD 2: Using extractUrlAttribute()
const fetchUrlAttribute = extractUrlAttribute(attr);
fetchUrlAttribute(elem);

در آخر، تمام توابعی که ساخته ایم را با استفاده از mosule.exports خروجی می گیریم. حال که تمام توابع خود را داریم، می توانیم به بخش تکه تکه کردن این آموزش برویم.

آماده شدن برای تکه تکه کردن Scotch:

فایل جدیدی به نام scotch.js در شاخه app پروژه خود بسازید و محتویات زیر را به آن اضافه کنید:

/* app/scotch.js */

const _ = require('lodash');

// Import helper functions
const {
  compose,
  composeAsync,
  extractNumber,
  enforceHttpsUrl,
  fetchHtmlFromUrl,
  extractFromElems,
  fromPairsToObject,
  fetchElemInnerText,
  fetchElemAttribute,
  extractUrlAttribute
} = require("./helpers");

// scotch.io (Base URL)
const SCOTCH_BASE = "https://scotch.io";

///////////////////////////////////////////////////////////////////////////////
// HELPER FUNCTIONS
///////////////////////////////////////////////////////////////////////////////

/**
 * Resolves the url as relative to the base scotch url
 * and returns the full URL
 */
const scotchRelativeUrl = url =>
  _.isString(url) ? `${SCOTCH_BASE}${url.replace(/^\/*?/, "/")}` : null;

/**
 * A composed function that extracts a url from element attribute,
 * resolves it to the Scotch base url and returns the url with https
 */
const extractScotchUrlAttribute = attr =>
  compose(enforceHttpsUrl, scotchRelativeUrl, fetchElemAttribute(attr));

همانطور که می توانید ببینید، lodash و برخی دیگر از توابعی که پیش تر ساختیم را وارد کردیم. همچنین constant ای به نام SCOTCH_BASE را تعریف کردیم که شامل URL اصلی وبسایت Scotch می شود. در آخر، دو تابع کمکی را اضافه کردیم:

ScotchRelativeUrl(): این تابع یک رشته url را می گیرد و با آن را با اعمال SOCTH_BASE بر می گرداند. اگر URL یک رشته نباشد، خالی (null) باز گردانده می شود. مثال:

scotchRelativeUrl('tutorials'); // returns => 'https://scotch.io/tutorials'
scotchRelativeUrl('//tutorials'); // returns => 'https://scotch.io///tutorials'
scotchRelativeUrl('http://domain.com'); // returns => 'https://scotch.io/http://domain.com'

extractScotchAttribute(): این یک تابع سطح بالا است که یک صفت را به عنوان آرگومان خود می گیرد، و تابعی باز می گرداند که یک عنصر را به عنوان آرگومان خود می گیرد. تابع باز گردانده شده، تلاش می کند تا مقدار URL صفتی در عنصر را پس از اعمال SCOTH_BASE به آن استخراج کند، سپس آن را به همراه یک https بر می گرداند. این تکه کد نشان می دهد که این تابع چگونه کار می کند:

// METHOD 1
const fetchAttribute = fetchElemAttribute(attr);
enforceHttpsUrl( scotchRelativeUrl( fetchAttribute(elem) ) );

// METHOD 2: Using extractScotchUrlAttribute()
const fetchUrlAttribute = extractScotchUrlAttribute(attr);
fetchUrlAttribute(elem);

توابع استخراج Scotch:

ما می خواهیم داده های زیر را برای هر یک از نویسنده های Scotch به دست بیاوریم:

. پروفایل (نام، نقش، آواتار و ...)

. لینک شبکه های اجتماعی (Facebook، Twitter، Github و ...)

. وضعیت (بازدید ها، تعداد پست ها و ...)

. پست ها

اگر به یاد داشته باشید، تابع کمکی extractFromElems() که پیش تر آن را ساختیم، یک تابع استخراج کننده برای استخراج محتویات است، که مجموعه ای از عناصر مشابه را نیاز دارد. در این بخش می خواهیم توابع استخراج کننده را تعریف کنیم.

استخراج لینک شبکه های اجتماعی

در ابتدا، یک تابع به نام extractSocialUrl() برای استخراج نام شبکه های اجتماعی و یو آر ال <a> آنها می سازیم. ساختار DOM لینک شبکه های اجتماعی عنصر <a> که extractSocialUrl() می خواهد به این شکل است:

<a href="https://github.com/gladchinda" target="_blank" title="GitHub">
  <span class="icon icon-github">
    <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" width="50" height="50" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
      ...
    </svg>
  </span>
</a>

صدا کردن تابع extractSocialUrl() باید آبجکتی که به شکل زیر است را برگرداند:

{ github: 'https://github.com/gladchinda' }

حال بیایید این تابع را بسازیم. این محتویات را به فایل app/scotch.js اضافه کنید:

/* app/scotch.js */

///////////////////////////////////////////////////////////////////////////////
// EXTRACTION FUNCTIONS
///////////////////////////////////////////////////////////////////////////////

/**
 * Extract a single social URL pair from container element
 */
const extractSocialUrl = elem => {

  // Find all social-icon <span> elements
  const icon = elem.find('span.icon');

  // Regex for social classes
  const regex = /^(?:icon|color)-(.+)$/;

  // Extracts only social classes from the class attribute
  const onlySocialClasses = regex => (classes = '') => classes
      .replace(/\s+/g, ' ')
      .split(' ')
      .filter(classname => regex.test(classname));

  // Gets the social network name from a class name
  const getSocialFromClasses = regex => classes => {
    let social = null;
    const [classname = null] = classes;

    if (_.isString(classname)) {
      const [, name = null] = classname.match(regex);
      social = name ? _.snakeCase(name) : null;
    }

    return social;
  };

  // Extract the href URL from the element
  const href = extractUrlAttribute('href')(elem);

  // Get the social-network name using a composed function
  const social = compose(
    getSocialFromClasses(regex),
    onlySocialClasses(regex),
    fetchElemAttribute('class')
  )(icon);

  // Return an object of social-network-name(key) and social-link(value)
  // Else return null if no social-network-name was found
  return social && { [social]: href };

};

بیایید ببینیم تابع extractSocialUrl() چگونه کار می کند:

1-ابتدا، ما عنصر <span> را با استفاده از کلاس icon می آوریم.

2-یک تابع سطح بالا به نام onlySocialClasses() تعریف می کنیم که یک عبارت معمولی را به عنوان آرگومان خود می گیرد و یک تابع را باز می گرداند. تابع باز گردانده شده، رشته ای از نام کلاس ها را که با یک اسپیس از هم جدا شده اند را می گیرد. سپس از این عبارت ساده استفاده می کند تا نام های کلاس social را استخراج کند و در یک آرایه باز گرداند. مثال:

const regex = /^(?:icon|color)-(.+)$/;
const extractSocial = onlySocialClasses(regex);
const classNames = 'first-class another-class color-twitter icon-github';
extractSocial(classNames); // returns [ 'color-twitter', 'icon-github' ]

3-سپس، یک تابع سطح بالا به نام getSocialFromClasses() می سازیم، که یک عبارت ساده را به عنوان آرگومان خود می گیرد و یک تابع را باز می گرداند. تابع بازگردانده شده، آرایه ای از رشته ها می گیرد. سپس از عبارت ساده برای استخراج نام شبکه از اولین کلاس استفاده می کند و آن را بر می گرداند. مثال:

const regex = /^(?:icon|color)-(.+)$/;
const extractSocialName = getSocialFromClasses(regex);
const classNames = [ 'color-twitter', 'icon-github' ];

extractSocialName(classNames); // returns 'twitter'

4-سپس، صفت href را از عنصر استخراج می کنیم. همچنین نام شبکه اجتماعی را از عنصر <span> با استفاده از یک تابع تشکیل شده از ترکیب توابع getSocialFromClasses(regex)، onlySocialClasses(regex) و fetchElemAttribute(‘class’) به وجود آمده است، استخراج می کنیم.

5-در آخر، یک آبجکت به همراه نام شبکه اجتماعی به عنوان کلید و یو آر ال href به عنوان مقدار را باز می گردانیم. گرچه، اگر هیچ شبکه اجتماعی ای آورده نشده باشد، مقدار خالی (null) برگردانده می شود. مثال:

{ twitter: 'https://twitter.com/gladchinda' }

استخراج پست ها و وضعیت

حال دو تابع دیگر به نام های extractPost() و extractStat() برای استخراج پست ها و وضعیت به ترتیب ایجاد می کنیم. قبل از ایجاد توابع، بیایید نگاهی به ساختار DOM عنصری که توسط توابع خواسته شده است بیندازیم.

این ساختار DOM عنصری است که توسط تابع extractPost() خواسته شده است:

<div class="card large-card" data-type="post" data-id="2448">
  <a href="/tutorials/password-strength-meter-in-angularjs" class="card__img lazy-background" data-src="https://cdn.scotch.io/7540/iKZoyh9WSlSzB9Bt5MNK_post-cover-photo.jpg">
    <span class="tag is-info">Post</span>
  </a>
  <h2 class="card__title">
    <a href="/tutorials/password-strength-meter-in-angularjs">Password Strength Meter in AngularJS</a>
  </h2>
  <div class="card-footer">
    <a class="name" href="/@gladchinda">Glad Chinda</a>
    <a href="/tutorials/password-strength-meter-in-angularjs" title="Views">
      ?️ <span>24,280</span>
    </a>
    <a href="/tutorials/password-strength-meter-in-angularjs#comments-section" title="Comments">
      ? <span class="comment-number" data-id="2448">5</span>
    </a>
  </div>
</div>

و این نیز ساختار DOM عنصری است که توسط تابع extractstat() خواسته شده است:

<div class="profile__stat column is-narrow">
  <div class="stat">41,454</div>
  <div class="label">Pageviews</div>
</div>

این محتویات را به فایل app/scotch.js اضافه کنید:

/* app/scotch.js */

/**
 * Extract a single post from container element
 */
const extractPost = elem => {
  const title = elem.find('.card__title a');
  const image = elem.find('a[data-src]');
  const views = elem.find("a[title='Views'] span");
  const comments = elem.find("a[title='Comments'] span.comment-number");

  return {
    title: fetchElemInnerText(title),
    image: extractUrlAttribute('data-src')(image),
    url: extractScotchUrlAttribute('href')(title),
    views: extractNumber(views),
    comments: extractNumber(comments)
  };
};

/**
 * Extract a single stat from container element
 */
const extractStat = elem => {
  const statElem = elem.find(".stat")
  const labelElem = elem.find('.label');

  const lowercase = val => _.isString(val) ? val.toLowerCase() : null;

  const stat = extractNumber(statElem);
  const label = compose(lowercase, fetchElemInnerText)(labelElem);

  return { [label]: stat };
};

تابع extractPost() نام، عکس، URL، تعداد بازدید و دیدگاه های یک پست را با parse کردن زیر مجموعه های عنصر داده شده استخراج می کند. این تابع از برخی از توابع کمکی که پیش تر ساختیم استفاده می کند تا عناصر مناسب را استخراج کند.

در زیر مثالی از آبجکت باز گردانده شده در اثر صدا کردن تابع extractPost() را میبینید:

{
  title: "Password Strength Meter in AngularJS",
  image: "https://cdn.scotch.io/7540/iKZoyh9WSlSzB9Bt5MNK_post-cover-photo.jpg",
  url: "https://scotch.io//tutorials/password-strength-meter-in-angularjs",
  views: 24280,
  comments: 5
}

تابع extractStat() داده های وضعیت موجود در عنصر داده شده را استخراج می کند. در زیر مثالی از آبجکت باز گردانده شده در اثر صدا کردن تابع extractStat() را میبینید:

{ pageviews: 41454 }

استخراج پروفایل نویسنده Scotch:

حال در ادامه تابع extractAuthorProfile() را تعریف می کنیم که پروفایل کامل نویسندگان Scotch را استخراج می کند. این محتویات را به app/scotch.js اضافه کنید:

/* app/scotch.js */

/**
 * Extract profile from a Scotch author's page using the Cheerio parser instance
 * and returns the author profile object
 */
const extractAuthorProfile = $ => {

  const mainSite = $('#site__main');
  const metaScotch = $("meta[property='og:url']");
  const scotchHero = mainSite.find('section.hero--scotch');
  const superGrid = mainSite.find('section.super-grid');

  const authorTitle = scotchHero.find(".profile__name h1.title");
  const profileRole = authorTitle.find(".tag");
  const profileAvatar = scotchHero.find("img.profile__avatar");
  const profileStats = scotchHero.find(".profile__stats .profile__stat");
  const authorLinks = scotchHero.find(".author-links a[target='_blank']");
  const authorPosts = superGrid.find(".super-grid__item [data-type='post']");

  const extractPosts = extractFromElems(extractPost)();
  const extractStats = extractFromElems(extractStat)(fromPairsToObject);
  const extractSocialUrls = extractFromElems(extractSocialUrl)(fromPairsToObject);

  return Promise.all([
    fetchElemInnerText(authorTitle.contents().first()),
    fetchElemInnerText(profileRole),
    extractUrlAttribute('content')(metaScotch),
    extractUrlAttribute('src')(profileAvatar),
    extractSocialUrls(authorLinks)($),
    extractStats(profileStats)($),
    extractPosts(authorPosts)($)
  ]).then(([ author, role, url, avatar, social, stats, posts ]) => ({ author, role, url, avatar, social, stats, posts }));

};

/**
 * Fetches the Scotch profile of the given author
 */
const fetchAuthorProfile = author => {
  const AUTHOR_URL = `${SCOTCH_BASE}/@${author.toLowerCase()}`;
  return composeAsync(extractAuthorProfile, fetchHtmlFromUrl)(AUTHOR_URL);
};

module.exports = { fetchAuthorProfile };

تابع extractAuthorProfile() بسیار ساده است. ابتدا از $ (نماد parse کننده Cheerio) را برای یافتن تعدادی عنصر و مجموعه عنصر استفاده می کنیم.

سپس، از تابع کمکی extractFromElems() و توابع استخراج کننده که پیش تر ساختیم (extractPost، extractStat و extractSocialUrl) برای ساخت تابع استخراج سطح بالاتری به طور همزمان استفاده می کنیم. به این که چگونه از تابع fromPairsToObject که پیش تر ساختیم به عنوان یک تابع تغییر شکل دهنده استفاده می کنیم دقت کنید.

در آخر، از تابع Promise.all() برای استخراج تمام داده های مورد نیاز استفاده می کنیم، و در عین حال از چند تابع کمکی که پیش تر ساختیم نیز کمک می گیریم. داده های استخراج شده در یک آرایه به این ترتیب قرار می گیرند: نام نویسنده، نقش، لینک صفحه Scotch، لینک آواتار، لینک شبکه های اجتماعی، وضعیت و پست ها.

آبجکت خروجی باید به این شکل باشد:

{
  author: 'Glad Chinda',
  role: 'Author',
  url: 'https://scotch.io/@gladchinda',
  avatar: 'https://cdn.scotch.io/7540/EnhoZyJOQ2ez9kVhsS9B_profile.jpg',
  social: {
    twitter: 'https://twitter.com/gladchinda',
    github: 'https://github.com/gladchinda'
  },
  stats: {
    posts: 6,
    pageviews: 41454,
    readers: 31676
  },
  posts: [
    {
      title: 'Password Strength Meter in AngularJS',
      image: 'https://cdn.scotch.io/7540/iKZoyh9WSlSzB9Bt5MNK_post-cover-photo.jpg',
      url: 'https://scotch.io//tutorials/password-strength-meter-in-angularjs',
      views: 24280,
      comments: 5
    },
    ...
  ]
}

همچنین تابع fetchAuthorProfile() را تعریف می کنیم، که نام کاربری یک نویسنده Scotch را می گیرد، و یک promise باز می گرداند که به صفحه پروفایل نویسنده ختم می شود. برای نویسنده ای که نام کاربری اش gladchinda است، یو آر ال Scotch برابر است با https://scotch.io/@gladchinda.

تابع fetchAuthorProfile() از تابع کمکی composeAsync() برای ساخت یک تابع استفاده می کند، که ابتدا محتویات DOM صفحه نویسنده Scotch را با استفاده از تابع کمکی fetchHtmlFromUrl() می گیرد، و در آخر با استفاده از تابع extractAuthorProfile() که به تازگی ساختیم، پروفایل نویسنده را استخراج می کند.

در آخر، تابع fetchAuthorProfile را به عنوان تنها مشخص کننده در آبجکت module.exports را خروجی می گیریم.

پایان دادن کار با یک Route:

تقریبا کارمان با API تمام شده است. حال باید یک route به سرورمان اضافه کنیم تا بتوانیم پروفایل هر نویسنده Scotch را به دست آوریم. این Route ساختار زیر را خواهد داشت، که در آن author نام نویسنده خواهد بود.

GET /scotch/:author

حال بیایید این Route را بسازیم. چند تغییر به فایل server.js اعمال می کنیم. ابتدا، این کد را به فایل server.js اضافه کنید:

/* server.js */

// Require the needed functions
const { sendResponse } = require('./app/helpers');
const { fetchAuthorProfile } = require('./app/scotch');

در آخر، این route را به فایل server.js اضافه کنید:

/* server.js */

// Add the Scotch author profile route
app.get('/scotch/:author', (req, res, next) => {
  const author = req.params.author;
  sendResponse(res)(fetchAuthorProfile(author));
});

همانطور که میبینید، نویسنده را که از route گرفته ایک، به تابع fetchAuthorPrile() می فرستیم تا پروفایل نویسنده داده شده را بگیریم. سپس از متود کمکی sendResponse() برای ارسال پروفایل بازگردانده شده به شکل یک پاسخ JSON استفاده می کنیم.

حال با موفقیت API خود را با استفاده از تکنیک تکه تکه کردن ساخته ایم. حال API را با اجرای دستور npm start در ترمینال خود اجرا کنید. 

نتیجه گیری:

در این آموزش، دیدیم که چگونه می توانیم تکنیک های تکه تکه کردن (به خصوص parse کردن DOM) را برای استخراج داده هایی از یک وبسایت استفاده کنیم. از پکیج Cheerio برای parse محتویات صفحه وب با استفاده از متود های در دسترس DOM به مانند کتابخانه معروف jQuery استفاده کردیم. گرچه توجه کنید که Cheerio محدودیت های خود را دارد. می توانید parse کردن های پیشرفته تری را با استفاده از مرورگر هایی مانند JSDOM و PhantonJS پیاده سازی کنید.

می توانید سورس کد API ای که در این آموزش ساختیم را در گیت هاب پیدا کنید. همچنین برنامه آزمایشی ای بر پایه API ساخته شده در این آموزش ساخته ام، که در اسکرین شات ابتدای آموزش نشان داده شده است. می توانید برنامه را در Heroku و سورس کد را در گیت هاب بیابید.

منبع

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

10 روش بهینه سازی فرم های وبسایت برای موبایل

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

بهبود امنیت با بهتر‌ کردن تجربه‌کاربری

کاربرانی که در اینترنت جستجو می‌کنند و نظاره‌گر هستند، همیشه دوست دارند که وبسایت‌های مقصود‌شان امن و کاربرپسند باشند. آن‌ها تنها به شرکت‌هایی اعتماد...

دلایل بهینه‌سازی وبسایت

زمانی که شرکت‌ها شروع به ساخت یک وبسایت می‌کنند مطمئنا از انجام این‌ کار هدفی دارند. ممکن است آن‌ها قصد داشته باشند تا نام یک برند را معروف کنند و یا...

توجهات کلیدی تجربه کاربری برای بهتر کردن صفحات محصولات فروشگاهی

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