Thứ năm, 12/12/2019 | 00:00 GMT+7

Cách cạo trang web bằng Node.js

Web cạo là kỹ thuật extract dữ liệu từ các trang web. Dữ liệu này có thể được lưu trữ thêm trong database hoặc bất kỳ hệ thống lưu trữ nào khác để phân tích hoặc các mục đích sử dụng khác. Trong khi việc extract dữ liệu từ các trang web có thể được thực hiện theo cách thủ công, việc extract trang web thường đề cập đến một quy trình tự động.

Phần lớn các bot và trình thu thập dữ liệu web sử dụng tính năng thu thập dữ liệu để extract dữ liệu. Có nhiều phương pháp và công cụ khác nhau mà bạn có thể sử dụng để tìm kiếm trang web và trong hướng dẫn này, ta sẽ tập trung vào việc sử dụng một kỹ thuật liên quan đến phân tích cú pháp DOM một trang web.

Yêu cầu

Việc tìm kiếm trên web có thể được thực hiện hầu như bằng bất kỳ ngôn ngữ lập trình nào có hỗ trợ phân tích cú pháp HTTPXML hoặc DOM . Trong hướng dẫn này, ta sẽ tập trung vào việc tìm kiếm web bằng JavaScript trong môi trường server Node.js.

Với ý nghĩ đó, hướng dẫn này giả định người đọc biết những điều sau:

  • Hiểu biết về JavaScript và cú pháp ES6 và ES7
  • Làm quen với jQuery
  • Các khái niệm lập trình hàm

Tiếp theo, ta sẽ xem qua dự án kết thúc của ta sẽ là gì.

Thông số dự án

Ta sẽ sử dụng tính năng quét web để extract một số dữ liệu từ trang web Scotch . Scotch không cung cấp API để lấy profile và hướng dẫn / bài đăng của các tác giả. Vì vậy, ta sẽ xây dựng một API để tìm nạp profile và hướng dẫn / bài đăng của các tác giả Scotch.

Đây là ảnh chụp màn hình của một ứng dụng demo được tạo dựa trên API mà ta sẽ xây dựng trong hướng dẫn này. Bạn có thể xem mã nguồn trên GitHub .

Ảnh chụp màn hình ứng dụng demo

Trước khi bắt đầu, hãy xem qua các gói và phụ thuộc bạn cần để hoàn thành dự án này.

Cài đặt dự án

Trước khi bắt đầu, hãy đảm bảo bạn đã cài đặt Nodenpm hoặc sợi trên máy của bạn . Vì ta sẽ sử dụng nhiều cú pháp ES6 / 7 trong hướng dẫn này, nên bạn nên sử dụng các version Node và npm sau để hỗ trợ ES6 / 7 hoàn chỉnh: Node 8.9.0 trở lên và npm 5.2.0 trở lên.

Ta sẽ sử dụng các gói cốt lõi sau:

  1. Cheerio - Cheerio là một triển khai nhanh, linh hoạt và gọn nhẹ của jQuery lõi được thiết kế đặc biệt cho server . Nó làm cho việc phân tích cú pháp DOM rất dễ dàng.

  2. Axios - Axios là một ứng dụng client HTTP dựa trên lời hứa cho trình duyệt và Node.js. Nó sẽ cho phép ta tìm nạp nội dung trang thông qua các yêu cầu HTTP.

  3. Express - Express là một khung ứng dụng web Node.js tối thiểu và linh hoạt, cung cấp một bộ tính năng mạnh mẽ cho các ứng dụng web và di động.

  4. Lodash - Lodash là một thư viện tiện ích JavaScript hiện đại cung cấp tính năng module , hiệu suất và tính năng bổ sung. Nó làm cho JavaScript dễ dàng hơn bằng cách loại bỏ những rắc rối khi làm việc với mảng, số, đối tượng, chuỗi, v.v.

Bước 1 - Tạo Danh mục Ứng dụng

Tạo một folder mới cho ứng dụng và chạy lệnh sau để cài đặt các phần phụ thuộc cần thiết cho ứng dụng.

# 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 

Bước 2 - Cài đặt ứng dụng server Express

Ta sẽ tiếp tục cài đặt ứng dụng server HTTP bằng Express. Tạo file server.js trong folder root của ứng dụng của bạn và thêm đoạn mã sau để cài đặt server :

/_ 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}.`)); 

Bước 3 - Sửa đổi scripts npm

Cuối cùng, ta sẽ sửa đổi phần "scripts" của file package.json để trông giống như đoạn mã sau:

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

Ta đã có tất cả những gì ta cần để bắt đầu xây dựng ứng dụng của bạn . Nếu bạn chạy lệnh npm start trong terminal của bạn ngay bây giờ, nó sẽ khởi động server ứng dụng trên cổng 3000 nếu có. Tuy nhiên, ta chưa thể truy cập bất kỳ tuyến đường nào vì ta chưa thêm các tuyến đường vào ứng dụng của bạn . Hãy bắt đầu xây dựng một số chức năng trợ giúp mà ta cần để tìm kiếm trang web.

Bước 4 - Tạo chức năng của người trợ giúp

Như đã nêu trước đó, ta sẽ tạo một vài hàm trợ giúp sẽ được sử dụng trong một số phần của ứng dụng của ta . Tạo một folder app mới trong folder root dự án của bạn. Tạo một file mới có tên helpers.js trong folder vừa tạo và thêm nội dung sau vào đó:

/_ app/helpers.js _/  const _ = require('lodash'); const axios = require("axios"); const cheerio = require("cheerio"); 

Trong mã này, ta yêu cầu các phụ thuộc mà ta cần cho các chức năng trợ giúp của ta . Hãy tiếp tục và thêm các chức năng trợ giúp.

Tạo các chức năng của trình trợ giúp tiện ích

Ta sẽ bắt đầu bằng cách tạo một số chức năng trợ giúp tiện ích. Thêm đoạn mã sau vào file 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); 

Ta hãy xem xét từng chức năng một để hiểu chúng làm gì.

  • compose() - Đây là một hàm bậc cao nhận một hoặc nhiều hàm làm đối số của nó và trả về một composed function . Hàm soạn thảo có tác dụng giống như việc gọi các hàm được truyền vào dưới dạng đối số từ phải sang trái, chuyển kết quả của một lệnh gọi hàm làm đối số cho hàm tiếp theo mỗi lần. <br/\> <br/\> Nếu bất kỳ các đối số được truyền cho compose() không phải là một function , hàm comp sẽ tạo ra một lỗi khi nào nó được gọi. Đây là đoạn mã mô tả cách hoạt động của 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() - Chức năng này hoạt động theo cách tương tự như compose() chức năng. Sự khác biệt duy nhất là nó không đồng bộ. Do đó, nó là lý tưởng để soạn các hàm có hành vi không đồng bộ - ví dụ, các hàm trả về các lời hứa.

  • enforceHttpsUrl() - Hàm này lấy chuỗi url làm đối số và trả về url với schemas https với điều kiện url bắt đầu bằng https:// , http:// hoặc // . Nếu url không phải là một chuỗi thì trả về null . Đây là một ví dụ.

enforceHttpsUrl('scotch.io'); // returns => 'scotch.io' enforceHttpsUrl('//scotch.io'); // returns => 'https://scotch.io' enforceHttpsUrl('http://scotch.io'); // returns => 'https://scotch.io' 
  • sanitizeNumber() - Hàm này yêu cầu một number hoặc string làm đối số. Nếu một number được chuyển cho nó, nó sẽ trả về số đó. Tuy nhiên, nếu một string được chuyển đến nó, nó sẽ xóa các ký tự không phải số khỏi chuỗi và trả về chuỗi đã được khử trùng. Đối với các kiểu giá trị khác, nó trả về null . Đây là một ví dụ:
sanitizeNumber(53.56); // returns => 53.56 sanitizeNumber('-2oo,40'); // returns => '-240' sanitizeNumber('badnumber.decimal'); // returns => '.' 
  • withoutNulls() - Hàm này mong đợi một array làm đối số và trả về một mảng mới chỉ chứa các mục non-null của mảng ban đầu. Đây là một ví dụ.
withoutNulls([ 'String', [], null, {}, null, 54 ]); // returns => ['String', [], {}, 54] 
  • arrayPairsToObject() - Hàm này yêu cầu một array ( { key: value } ) đối tượng và trả về một đối tượng đã chuyển đổi với các khóa và giá trị. Đây là một ví dụ.
const pairs = [ { key1: 'value1' }, { key2: 'value2' }, { key3: 'value3' } ];  arrayPairsToObject(pairs); // returns => { key1: 'value1', key2: 'value2', key3: 'value3' } 
  • fromPairsToObject() - Đây là một hàm tổng hợp được tạo bằng cách sử dụng fromPairsToObject() compose() . Nó có tác dụng tương tự như thực thi:
arrayPairsToObject( withoutNulls(array) ); 

Chức năng của Trình trợ giúp Yêu cầu và Phản hồi

Thêm phần sau vào file 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;     }); }; 

Ở đây, ta đã thêm hai hàm mới: sendResponse()fetchHtmlFromUrl() . Hãy cố gắng hiểu những gì họ làm.

  • sendResponse() - Đây là một hàm bậc cao hơn mong đợi một stream phản hồi HTTP Express ( res ) làm đối số của nó và trả về một async function . Hàm async function trả về mong đợi một promise hoặc một thenable làm đối số của nó ( request ). <br/\> <br/\> Nếu lời hứa request giải quyết, thì phản hồi JSON thành công sẽ được gửi bằng res.json() , chứa lời giải dữ liệu. Nếu lời hứa bị từ chối, thì phản hồi JSON lỗi với mã trạng thái HTTP thích hợp sẽ được gửi. Đây là cách nó được dùng trong tuyến đường Tốc hành:
app.get('/path', (req, res, next) => {   const request = Promise.resolve([1, 2, 3, 4, 5]);   sendResponse(res)(request); }); 

Thực hiện một yêu cầu GET tới điểm cuối /path sẽ trả về phản hồi JSON này:

{   "status": "success",   "data": [1, 2, 3, 4, 5] } 
  • fetchHtmlFromUrl() - Đây là một async function không đồng bộ mong đợi một chuỗi url làm đối số của nó. Đầu tiên, nó sử dụng axios.get() để tìm nạp nội dung của URL (trả về một lời hứa). Nếu lời hứa được giải quyết, nó sẽ sử dụng cheerio.load() với nội dung được trả về để tạo một thể hiện trình phân tích cú pháp Cheerio, rồi trả về thể hiện đó. Tuy nhiên, nếu lời hứa bị từ chối, nó sẽ tạo ra một lỗi với mã trạng thái thích hợp. <br/\> <br/\> Phiên bản phân tích cú pháp Cheerio được trả về bởi hàm này sẽ cho phép ta extract dữ liệu ta yêu cầu. Ta có thể sử dụng nó theo nhiều cách tương tự như ta sử dụng cá thể jQuery được trả về bằng cách gọi $() hoặc jQuery() trên một mục tiêu DOM.

Các chức năng của trình trợ giúp phân tích cú pháp DOM

Hãy tiếp tục để thêm một số chức năng bổ sung để giúp ta phân tích cú pháp DOM. Thêm nội dung sau vào file 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 }; 

Ta đã thêm một số chức năng khác. Dưới đây là các chức năng và những gì chúng làm:

  • fetchElemInnerText() - Hàm này yêu cầu một element làm đối số. Nó extract innerText của phần tử bằng cách gọi elem.text() , nó cắt bớt văn bản của các khoảng trắng xung quanh và trả về văn bản bên trong đã được cắt bớt. Đây là một ví dụ.
const $ = cheerio.load('<div class="fullname">  Glad Chinda </div>'); const elem = $('div.fullname');  fetchElemInnerText(elem); // returns => 'Glad Chinda' 
  • fetchElemAttribute() - Đây là một hàm bậc cao hơn mong đợi một attribute làm đối số và trả về một hàm khác mong đợi một element làm đối số. Hàm trả về extract giá trị của attribute của phần tử bằng cách gọi elem.attr(attribute) . Đây là một ví dụ.
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() - Đây là một hàm bậc cao hơn trả về một hàm bậc cao khác. Ở đây, ta đã sử dụng một kỹ thuật lập trình hàm được gọi là currying để tạo ra một chuỗi các hàm, mỗi hàm chỉ yêu cầu một đối số. Đây là chuỗi các đối số:
extractorFunction -> transformFunction -> elementsCollection -> cheerioInstance 

extractFromElems() cho phép extract dữ liệu từ một tập hợp các phần tử tương tự bằng cách sử dụng hàm extractor và cũng có thể chuyển đổi dữ liệu được extract bằng hàm transform . Hàm extractor nhận một phần tử làm đối số, trong khi hàm transform nhận một mảng giá trị làm đối số.
<br/\> <br/\>
Giả sử ta có một tập hợp các phần tử, mỗi phần tử chứa tên của một người dưới dạng innerText . Ta muốn extract tất cả các tên này và trả lại chúng trong một mảng, tất cả đều được viết hoa. Đây là cách ta có thể thực hiện việc này bằng cách sử dụng 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() - Đây là một hàm tổng hợp mong đợi một element làm đối số và cố gắng extract một số từ innerText của phần tử. Nó thực hiện điều này bằng cách soạn parseInt() , sanitizeNumber()fetchElemInnerText() . Nó có tác dụng tương tự như thực thi:
parseInt( sanitizeNumber( fetchElemInnerText(elem) ) ); 
  • extractUrlAttribute() - Đây là một hàm bậc cao hơn bao gồm mong đợi một attribute làm đối số và trả về một hàm khác mong đợi một element làm đối số. Hàm trả về cố gắng extract giá trị URL của một thuộc tính trong phần tử và trả về nó bằng schemas https . Đây là một đoạn mã cho thấy cách hoạt động:
// METHOD 1 const fetchAttribute = fetchElemAttribute(attr); enforceHttpsUrl( fetchAttribute(elem) );  // METHOD 2: Using extractUrlAttribute() const fetchUrlAttribute = extractUrlAttribute(attr); fetchUrlAttribute(elem); 

Cuối cùng, ta xuất tất cả các chức năng trợ giúp mà ta đã tạo bằng module.exports . Bây giờ ta đã có các chức năng trợ giúp của bạn , ta có thể tiếp tục phần tìm kiếm web của hướng dẫn này.

Bước 5 - Cài đặt Scraping bằng cách gọi URL

Tạo một file mới có tên scotch.js trong folder app của dự án của bạn và thêm nội dung sau vào đó:

/_ 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)); 

Như bạn thấy, ta đã nhập lodash cũng như một số hàm trợ giúp mà ta đã tạo trước đó. Ta cũng đã xác định một hằng số có tên là SCOTCH_BASE chứa URL cơ sở của trang web Scotch. Cuối cùng, ta đã thêm hai chức năng trợ giúp:

  • scotchRelativeUrl() - Hàm này lấy một chuỗi url tương đối làm đối số và trả về URL với SCOTCH_BASE cấu hình trước được thêm vào. Nếu url không phải là một chuỗi thì trả về null . Đây là một ví dụ.
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' 
  • extractScotchUrlAttribute() - Đây là một hàm bậc cao hơn bao gồm mong đợi một attribute là đối số và trả về một hàm khác mong đợi một element làm đối số. Hàm được trả về cố gắng extract giá trị URL của một thuộc tính trong phần tử, SCOTCH_BASE cấu hình SCOTCH_BASE vào nó và trả về nó với schemas https . Đây là một đoạn mã cho thấy cách hoạt động:
// METHOD 1 const fetchAttribute = fetchElemAttribute(attr); enforceHttpsUrl( scotchRelativeUrl( fetchAttribute(elem) ) );  // METHOD 2: Using extractScotchUrlAttribute() const fetchUrlAttribute = extractScotchUrlAttribute(attr); fetchUrlAttribute(elem); 

Bước 6 - Sử dụng các chức năng extract

Ta muốn có thể extract dữ liệu sau cho bất kỳ tác giả Scotch nào:

  • hồ sơ (tên, role , hình đại diện, v.v.)
  • liên kết xã hội (facebook, twitter, github, v.v.)
  • thống kê (tổng lượt xem, tổng số bài đăng, v.v.)
  • bài viết

Nếu bạn nhớ lại, các extractFromElems() chức năng helper ta đã tạo trước đó yêu cầu một extractor chức năng để extract nội dung từ một tập hợp các yếu tố tương tự. Ta sẽ định nghĩa một số hàm extract trong phần này.

Đầu tiên, ta sẽ tạo một extractSocialUrl() để extract tên mạng xã hội và URL từ phần tử <a> liên kết xã hội. Đây là cấu trúc DOM của phần tử liên kết xã hội <a> mong đợi bởi 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> 

Gọi hàm extractSocialUrl() sẽ trả về một đối tượng trông giống như sau:

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

Hãy tiếp tục tạo chức năng. Thêm nội dung sau vào file 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 };  }; 

Hãy thử tìm hiểu cách hoạt động của hàm extractSocialUrl() :

  1. Đầu tiên, ta tìm nạp phần tử con <span> bằng một lớp icon . Ta cũng xác định một biểu thức chính quy trùng với tên lớp biểu tượng xã hội.

  2. Ta định nghĩa onlySocialClasses() bậc cao onlySocialClasses() nhận một biểu thức chính quy làm đối số của nó và trả về một hàm. Hàm trả về nhận một chuỗi tên lớp được phân tách bằng dấu cách. Sau đó, nó sử dụng biểu thức chính quy để chỉ extract các tên tầng lớp xã hội từ danh sách và trả về chúng trong một mảng. Đây là một ví dụ:

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' ] 
  1. Tiếp theo, ta định nghĩa getSocialFromClasses() bậc cao getSocialFromClasses() lấy một biểu thức chính quy làm đối số của nó và trả về một hàm. Hàm trả về nhận một mảng các chuỗi lớp đơn. Sau đó, nó sử dụng biểu thức chính quy để extract tên mạng xã hội từ lớp đầu tiên trong danh sách và trả về nó. Đây là một ví dụ:
const regex = /^(?:icon|color)-(.+)$/; const extractSocialName = getSocialFromClasses(regex); const classNames = [ 'color-twitter', 'icon-github' ];  extractSocialName(classNames); // returns 'twitter' 
  1. Sau đó, ta extract URL thuộc tính href từ phần tử. Ta cũng extract tên mạng xã hội từ phần tử biểu tượng <span> bằng cách sử dụng một hàm tổng hợp được tạo bằng cách soạn getSocialFromClasses(regex) , onlySocialClasses(regex)fetchElemAttribute('class') .

  2. Cuối cùng, ta trả về một đối tượng có tên mạng xã hội làm khóa và URL href làm giá trị. Tuy nhiên, nếu không có mạng xã hội nào được tìm nạp, thì giá trị null sẽ được trả về. Đây là một ví dụ về đối tượng được trả về:

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

Extract các bài đăng và số liệu thống kê

Ta sẽ tiếp tục tạo hai hàm extract bổ sung là: extractPost()extractStat() , để extract các bài đăng và thống kê tương ứng. Trước khi tạo các hàm, ta hãy xem cấu trúc DOM của các phần tử mà các hàm này mong đợi.

Đây là cấu trúc DOM của phần tử được mong đợi bởi 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> 

Đây là cấu trúc DOM của phần tử được mong đợi bởi extractStat() .

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

Thêm nội dung sau vào file 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 }; }; 

Hàm extractPost() extract tiêu đề, hình ảnh, URL, lượt xem và comment của một bài đăng bằng cách phân tích cú pháp các phần tử con của phần tử đã cho. Nó sử dụng một số hàm trợ giúp mà ta đã tạo trước đó để extract dữ liệu từ các phần tử thích hợp.

Đây là một ví dụ về đối tượng được trả về khi gọi 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 } 

Hàm extractStat() extract dữ liệu thống kê có trong phần tử đã cho. Đây là một ví dụ về đối tượng được trả về khi gọi extractStat() .

{ pageviews: 41454 } 

Bước 7 - Extract một trang web cụ thể

Bây giờ ta sẽ tiến hành định nghĩa hàm extractAuthorProfile() extract profile hoàn chỉnh của tác giả Scotch. Thêm nội dung sau vào file 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 = $('#sitemain');   const metaScotch = $("meta**[**property='og:url']");   const scotchHero = mainSite.find('section.hero--scotch');   const superGrid = mainSite.find('section.super-grid');    const authorTitle = scotchHero.find(".profilename h1.title");   const profileRole = authorTitle.find(".tag");   const profileAvatar = scotchHero.find("img.profileavatar");   const profileStats = scotchHero.find(".profilestats .profilestat");   const authorLinks = scotchHero.find(".author-links a**[**target='_blank']");   const authorPosts = superGrid.find(".super-griditem **[**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 }; 

Hàm extractAuthorProfile() rất đơn giản. Đầu tiên ta sử dụng $ (phiên bản phân tích cú pháp cheerio) để tìm một vài phần tử và tập hợp phần tử.

Tiếp theo, ta sử dụng hàm trợ giúp extractFromElems() cùng với các hàm extract mà ta đã tạo trước đó trong phần này ( extractPost , extractStatextractSocialUrl ) để tạo các hàm extract bậc cao hơn. Lưu ý cách ta sử dụng hàm trợ giúp fromPairsToObject mà ta đã tạo trước đó như một hàm biến đổi.

Cuối cùng, ta sử dụng Promise.all() để extract tất cả dữ liệu cần thiết, tận dụng một số hàm trợ giúp mà ta đã tạo trước đó. Dữ liệu extract được chứa trong cấu trúc mảng theo trình tự sau: tên tác giả, role , liên kết Scotch, liên kết ảnh đại diện, liên kết xã hội, số liệu thống kê và bài đăng.

Lưu ý cách ta sử dụng hàm hủy trong trình xử lý hứa .then() để xây dựng đối tượng cuối cùng được trả về khi tất cả các hứa hẹn giải quyết xong. Đối tượng được trả về sẽ giống như sau:

{   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     },     ...   ] } 

Ta cũng xác định hàm fetchAuthorProfile() chấp nhận tên user Scotch của tác giả và trả về một Lời hứa giải quyết profile của tác giả. Đối với tác giả có tên user là gladchinda , URL của Scotch là https://scotch.io/@gladchinda .

fetchAuthorProfile() sử dụng hàm trợ giúp composeAsync() để tạo một hàm soạn thảo trước tiên tìm nạp nội dung DOM trên trang Scotch của tác giả bằng cách sử dụng hàm trợ giúp fetchHtmlFromUrl() và cuối cùng extract profile của tác giả bằng cách sử dụng hàm extractAuthorProfile() mà ta chỉ tạo.

Cuối cùng, ta xuất fetchAuthorProfile làm định danh duy nhất trong đối tượng module.exports .

Bước 8 - Cách tạo lộ trình

Ta gần như đã hoàn thành với API của bạn . Ta cần thêm một tuyến vào server của bạn để cho phép ta tìm nạp profile của bất kỳ tác giả Scotch nào. Tuyến đường sẽ có cấu trúc như sau, trong đó tham số author đại diện cho tên user của tác giả Scotch.

GET /scotch/:author 

Hãy tiếp tục và tạo ra tuyến đường này. Ta sẽ thực hiện một vài thay đổi đối với file server.js . Đầu tiên, thêm phần sau vào file server.js để yêu cầu một số chức năng ta cần.

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

Cuối cùng, thêm tuyến vào file server.js ngay sau phần mềm trung gian.

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

Như bạn thấy , ta chuyển author nhận được từ tham số định tuyến vào hàm fetchAuthorProfile() để lấy profile của tác giả đã cho. Sau đó, ta sử dụng phương thức trợ giúp sendResponse() để gửi profile được trả về dưới dạng phản hồi JSON.

Ta đã tạo thành công API của bạn bằng cách sử dụng kỹ thuật quét web. Hãy tiếp tục và kiểm tra API bằng cách chạy lệnh npm start trên terminal của bạn. Chạy công cụ kiểm tra HTTP yêu thích của bạn, ví dụ như Postman và kiểm tra điểm cuối API. Nếu bạn làm theo tất cả các bước một cách chính xác, bạn sẽ có kết quả giống như bản trình diễn sau:

< img src = “https://farm1.staticflickr.com/960/41038838905\_ab703d85fb\_o.jpg” width = “1280” height = “784” alt = “Bản trình diễn API Scraping Scotch” \> </ a \> < script async src = “// nhúngr.flickr.com/assets/client-code.js” charset = “utf-8” \> </ script \>

Kết luận

Trong hướng dẫn này, ta đã thấy cách ta có thể sử dụng các kỹ thuật rà soát web (đặc biệt là phân tích cú pháp DOM) để extract dữ liệu từ một trang web. Ta đã sử dụng gói Cheerio để phân tích cú pháp nội dung của một trang web bằng cách sử dụng các phương thức DOM có sẵn theo kiểu gần giống như thư viện jQuery phổ biến. Tuy nhiên, lưu ý Cheerio có những hạn chế của nó. Bạn có thể phân tích cú pháp nâng cao hơn bằng cách sử dụng các trình duyệt không có đầu như JSDOMPhantomJS .

Bạn có thể tìm thấy mã nguồn cho API mà ta đã xây dựng trong hướng dẫn này trên GitHub . Ta cũng đã xây dựng một ứng dụng demo dựa trên API từ hướng dẫn này như trong ảnh chụp màn hình ban đầu. Bạn có thể xem mã nguồn trên GitHub .


Tags:

Các tin liên quan