Thứ tư, 19/08/2020 | 00:00 GMT+7

Làm thế nào để tạo một Web Scraper đồng thời với Puppeteer, Node.js, Docker và Kubernetes

Thu thập dữ liệu web, còn gọi là thu thập thông tin web, sử dụng các chương trình để extract , phân tích cú pháp và download nội dung và dữ liệu từ các trang web.

Bạn có thể quét dữ liệu từ vài chục trang web bằng một máy duy nhất, nhưng nếu bạn phải truy xuất dữ liệu từ hàng trăm hoặc thậm chí hàng nghìn trang web, bạn có thể cân nhắc việc phân phối dung lượng công việc.

Trong hướng dẫn này, bạn sẽ sử dụng Puppeteer để cạo books.toscrape , một hiệu sách hư cấu có chức năng như một nơi an toàn cho người mới bắt đầu học cách cạo trên web và để các nhà phát triển xác thực công nghệ cạo của họ. Tại thời điểm viết bài này, có 1000 cuốn sách trên books.toscrape và 1000 trang web mà bạn có thể tìm kiếm. Tuy nhiên, trong hướng dẫn này, bạn sẽ chỉ cạo 400 trang đầu tiên. Để quét tất cả các trang web này trong một khoảng thời gian ngắn, bạn sẽ xây dựng và triển khai một ứng dụng có thể mở rộng chứa khung web Express và trình điều khiển trình duyệt Puppeteer vào một cụm Kubernetes . Để tương tác với trình quét của bạn, sau đó bạn sẽ xây dựng một ứng dụng có chứa axios , một ứng dụng client HTTP dựa trên lời hứa và lowdb , một database JSON nhỏ cho Node.js.

Khi bạn hoàn thành hướng dẫn này, bạn sẽ có một trình quét có thể mở rộng có khả năng extract đồng thời dữ liệu từ nhiều trang. Ví dụ: với cài đặt mặc định và một cụm ba nút, bạn sẽ mất chưa đến 2 phút để quét 400 trang trên books.toscrape. Sau khi mở rộng cụm của bạn, sẽ mất khoảng 30 giây.

Cảnh báo: Đạo đức và tính hợp lệ của việc tìm kiếm trên web rất phức tạp và liên tục phát triển. Chúng cũng khác nhau dựa trên vị trí của bạn, vị trí của dữ liệu và trang web được đề cập. Hướng dẫn này quét một trang web đặc biệt, books.toscrape.com , được thiết kế rõ ràng để kiểm tra các ứng dụng quét . Scrap bất kỳ domain nào khác nằm ngoài phạm vi của hướng dẫn này.

Yêu cầu

Để làm theo hướng dẫn này, bạn cần một máy có:

Bước 1 - Phân tích trang web mục tiêu

Trước khi viết bất kỳ mã nào, hãy chuyển đến books.toscrape trong trình duyệt web. Kiểm tra cách dữ liệu được cấu trúc và tại sao việc cạo đồng thời là giải pháp tối ưu.

Tiêu đề trang chủ books.toscrape

Lưu ý có 1.000 cuốn sách trên trang web này, nhưng mỗi trang chỉ hiển thị 20 cuốn.

Di chuyển đến dưới cùng của trang.

chân trang trang chủ books.toscrape

Nội dung trên trang web này được phân trang và có tổng cộng 50 trang. Bởi vì mỗi trang hiển thị 20 cuốn sách và bạn chỉ muốn lấy ra 400 cuốn sách đầu tiên, bạn sẽ chỉ truy xuất tên sách, giá cả, xếp hạng và URL cho mỗi cuốn sách được hiển thị trên 20 trang đầu tiên.

Toàn bộ quá trình sẽ mất ít hơn 1 phút.

Mở công cụ dành cho nhà phát triển của trình duyệt và kiểm tra cuốn sách đầu tiên trên trang. Bạn sẽ thấy nội dung sau:

Trang chủ books.toscrape với các công cụ dành cho nhà phát triển

Mọi cuốn sách đều nằm trong <section> và mỗi cuốn sách được liệt kê trong <li> riêng của nó. Bên trong mỗi <li> có một <article> có thuộc tính class bằng product_pod . Đây là phần tử mà ta muốn loại bỏ.

Sau khi lấy metadata cho mỗi cuốn sách trong 20 trang đầu tiên và lưu trữ nó, bạn sẽ có một database local chứa 400 cuốn sách. Tuy nhiên, vì thông tin chi tiết hơn về cuốn sách tồn tại trên trang riêng của cuốn sách, bạn cần chuyển 400 trang bổ sung bằng cách sử dụng URL bên trong metadata của mỗi cuốn sách. Sau đó, bạn sẽ truy xuất các chi tiết sách bị thiếu mà bạn muốn và thêm dữ liệu này vào database local của bạn. Dữ liệu còn thiếu mà bạn sẽ truy xuất là mô tả, UPC (Mã sách chung), số lượng bài đánh giá và tính khả dụng của sách. Xem qua 400 trang bằng một máy có thể mất hơn 7 phút và đây là lý do tại sao bạn cần Kubernetes để phân chia công việc trên nhiều máy.

Bây giờ hãy nhấp vào liên kết của cuốn sách đầu tiên trên trang chủ, trang này sẽ mở ra trang chi tiết của cuốn sách đó. Mở lại các công cụ dành cho nhà phát triển của trình duyệt và kiểm tra trang.

Trang sách books.toscrape với các công cụ dành cho nhà phát triển

Thông tin bị thiếu mà bạn muốn extract lại nằm trong <article> có thuộc tính class bằng product_page .

Để tương tác với trình quét của ta trong cụm, bạn cần tạo một ứng dụng client có khả năng gửi HTTP yêu cầu HTTP đến cụm Kubernetes của ta . Đầu tiên bạn sẽ viết mã cho phía server và sau đó là phía client của dự án này.

Trong phần này, bạn đã xem xét thông tin mà trình quét của bạn sẽ truy xuất và lý do tại sao bạn cần triển khai trình quét này cho một cụm Kubernetes. Trong phần tiếp theo, bạn sẽ tạo các folder cho các ứng dụng client và server .

Bước 2 - Tạo folder root dự án

Trong bước này, bạn sẽ tạo cấu trúc folder cho dự án của bạn . Sau đó, bạn sẽ khởi tạo một dự án Node.js cho các ứng dụng client và server của bạn .

Mở cửa sổ dòng lệnh và tạo một folder mới có tên là concurrent-webscraper :

  • mkdir concurrent-webscraper

Điều hướng vào folder :

  • cd ./concurrent-webscraper

Bây giờ, hãy tạo ba folder con có tên là server , clientk8s :

  • mkdir server client k8s

Điều hướng vào folder server :

  • cd ./server

Tạo một dự án Node.js mới. Chạy lệnh init của npm sẽ tạo một file package.json , file này sẽ giúp bạn quản lý các phụ thuộc và metadata của bạn .

Chạy lệnh khởi tạo:

  • npm init

Để chấp nhận các giá trị mặc định, nhấn ENTER đến tất cả các dấu nhắc ; cách khác, bạn có thể cá nhân hóa câu trả lời của bạn . Bạn có thể đọc thêm về cài đặt khởi tạo của npm trong Bước một trong hướng dẫn của ta , Cách sử dụng Mô-đun Node.js với npm và package.json .

Mở file package.json và chỉnh sửa nó:

  • nano package.json

Bạn cần sửa đổi thuộc tính main , thêm một số thông tin vào chỉ thị scripts , sau đó tạo chỉ thị dependencies .

Thay thế nội dung bên trong file bằng mã được đánh dấu:

./server/package.json
{   "name": "server",   "version": "1.0.0",   "description": "",   "main": "server.js",   "scripts": {     "start": "node server.js"   },   "keywords": [],   "author": "",   "license": "ISC",   "dependencies": {   "body-parser": "^1.19.0",   "express": "^4.17.1",   "puppeteer": "^3.0.0"   } } 

Ở đây bạn đã thay đổi thuộc tính mainscripts , đồng thời bạn cũng chỉnh sửa dependencies tính dependencies . Vì ứng dụng server sẽ chạy bên trong containers Docker, bạn không cần phải chạy lệnh npm install , lệnh này thường sau khi khởi tạo và tự động thêm từng phần phụ thuộc vào package.json .

Lưu và đóng file .

Điều hướng đến folder client của bạn:

  • cd ../client

Tạo một dự án Node.js khác:

  • npm init

Làm theo quy trình tương tự để chấp nhận cài đặt mặc định hoặc tùy chỉnh phản hồi của bạn.

Mở file package.json và chỉnh sửa nó:

  • nano package.json

Thay thế nội dung bên trong file bằng mã được đánh dấu:

./client/package.json
{   "name": "client",   "version": "1.0.0",   "description": "",   "main": "main.js",   "scripts": {     "start": "node main.js"   },   "author": "",   "license": "ISC" } 

Ở đây bạn đã thay đổi các thuộc tính mainscripts .

Lần này, sử dụng npm để cài đặt các phụ thuộc cần thiết:

  • npm install axios lowdb --save

Trong khối mã này, bạn đã cài đặt axioslowdb . 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. Bạn sẽ sử dụng module này để gửi HTTP yêu cầu HTTP không đồng bộ đến các điểm cuối REST trong trình quét của ta để tương tác với nó; lowdb là database JSON nhỏ cho Node.js và trình duyệt, bạn sẽ sử dụng database này để lưu trữ dữ liệu cóp nhặt của bạn .

Trong bước này, bạn đã tạo một folder dự án và khởi tạo một dự án Node.js cho server ứng dụng của bạn sẽ chứa trình quét; sau đó bạn cũng làm như vậy đối với ứng dụng client sẽ tương tác với server ứng dụng. Bạn cũng đã tạo một folder cho các file cấu hình Kubernetes của bạn . Trong bước tiếp theo, bạn sẽ bắt đầu xây dựng server ứng dụng.

Bước 3 - Xây dựng file Scraper đầu tiên

Trong bước này và bước 4, bạn sẽ tạo bộ quét ở phía server . Ứng dụng này sẽ bao gồm hai file : puppeteerManager.jsserver.js . Tệp puppeteerManager.js sẽ tạo và quản lý các phiên trình duyệt và file server.js sẽ nhận được yêu cầu extract một hoặc nhiều trang web. Đổi lại, các yêu cầu này sẽ gọi một phương thức bên trong puppeteerManager.js sẽ quét một trang web nhất định và trả về dữ liệu đã được cạo. Trong bước này, bạn sẽ tạo file puppeteerManager.js . Trong Bước 4, bạn sẽ tạo file server.js .

Đầu tiên, quay lại folder server và tạo một file có tên là puppeteerManager.js .

Điều hướng đến folder server :

  • cd ../server

Tạo và mở file puppeteerManager.js bằng editor bạn muốn :

  • nano puppeteerManager.js

Tệp puppeteerManager.js của bạn sẽ chứa một lớp được gọi là PuppeteerManager và lớp này sẽ tạo và quản lý một version trình duyệt Puppeteer . Đầu tiên bạn sẽ tạo lớp này và sau đó thêm một phương thức khởi tạo vào nó.

Thêm mã sau vào file puppeteerManager.js của bạn:

puppeteerManager.js
class PuppeteerManager {     constructor(args) {         this.url = args.url         this.existingCommands = args.commands         this.nrOfPages = args.nrOfPages         this.allBooks = [];         this.booksDetails = {}     } } module.exports = { PuppeteerManager } 

Trong khối mã đầu tiên này, bạn đã tạo lớp PuppeteerManager và thêm một hàm tạo vào nó.
Hàm tạo dự kiến nhận một đối tượng chứa các thuộc tính sau:

  • url : Thuộc tính này sẽ chứa một chuỗi, đây sẽ là địa chỉ của trang mà bạn muốn quét.
  • commands : Thuộc tính này sẽ chứa một mảng, cung cấp các hướng dẫn cho trình duyệt. Ví dụ: nó sẽ hướng trình duyệt nhấp vào một nút hoặc phân tích cú pháp một phần tử DOM cụ thể. Mỗi command có các thuộc tính sau: description , locatorCsstype . description cho bạn biết command làm gì, locatorCss tìm phần tử thích hợp trong DOMtype chọn hành động cụ thể.
  • nrOfPages : Thuộc tính này sẽ chứa một số nguyên mà ứng dụng của bạn sẽ sử dụng để xác định số lần commands sẽ lặp lại. Ví dụ: books.toscrape.com chỉ hiển thị 20 cuốn sách mỗi trang, vì vậy để có tất cả 400 cuốn sách trên tất cả 20 trang, bạn sẽ sử dụng thuộc tính này để lặp lại các commands hiện có 20 lần.

Trong khối mã này, bạn cũng đã gán các thuộc tính đối tượng đã nhận cho các biến số tạo url , existingCommandsnrOfPages . Sau đó, bạn tạo thêm hai biến: allBooksbooksDetails . Bạn sẽ sử dụng biến allBooks để lưu trữ metadata cho tất cả các sách đã truy xuất và biến booksDetails để lưu trữ chi tiết sách bị thiếu cho từng cuốn sách cụ thể.

Đến đây bạn đã sẵn sàng để thêm một vài phương thức vào lớp PuppeteerManager . Lớp này sẽ có các phương thức sau: runPuppeteer() , executeCommand() , sleep() , getAllBooks()getBooksDetails() . Bởi vì những phương pháp này tạo thành cốt lõi của ứng dụng cạp của bạn, nên bạn nên kiểm tra từng phương pháp một.

Mã hóa phương thức runPuppeteer()

Phương thức đầu tiên bên trong lớp PuppeteerManagerrunPuppeteer() . Điều này sẽ yêu cầu module Puppeteer và chạy version trình duyệt của bạn.

Ở cuối lớp PuppeteerManager , hãy thêm mã sau:

puppeteerManager.js
. . .     async runPuppeteer() {         const puppeteer = require('puppeteer')         let commands = []         if (this.nrOfPages > 1) {             for (let i = 0; i < this.nrOfPages; i++) {                 if (i < this.nrOfPages - 1) {                     commands.push(...this.existingCommands)                 } else {                     commands.push(this.existingCommands[0])                 }             }         } else {             commands = this.existingCommands         }         console.log('commands length', commands.length)     } 

Trong khối mã này, bạn đã tạo phương thức runPuppeteer() . Đầu tiên, bạn yêu cầu module puppeteer và sau đó tạo một biến bắt đầu bằng một mảng trống được gọi là commands . Sử dụng logic có điều kiện, bạn đã nói rằng nếu số lượng trang cần xử lý lớn hơn một, mã sẽ lặp qua nrOfPages và thêm các commands existingCommands cho mỗi trang vào mảng commands . Tuy nhiên, khi đến trang cuối cùng, nó không thêm command cuối cùng trong mảng Các commands existingCommands mảng commandscommand cuối cùng nhấp vào nút trang tiếp theo .

Bước tiếp theo là tạo một version trình duyệt.

Ở cuối phương thức runPuppeteer() mà bạn vừa tạo, hãy thêm mã sau:

puppeteerManager.js
. . .     async runPuppeteer() {         . . .          const browser = await puppeteer.launch({             headless: true,             args: [                 "--no-sandbox",                 "--disable-gpu",             ]         });         let page = await browser.newPage()          . . .     } 

Trong khối mã này, bạn đã tạo một version browser bằng phương thức puppeteer.launch() . Bạn đang chỉ định rằng version chạy ở chế độ headless . Đây là tùy chọn mặc định và cần thiết cho dự án này vì bạn đang chạy ứng dụng trên Kubernetes. Hai đối số tiếp theo là tiêu chuẩn khi tạo trình duyệt không có giao diện user đồ họa. Cuối cùng, bạn đã tạo một đối tượng page mới bằng phương thức browser.newPage() của Puppeteer . Phương thức .launch() trả về một Promise , yêu cầu từ khóa await .

Đến đây bạn đã sẵn sàng để thêm một số hành vi vào đối tượng page mới của bạn , bao gồm cả cách nó sẽ chuyển một URL.

Ở cuối phương thức runPuppeteer() , hãy thêm mã sau:

puppeteerManager.js
. . .     async runPuppeteer() {         . . .          await page.setRequestInterception(true);         page.on('request', (request) => {             if (['image'].indexOf(request.resourceType()) !== -1) {                 request.abort();             } else {                 request.continue();             }         });          await page.on('console', msg => {             for (let i = 0; i < msg._args.length; ++i) {                 msg._args[i].jsonValue().then(result => {                     console.log(result);                 })             }         });          await page.goto(this.url);          . . .     } 

Trong khối mã này, đối tượng page chặn tất cả các yêu cầu bằng phương thức page.setRequestInterception() của Puppeteer và nếu yêu cầu là tải một image , nó sẽ ngăn hình ảnh tải, do đó giảm thời gian cần thiết để tải một trang web. Sau đó, đối tượng page chặn bất kỳ nỗ lực nào để hiển thị thông báo trong ngữ cảnh trình duyệt bằng cách sử dụng sự kiện Puppeteer page.on('console') . Sau đó, page chuyển đến một url nhất định bằng phương thức page.goto() .

Bây giờ, hãy thêm một số hành vi khác vào đối tượng page của bạn để kiểm soát cách nó tìm thấy các phần tử trong DOM và chạy các lệnh trên chúng.

Ở cuối phương thức runPuppeteer() thêm mã sau:

puppeteerManager.js
. . .     async runPuppeteer() {         . . .          let timeout = 6000         let commandIndex = 0         while (commandIndex < commands.length) {             try {                 console.log(`command ${(commandIndex + 1)}/${commands.length}`)                 let frames = page.frames()                 await frames[0].waitForSelector(commands[commandIndex].locatorCss, { timeout: timeout })                 await this.executeCommand(frames[0], commands[commandIndex])                 await this.sleep(1000)             } catch (error) {                 console.log(error)                 break             }             commandIndex++         }         console.log('done')         await browser.close()     } 

Trong khối mã này, bạn đã tạo hai biến, timeoutcommandIndex . Biến đầu tiên sẽ giới hạn khoảng thời gian mà mã sẽ đợi một phần tử trên trang web và biến thứ hai kiểm soát cách bạn sẽ lặp qua mảng commands .

Bên while vòng lặp while, mã đi qua mọi command trong mảng commands . Đầu tiên, bạn đang tạo một mảng gồm tất cả các khung được gắn vào trang bằng phương thức page.frames() . Nó tìm kiếm một phần tử DOM trong một đối tượng frame của một page bằng cách sử dụng phương thức frame.waitForSelector() và thuộc tính locatorCss . Nếu một phần tử được tìm thấy, nó sẽ gọi phương thức executeCommand() và chuyển frame và đối tượng command làm tham số. Sau khi executeCommand trả về, nó gọi phương thức sleep() , làm cho mã đợi 1 giây trước khi thực hiện command tiếp theo. Cuối cùng, khi không còn lệnh nào nữa, version browser đóng lại.

Điều này hoàn thành phương thức runPuppeteer() của bạn. Đến đây, file puppeteerManager.js của bạn sẽ giống như sau:

puppeteerManager.js
class PuppeteerManager {     constructor(args) {         this.url = args.url         this.existingCommands = args.commands         this.nrOfPages = args.nrOfPages         this.allBooks = [];         this.booksDetails = {}     }      async runPuppeteer() {         const puppeteer = require('puppeteer')         let commands = []         if (this.nrOfPages > 1) {             for (let i = 0; i < this.nrOfPages; i++) {                 if (i < this.nrOfPages - 1) {                     commands.push(...this.existingCommands)                 } else {                     commands.push(this.existingCommands[0])                 }             }         } else {             commands = this.existingCommands         }         console.log('commands length', commands.length)          const browser = await puppeteer.launch({             headless: true,             args: [                 "--no-sandbox",                 "--disable-gpu",             ]         });          let page = await browser.newPage()         await page.setRequestInterception(true);         page.on('request', (request) => {             if (['image'].indexOf(request.resourceType()) !== -1) {                 request.abort();             } else {                 request.continue();             }         });          await page.on('console', msg => {             for (let i = 0; i < msg._args.length; ++i) {                 msg._args[i].jsonValue().then(result => {                     console.log(result);                 })              }         });          await page.goto(this.url);          let timeout = 6000         let commandIndex = 0         while (commandIndex < commands.length) {             try {                  console.log(`command ${(commandIndex + 1)}/${commands.length}`)                 let frames = page.frames()                 await frames[0].waitForSelector(commands[commandIndex].locatorCss, { timeout: timeout })                 await this.executeCommand(frames[0], commands[commandIndex])                 await this.sleep(1000)             } catch (error) {                 console.log(error)                 break             }             commandIndex++         }         console.log('done')         await browser.close();     } } 

Đến đây bạn đã sẵn sàng để viết mã phương thức thứ hai cho puppeteerManager.js : executeCommand() .

Mã hóa phương thức executeCommand()

Sau khi tạo phương thức runPuppeteer() , bây giờ là lúc tạo phương thức executeCommand() . Phương thức này chịu trách nhiệm quyết định những hành động Puppeteer sẽ thực hiện, như nhấp vào nút hoặc phân tích cú pháp một hoặc nhiều phần tử DOM .

Ở cuối lớp PuppeteerManager thêm mã sau:

puppeteerManager.js
. . .     async executeCommand(frame, command) {         await console.log(command.type, command.locatorCss)         switch (command.type) {             case "click":                 break;             case "getItems":                 break;             case "getItemDetails":                 break;         }     } 

Trong khối mã này, bạn đã tạo phương thức executeCommand() . Phương thức này mong đợi hai đối số, một đối tượng frame sẽ chứa các phần tử trang và một đối tượng command sẽ chứa các lệnh. Phương thức này bao gồm một câu lệnh switch với các trường hợp sau: click , getItemsgetItemDetails .

Xác định trường hợp click .

Thay thế break; bên dưới case "click": với mã sau:

puppeteerManager.js
    async executeCommand(frame, command) {         . . .             case "click":                 try {                     await frame.$eval(command.locatorCss, element => element.click());                     return true                 } catch (error) {                     console.log("error", error)                     return false                 }         . . .             } 

Mã của bạn sẽ kích hoạt trường hợp click khi command.type bằng click . Khối mã này chịu trách nhiệm nhấp vào nút tiếp theo để di chuyển qua danh sách sách được phân trang.

Bây giờ lập trình câu lệnh case tiếp theo.

Thay thế break; bên dưới case "getItems": với mã sau:

puppeteerManager.js
    async executeCommand(frame, command) {         . . .             case "getItems":                 try {                     let books = await frame.evaluate((command) => {                         function wordToNumber(word) {                             let number = 0                             let words = ["zero","one","two","three","four","five"]                             for(let n=0;n<words.length;words++){                                 if(word == words[n]){                                     number = n                                     break                                 }                             }                             return number                         }                          try {                             let parsedItems = [];                             let items = document.querySelectorAll(command.locatorCss);                             items.forEach((item) => {                                 let link = 'http://books.toscrape.com/catalogue/' + item.querySelector('div.image_container a').getAttribute('href').replace('catalogue/', '')<^>                                 let starRating = item.querySelector('p.star-rating').getAttribute('class').replace('star-rating ', '').toLowerCase().trim()                                 let title = item.querySelector('h3 a').getAttribute('title')                                 let price = item.querySelector('p.price_color').innerText.replace('£', '').trim()                                 let book = {                                     title: title,                                     price: parseInt(price),                                     rating: wordToNumber(starRating),                                     url: link                                 }                                 parsedItems.push(book)                             })                             return parsedItems;                         } catch (error) {                             console.log(error)                         }                     }, command).then(result => {                         this.allBooks.push.apply(this.allBooks, result)                         console.log('allBooks length ', this.allBooks.length)                     })                     return true                 } catch (error) {                     console.log("error", error)                     return false                 }         . . .     } 

Trường hợp getItems sẽ kích hoạt khi command.type bằng với getItems . Bạn đang sử dụng phương thức frame.evaluate() để chuyển đổi ngữ cảnh của trình duyệt và sau đó tạo một hàm có tên là wordToNumber() . Hàm này sẽ chuyển đổi starRating của một cuốn sách từ một chuỗi thành một số nguyên. Sau đó, mã sẽ sử dụng phương thức document.querySelectorAll() để phân tích cú pháp và trùng với DOM và truy xuất metadata của sách được hiển thị trong frame nhất định của trang web. Khi metadata được truy xuất, mã sẽ thêm nó vào mảng allBooks .

Đến đây bạn có thể xác định câu lệnh case cuối cùng.

Thay thế break; bên dưới case "getItemDetails" với mã sau:

puppeteerManager.js
    async executeCommand(frame, command) {         . . .             case "getItemDetails":                 try {                     this.booksDetails = JSON.parse(JSON.stringify(await frame.evaluate((command) => {                         try {                             let item = document.querySelector(command.locatorCss);                             let description = item.querySelector('.product_page > p:nth-child(3)').innerText.trim()                             let upc = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2)')                                 .innerText.trim()                             let nrOfReviews = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(7) > td:nth-child(2)')                                 .innerText.trim()                             let availability = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(6) > td:nth-child(2)')                                 .innerText.replace('In stock (', '').replace(' available)', '')                             let details = {                                 description: description,                                 upc: upc,                                 nrOfReviews: parseInt(nrOfReviews),                                 availability: parseInt(availability)                             }                             return details;                         } catch (error) {                             console.log(error)                             return error                         }                      }, command)))                     console.log(this.booksDetails)                     return true                 } catch (error) {                     console.log("error", error)                     return false                 }     } 

Trường hợp getItemDetails sẽ kích hoạt khi command.type bằng getItemDetails . Bạn đã sử dụng lại các phương thức frame.evaluate().querySelector() để chuyển đổi ngữ cảnh trình duyệt và phân tích cú pháp DOM . Nhưng lần này, bạn truy xuất các chi tiết còn thiếu cho mỗi cuốn sách trong một frame nhất định của trang web. Sau đó, bạn đã gán các chi tiết còn thiếu này cho đối tượng booksDetails .

Điều này hoàn thành phương thức executeCommand() của bạn. Tệp puppeteerManager.js của bạn bây giờ sẽ giống như sau:

puppeteerManager.js
class PuppeteerManager {     constructor(args) {         this.url = args.url         this.existingCommands = args.commands         this.nrOfPages = args.nrOfPages         this.allBooks = [];         this.booksDetails = {}     }      async runPuppeteer() {         const puppeteer = require('puppeteer')         let commands = []         if (this.nrOfPages > 1) {             for (let i = 0; i < this.nrOfPages; i++) {                 if (i < this.nrOfPages - 1) {                     commands.push(...this.existingCommands)                 } else {                     commands.push(this.existingCommands[0])                 }             }         } else {             commands = this.existingCommands         }         console.log('commands length', commands.length)          const browser = await puppeteer.launch({             headless: true,             args: [                 "--no-sandbox",                 "--disable-gpu",             ]         });          let page = await browser.newPage()         await page.setRequestInterception(true);         page.on('request', (request) => {             if (['image'].indexOf(request.resourceType()) !== -1) {                 request.abort();             } else {                 request.continue();             }         });          await page.on('console', msg => {             for (let i = 0; i < msg._args.length; ++i) {                 msg._args[i].jsonValue().then(result => {                     console.log(result);                 })              }         });          await page.goto(this.url);          let timeout = 6000         let commandIndex = 0         while (commandIndex < commands.length) {             try {                  console.log(`command ${(commandIndex + 1)}/${commands.length}`)                 let frames = page.frames()                 await frames[0].waitForSelector(commands[commandIndex].locatorCss, { timeout: timeout })                 await this.executeCommand(frames[0], commands[commandIndex])                 await this.sleep(1000)             } catch (error) {                 console.log(error)                 break             }             commandIndex++         }         console.log('done')         await browser.close();     }      async executeCommand(frame, command) {         await console.log(command.type, command.locatorCss)         switch (command.type) {             case "click":                 try {                     await frame.$eval(command.locatorCss, element => element.click());                     return true                 } catch (error) {                     console.log("error", error)                     return false                 }             case "getItems":                 try {                     let books = await frame.evaluate((command) => {                         function wordToNumber(word) {                             let number = 0                             let words = ["zero","one","two","three","four","five"]                             for(let n=0;n<words.length;words++){                                 if(word == words[n]){                                     number = n                                     break                                 }                             }                               return number                         }                         try {                             let parsedItems = [];                             let items = document.querySelectorAll(command.locatorCss);                              items.forEach((item) => {                                 let link = 'http://books.toscrape.com/catalogue/' + item.querySelector('div.image_container a').getAttribute('href').replace('catalogue/', '')                                 let starRating = item.querySelector('p.star-rating').getAttribute('class').replace('star-rating ', '').toLowerCase().trim()                                 let title = item.querySelector('h3 a').getAttribute('title')                                 let price = item.querySelector('p.price_color').innerText.replace('£', '').trim()                                 let book = {                                     title: title,                                     price: parseInt(price),                                     rating: wordToNumber(starRating),                                     url: link                                 }                                 parsedItems.push(book)                             })                             return parsedItems;                         } catch (error) {                             console.log(error)                         }                     }, command).then(result => {                         this.allBooks.push.apply(this.allBooks, result)                         console.log('allBooks length ', this.allBooks.length)                     })                     return true                 } catch (error) {                     console.log("error", error)                     return false                 }             case "getItemDetails":                 try {                     this.booksDetails = JSON.parse(JSON.stringify(await frame.evaluate((command) => {                         try {                             let item = document.querySelector(command.locatorCss);                             let description = item.querySelector('.product_page > p:nth-child(3)').innerText.trim()                             let upc = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2)')                                 .innerText.trim()                             let nrOfReviews = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(7) > td:nth-child(2)')                                 .innerText.trim()                             let availability = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(6) > td:nth-child(2)')                                 .innerText.replace('In stock (', '').replace(' available)', '')                             let details = {                                 description: description,                                 upc: upc,                                 nrOfReviews: parseInt(nrOfReviews),                                 availability: parseInt(availability)                             }                             return details;                         } catch (error) {                             console.log(error)                             return error                         }                      }, command)))                      console.log(this.booksDetails)                     return true                 } catch (error) {                     console.log("error", error)                     return false                 }         }     } } 

Đến đây bạn đã sẵn sàng tạo phương thức thứ ba cho lớp PuppeteerManager của bạn : sleep() .

Mã hóa phương sleep()

Với phương thức executeCommand() được tạo, bước tiếp theo của bạn là tạo phương thức sleep() . Phương pháp này sẽ làm cho mã của bạn đợi một khoảng thời gian cụ thể trước khi thực thi dòng mã tiếp theo. Điều này là cần thiết để giảm crawl rate . Nếu không có biện pháp phòng ngừa này, ví dụ, người quét có thể nhấp vào một nút trên trang A và sau đó tìm kiếm một phần tử trên trang B trước khi trang B tải.

Ở cuối lớp PuppeteerManager thêm mã sau:

puppeteerManager.js
. . .     sleep(ms) {         return new Promise(resolve => setTimeout(resolve, ms))     } 

Bạn đang chuyển một số nguyên cho phương thức sleep() . Số nguyên này là khoảng thời gian tính bằng mili giây mà mã phải đợi.

Bây giờ viết mã hai phương thức cuối cùng bên trong lớp PuppeteerManager : getAllBooks()getBooksDetails() .

Mã hóa các phương thức getAllBooks()getBooksDetails()

Sau khi tạo phương thức sleep() , hãy tạo phương thức getAllBooks() . Một hàm bên trong file server.js sẽ gọi hàm này. getAllBooks() chịu trách nhiệm gọi runPuppeteer() , lấy sách được hiển thị trên một số trang nhất định, sau đó trả lại sách đã truy xuất cho hàm đã gọi nó trong file server.js .

Ở cuối lớp PuppeteerManager thêm mã sau:

puppeteerManager.js
. . .     async getAllBooks() {         await this.runPuppeteer()         return this.allBooks     } 

Lưu ý cách khối này sử dụng một Lời hứa khác.

Đến đây bạn có thể tạo phương thức cuối cùng: getBooksDetails() . Giống như getAllBooks() , một hàm bên trong server.js sẽ gọi hàm này. getBooksDetails() tuy nhiên, chịu trách nhiệm truy xuất các chi tiết còn thiếu cho mỗi cuốn sách. Nó cũng sẽ trả về các chi tiết này cho hàm đã gọi nó trong file server.js .

Ở cuối lớp PuppeteerManager thêm mã sau:

puppeteerManager.js
. . .     async getBooksDetails() {         await this.runPuppeteer()         return this.booksDetails     } 

Đến đây bạn đã hoàn tất mã hóa file puppeteerManager.js của bạn .

Sau khi thêm năm phương pháp được mô tả trong phần này, file hoàn chỉnh của bạn sẽ giống như sau:

puppeteerManager.js
class PuppeteerManager {     constructor(args) {         this.url = args.url         this.existingCommands = args.commands         this.nrOfPages = args.nrOfPages         this.allBooks = [];         this.booksDetails = {}     }      async runPuppeteer() {         const puppeteer = require('puppeteer')         let commands = []         if (this.nrOfPages > 1) {             for (let i = 0; i < this.nrOfPages; i++) {                 if (i < this.nrOfPages - 1) {                     commands.push(...this.existingCommands)                 } else {                     commands.push(this.existingCommands[0])                 }             }         } else {             commands = this.existingCommands         }         console.log('commands length', commands.length)          const browser = await puppeteer.launch({             headless: true,             args: [                 "--no-sandbox",                 "--disable-gpu",             ]         });          let page = await browser.newPage()         await page.setRequestInterception(true);         page.on('request', (request) => {             if (['image'].indexOf(request.resourceType()) !== -1) {                 request.abort();             } else {                 request.continue();             }         });          await page.on('console', msg => {             for (let i = 0; i < msg._args.length; ++i) {                 msg._args[i].jsonValue().then(result => {                     console.log(result);                 })              }         });          await page.goto(this.url);          let timeout = 6000         let commandIndex = 0         while (commandIndex < commands.length) {             try {                  console.log(`command ${(commandIndex + 1)}/${commands.length}`)                 let frames = page.frames()                 await frames[0].waitForSelector(commands[commandIndex].locatorCss, { timeout: timeout })                 await this.executeCommand(frames[0], commands[commandIndex])                 await this.sleep(1000)             } catch (error) {                 console.log(error)                 break             }             commandIndex++         }         console.log('done')         await browser.close();     }      async executeCommand(frame, command) {         await console.log(command.type, command.locatorCss)         switch (command.type) {             case "click":                 try {                     await frame.$eval(command.locatorCss, element => element.click());                     return true                 } catch (error) {                     console.log("error", error)                     return false                 }             case "getItems":                 try {                     let books = await frame.evaluate((command) => {                         function wordToNumber(word) {                             let number = 0                             let words = ["zero","one","two","three","four","five"]                             for(let n=0;n<words.length;words++){                                 if(word == words[n]){                                     number = n                                     break                                 }                             }                               return number                         }                          try {                             let parsedItems = [];                             let items = document.querySelectorAll(command.locatorCss);                              items.forEach((item) => {                                 let link = 'http://books.toscrape.com/catalogue/' + item.querySelector('div.image_container a').getAttribute('href').replace('catalogue/', '')                                 let starRating = item.querySelector('p.star-rating').getAttribute('class').replace('star-rating ', '').toLowerCase().trim()                                 let title = item.querySelector('h3 a').getAttribute('title')                                 let price = item.querySelector('p.price_color').innerText.replace('£', '').trim()                                 let book = {                                     title: title,                                     price: parseInt(price),                                     rating: wordToNumber(starRating),                                     url: link                                 }                                 parsedItems.push(book)                             })                             return parsedItems;                         } catch (error) {                             console.log(error)                         }                     }, command).then(result => {                         this.allBooks.push.apply(this.allBooks, result)                         console.log('allBooks length ', this.allBooks.length)                     })                     return true                 } catch (error) {                     console.log("error", error)                     return false                 }             case "getItemDetails":                 try {                     this.booksDetails = JSON.parse(JSON.stringify(await frame.evaluate((command) => {                         try {                             let item = document.querySelector(command.locatorCss);                             let description = item.querySelector('.product_page > p:nth-child(3)').innerText.trim()                             let upc = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(1) > td:nth-child(2)')                                 .innerText.trim()                             let nrOfReviews = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(7) > td:nth-child(2)')                                 .innerText.trim()                             let availability = item.querySelector('.table > tbody:nth-child(1) > tr:nth-child(6) > td:nth-child(2)')                                 .innerText.replace('In stock (', '').replace(' available)', '')                             let details = {                                 description: description,                                 upc: upc,                                 nrOfReviews: parseInt(nrOfReviews),                                 availability: parseInt(availability)                             }                             return details;                         } catch (error) {                             console.log(error)                             return error                         }                      }, command)))                      console.log(this.booksDetails)                     return true                 } catch (error) {                     console.log("error", error)                     return false                 }         }     }      sleep(ms) {         return new Promise(resolve => setTimeout(resolve, ms))     }      async getAllBooks() {         await this.runPuppeteer()         return this.allBooks     }      async getBooksDetails() {         await this.runPuppeteer()         return this.booksDetails     } }  module.exports = { PuppeteerManager } 

Trong bước này, bạn đã sử dụng module Puppeteer để tạo file puppeteerManager.js . Tệp này tạo thành cốt lõi của trình quét của bạn. Trong phần tiếp theo, bạn sẽ tạo file server.js .

Bước 4 - Xây dựng file Scraper thứ hai

Trong bước này, bạn sẽ tạo file server.js - nửa sau của server ứng dụng của bạn. Tệp này sẽ nhận được các yêu cầu có chứa thông tin sẽ hướng dữ liệu nào cần cạo và sau đó trả lại dữ liệu đó cho client .

Tạo file server.js và mở nó:

  • nano server.js

Thêm mã sau:

server.js
const express = require('express'); const bodyParser = require('body-parser') const os = require('os');  const PORT = 5000; const app = express(); let timeout = 1500000  app.use(bodyParser.urlencoded({ extended: true })) app.use(bodyParser.json())  let browsers = 0 let maxNumberOfBrowsers = 5 

Trong khối mã này, bạn yêu cầu module expressbody-parser . Các module này là cần thiết để tạo một server ứng dụng có khả năng xử lý HTTP yêu cầu HTTP . Mô-đun express sẽ tạo một server ứng dụng và module body-parser sẽ phân tích cú pháp các phần thân yêu cầu đến trong một phần mềm trung gian trước khi nhận nội dung của phần thân. Sau đó bạn có yêu cầu os module, mà sẽ lấy tên của máy chạy ứng dụng của bạn. Sau đó, bạn đã chỉ định một cổng cho ứng dụng và tạo các browsers biến và maxNumberOfBrowsers . Các biến này sẽ giúp quản lý số lượng version trình duyệt mà server có thể tạo. Trong trường hợp này, ứng dụng bị giới hạn trong việc tạo năm version trình duyệt, nghĩa là trình quét sẽ có thể truy xuất dữ liệu từ năm trang đồng thời.

Web server của ta sẽ có các tuyến sau: / , /api/books/api/booksDetails .

Ở cuối file server.js của bạn, hãy xác định / route bằng mã sau:

server.js
. . .  app.get('/', (req, res) => {   console.log(os.hostname())   let response = {     msg: 'hello world',     hostname: os.hostname().toString()   }   res.send(response); }); 

Bạn sẽ sử dụng / route để kiểm tra xem server ứng dụng của bạn có đang chạy hay không. Một yêu cầu GET được gửi đến tuyến đường này sẽ trả về một đối tượng chứa hai thuộc tính: msg , đối tượng này sẽ chỉ nói “hello world” và hostname , sẽ xác định máy nơi một version của server ứng dụng đang chạy.

Bây giờ hãy xác định lộ trình /api/books .

Ở cuối file server.js của bạn, hãy thêm mã sau:

server.js
. . .  app.post('/api/books', async (req, res) => {   req.setTimeout(timeout);   try {     let data = req.body     console.log(req.body.url)     while (browsers == maxNumberOfBrowsers) {       await sleep(1000)     }     await getBooksHandler(data).then(result => {       let response = {         msg: 'retrieved books ',         hostname: os.hostname(),         books: result       }       console.log('done')       res.send(response)     })   } catch (error) {     res.send({ error: error.toString() })   } }); 

Tuyến /api/books sẽ yêu cầu người quét truy xuất metadata liên quan đến sách trên một trang web nhất định. Một yêu cầu POST tới tuyến đường này sẽ kiểm tra xem số lượng browsers đang chạy có bằng với maxNumberOfBrowsers , và nếu không, nó sẽ gọi phương thức getBooksHandler() . Phương thức này sẽ tạo một version mới của lớp PuppeteerManager và truy xuất metadata của cuốn sách. Khi nó đã truy xuất metadata , nó sẽ trả về trong phần nội dung phản hồi cho client . Đối tượng phản hồi sẽ chứa một chuỗi, msg , đọc retrieved books , một mảng, books , chứa metadata và một chuỗi khác, hostname , sẽ trả về tên của máy / containers / pod nơi ứng dụng đang chạy.

Ta có một tuyến đường cuối cùng để xác định: /api/booksDetails .

Thêm mã sau vào cuối file server.js của bạn:

server.js
. . .  app.post('/api/booksDetails', async (req, res) => {   req.setTimeout(timeout);   try {     let data = req.body     console.log(req.body.url)     while (browsers == maxNumberOfBrowsers) {       await sleep(1000)     }     await getBookDetailsHandler(data).then(result => {       let response = {         msg: 'retrieved book details',         hostname: os.hostname(),         url: req.body.url,         booksDetails: result       }       console.log('done', response)       res.send(response)     })   } catch (error) {     res.send({ error: error.toString() })   } }); 

Gửi một yêu cầu POST tới tuyến /api/booksDetails sẽ yêu cầu người /api/booksDetails lấy thông tin còn thiếu cho một cuốn sách nhất định. Server ứng dụng sẽ kiểm tra xem số lượng browsers đang chạy có bằng với số lượng tối đa của browsers maxNumberOfBrowsers . Nếu đúng, nó sẽ gọi phương thức sleep() và đợi 1 giây trước khi kiểm tra lại, còn nếu không bằng, nó sẽ gọi phương thức getBookDetailsHandler() . Giống như phương thức getBooksHandler() , phương thức này sẽ tạo một thể hiện mới của lớp PuppeteerManager và lấy thông tin còn thiếu.

Sau đó, chương trình sẽ trả lại dữ liệu đã truy xuất trong phần thân phản hồi cho client . Đối tượng phản hồi sẽ chứa một chuỗi, msg , cho biết retrieved book details , một chuỗi, hostname , sẽ trả về tên của máy đang chạy ứng dụng và một chuỗi khác, url , chứa URL của trang dự án. Nó cũng sẽ chứa một mảng, booksDetails , chứa tất cả thông tin bị thiếu cho một cuốn sách.

Web server của bạn cũng sẽ có các chức năng sau: getBooksHandler() , getBookDetailsHandler()sleep() .

Bắt đầu với hàm getBooksHandler() .

Ở cuối file server.js của bạn, hãy thêm mã sau:

server.js
. . .  async function getBooksHandler(arg) {   let pMng = require('./puppeteerManager')   let puppeteerMng = new pMng.PuppeteerManager(arg)   browsers += 1   try {     let books = await puppeteerMng.getAllBooks().then(result => {       return result     })     browsers -= 1     return books   } catch (error) {     browsers -= 1     console.log(error)   } } 

Hàm getBooksHandler() sẽ tạo một version mới của lớp PuppeteerManager . Nó sẽ tăng số lượng browsers đang chạy, chuyển đối tượng chứa thông tin cần thiết để truy xuất sách, sau đó gọi phương thức getAllBooks() . Sau khi dữ liệu được truy xuất, nó giảm số lượng browsers đang chạy và sau đó trả lại dữ liệu mới được truy xuất về tuyến đường /api/books .

Bây giờ thêm đoạn mã sau để xác định hàm getBookDetailsHandler() :

server.js
. . .  async function getBookDetailsHandler(arg) {   let pMng = require('./puppeteerManager')   let puppeteerMng = new pMng.PuppeteerManager(arg)   browsers += 1   try {     let booksDetails = await puppeteerMng.getBooksDetails().then(result => {       return result     })     browsers -= 1     return booksDetails   } catch (error) {     browsers -= 1     console.log(error)   } } 

Hàm getBookDetailsHandler() sẽ tạo một version mới của lớp PuppeteerManager . Nó hoạt động giống như hàm getBooksHandler() ngoại trừ nó xử lý metadata bị thiếu cho mỗi cuốn sách và trả về tuyến đường /api/booksDetails .

Ở cuối file server.js của bạn, hãy thêm mã sau để xác định hàm sleep() :

server.js
  function sleep(ms) {     console.log(' running maximum number of browsers')     return new Promise(resolve => setTimeout(resolve, ms))   } 

Hàm sleep() làm cho mã chờ trong một khoảng thời gian cụ thể khi số lượng browsers bằng maxNumberOfBrowsers . Ta truyền một số nguyên cho hàm này và số nguyên này đại diện cho lượng thời gian tính bằng mili giây mà mã sẽ đợi cho đến khi nó có thể kiểm tra xem browsers có bằng với maxNumberOfBrowsers .

Tệp của bạn đã hoàn tất.

Sau khi tạo tất cả các tuyến và chức năng cần thiết, file server.js sẽ giống như sau:

server.js
const express = require('express'); const bodyParser = require('body-parser') const os = require('os');  const PORT = 5000; const app = express(); let timeout = 1500000  app.use(bodyParser.urlencoded({ extended: true })) app.use(bodyParser.json())  let browsers = 0 let maxNumberOfBrowsers = 5  app.get('/', (req, res) => {   console.log(os.hostname())   let response = {     msg: 'hello world',     hostname: os.hostname().toString()   }   res.send(response); });  app.post('/api/books', async (req, res) => {   req.setTimeout(timeout);   try {     let data = req.body     console.log(req.body.url)     while (browsers == maxNumberOfBrowsers) {       await sleep(1000)     }     await getBooksHandler(data).then(result => {       let response = {         msg: 'retrieved books ',         hostname: os.hostname(),         books: result       }       console.log('done')       res.send(response)     })   } catch (error) {     res.send({ error: error.toString() })   } });   app.post('/api/booksDetails', async (req, res) => {   req.setTimeout(timeout);   try {     let data = req.body     console.log(req.body.url)     while (browsers == maxNumberOfBrowsers) {       await sleep(1000)     }     await getBookDetailsHandler(data).then(result => {       let response = {         msg: 'retrieved book details',         hostname: os.hostname(),         url: req.body.url,         booksDetails: result       }       console.log('done', response)       res.send(response)     })   } catch (error) {     res.send({ error: error.toString() })   } });  async function getBooksHandler(arg) {   let pMng = require('./puppeteerManager')   let puppeteerMng = new pMng.PuppeteerManager(arg)   browsers += 1   try {     let books = await puppeteerMng.getAllBooks().then(result => {       return result     })     browsers -= 1     return books   } catch (error) {     browsers -= 1     console.log(error)   } }  async function getBookDetailsHandler(arg) {   let pMng = require('./puppeteerManager')   let puppeteerMng = new pMng.PuppeteerManager(arg)   browsers += 1   try {     let booksDetails = await puppeteerMng.getBooksDetails().then(result => {       return result     })     browsers -= 1     return booksDetails   } catch (error) {     browsers -= 1     console.log(error)   } }  function sleep(ms) {   console.log(' running maximum number of browsers')   return new Promise(resolve => setTimeout(resolve, ms)) }  app.listen(PORT); console.log(`Running on port: ${PORT}`); 

Ở bước này, bạn đã hoàn thành việc tạo server ứng dụng. Trong bước tiếp theo, bạn sẽ tạo một hình ảnh cho server ứng dụng và sau đó triển khai nó vào cụm Kubernetes của bạn.

Bước 5 - Xây dựng Docker image

Trong bước này, bạn sẽ tạo một Docker image chứa ứng dụng cạp của bạn. Trong Bước 6, bạn sẽ triển khai hình ảnh đó vào một cụm Kubernetes.

Để tạo Docker image cho ứng dụng của bạn, bạn cần tạo Dockerfile và sau đó xây dựng containers .

Đảm bảo rằng bạn vẫn ở trong folder ./server .

Bây giờ tạo Dockerfile và mở nó:

  • nano Dockerfile

Viết mã sau bên trong Dockerfile :

Dockerfile
FROM node:10  RUN apt-get update  RUN apt-get install -yyq ca-certificates  RUN apt-get install -yyq libappindicator1 libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6  RUN apt-get install -yyq gconf-service lsb-release wget xdg-utils  RUN apt-get install -yyq fonts-liberation  WORKDIR /usr/src/app  COPY package*.json ./  RUN npm install  COPY . .  EXPOSE 5000 CMD [ "node", "server.js" ] 

Hầu hết mã trong khối này là mã dòng lệnh tiêu chuẩn cho Dockerfile. Bạn đã tạo hình ảnh từ hình ảnh node:10 . Tiếp theo, bạn đã sử dụng lệnh RUN để cài đặt các gói cần thiết để chạy Puppeteer trong containers Docker, sau đó bạn tạo folder ứng dụng. Bạn đã sao chép file package.json của scraper vào folder ứng dụng và cài đặt các phần phụ thuộc được chỉ định bên trong file package.json . Cuối cùng, bạn đã group nguồn ứng dụng, hiển thị ứng dụng trên cổng 5000 và chọn server.js làm file mục nhập.

Bây giờ, hãy tạo một file .dockerignore và mở nó. Điều này sẽ giữ cho các file nhạy cảm và không cần thiết nằm ngoài tầm kiểm soát của version .

Tạo file bằng editor bạn muốn :

  • nano .dockerignore

Thêm nội dung sau vào file :

./server/.dockerignore
node_modules npm-debug.log 

Sau khi tạo Dockerfile và file .dockerignore , bạn có thể tạo Docker image của ứng dụng và đẩy nó vào repository trong account Docker Hub của bạn. Trước khi đẩy hình ảnh, hãy kiểm tra xem bạn đã đăng nhập vào account Docker Hub của bạn chưa.

Đăng nhập vào Docker Hub:

  • docker login --username=your_username --password=your_password

Xây dựng hình ảnh:

  • docker build -t your_username/concurrent-scraper .

Bây giờ là lúc để kiểm tra cạp. Trong thử nghiệm này, bạn sẽ gửi một yêu cầu cho mỗi tuyến đường.

Đầu tiên, hãy khởi động ứng dụng:

  • docker run -p 5000:5000 -d your_username/concurrent-scraper

Bây giờ, hãy sử dụng curl để gửi một yêu cầu GET tới / route:

  • curl http://localhost:5000/

Bằng cách gửi một yêu cầu GET tới / route, bạn sẽ nhận được phản hồi có chứa một msg hello world và một hostname . Tên hostname này là id của containers Docker của bạn. Bạn sẽ thấy một kết quả tương tự như thế này, nhưng với ID duy nhất của máy bạn:

Output
{"msg":"hello world","hostname":"0c52d53f97d3"}

Bây giờ, hãy gửi một yêu cầu POST tới tuyến /api/books để lấy metadata của tất cả các sách được hiển thị trên một trang web:

  • curl --header "Content-Type: application/json" --request POST --data '{"url": "http://books.toscrape.com/index.html" , "nrOfPages":1 , "commands":[{"description": "get items metadata", "locatorCss": ".product_pod","type": "getItems"},{"description": "go to next page","locatorCss": ".next > a:nth-child(1)","type": "Click"}]}' http://localhost:5000/api/books

Bằng cách gửi yêu cầu POST tới tuyến /api/books bạn sẽ nhận được phản hồi có chứa một msg nói rằng retrieved books , hostname tương tự với tên trong yêu cầu trước đó và mảng books chứa tất cả 20 cuốn sách được hiển thị trên trang đầu tiên của trang web books.toscrape . Bạn sẽ thấy một kết quả như thế này, nhưng với ID duy nhất của máy bạn:

Output
{"msg":"retrieved books ","hostname":"0c52d53f97d3","books":[{"title":"A Light in the Attic","price":null,"rating":0,"url":"http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"},{"title":"Tipping the Velvet","price":null,"rating":0,"url":"http://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html"}, [ . . . ] }]}

Bây giờ, hãy gửi một yêu cầu POST tới tuyến /api/booksDetails để lấy thông tin còn thiếu cho một cuốn sách ngẫu nhiên:

  • curl --header "Content-Type: application/json" --request POST --data '{"url": "http://books.toscrape.com/catalogue/slow-states-of-collapse-poems_960/index.html" , "nrOfPages":1 , "commands":[{"description": "get item details", "locatorCss": "article.product_page","type": "getItemDetails"}]}' http://localhost:5000/api/booksDetails

Bằng cách gửi yêu cầu POST đến tuyến /api/booksDetails bạn sẽ nhận được phản hồi có chứa msg cho biết retrieved book details , đối tượng booksDetails chứa thông tin chi tiết còn thiếu của sách này , url chứa địa chỉ của trang sản phẩm, cũng như hostname giống như hostname trong các yêu cầu trước. Bạn sẽ thấy một kết quả như thế này:

Output
{"msg":"retrieved book details","hostname":"0c52d53f97d3","url":"http://books.toscrape.com/catalogue/slow-states-of-collapse-poems_960/index.html","booksDetails":{"description":"The eagerly anticipated debut from one of Canada’s most exciting new poets In her debut collection, Ashley-Elizabeth Best explores the cultivation of resilience during uncertain and often trying times [...]","upc":"b4fd5943413e089a","nrOfReviews":0,"availability":17}}

Nếu các lệnh curl của bạn không trả về phản hồi chính xác, hãy đảm bảo mã trong file puppeteerManager.jsserver.js trùng với các khối mã cuối cùng trong hai bước trước đó. Ngoài ra, hãy đảm bảo containers Docker đang chạy và nó không bị lỗi. Bạn có thể thực hiện bằng cách cố gắng chạy Docker image mà không có tùy chọn -d (tùy chọn này làm cho Docker image chạy ở chế độ tách rời), sau đó gửi một yêu cầu HTTP đến một trong các tuyến.

Nếu bạn vẫn gặp lỗi khi cố gắng chạy Docker image , hãy thử dừng tất cả các containers đang chạy và chạy hình ảnh quét mà không có tùy chọn -d .

Đầu tiên dừng tất cả các containers :

  • docker stop $(docker ps -a -q)

Sau đó, chạy lệnh Docker mà không có cờ -d :

  • docker run -p 5000:5000 your_username/concurrent-scraper

Nếu bạn không gặp bất kỳ lỗi nào, hãy làm sạch cửa sổ terminal :

  • clear

Đến đây bạn đã kiểm tra thành công hình ảnh, bạn có thể gửi nó vào repository của bạn . Đẩy hình ảnh vào repository trong account Docker Hub của bạn:

  • docker push your_username/concurrent-scraper:latest

Với ứng dụng cạp của bạn hiện có sẵn dưới dạng hình ảnh trên Docker Hub, bạn đã sẵn sàng triển khai tới Kubernetes. Đây sẽ là bước tiếp theo của bạn.

Bước 6 - Triển khai Scraper cho Kubernetes

Với hình ảnh quét của bạn được xây dựng và đẩy vào repository của bạn, bây giờ bạn đã sẵn sàng để triển khai.

Đầu tiên, sử dụng kubectl để tạo một không gian tên mới có tên là concurrent-scraper-context :

  • kubectl create namespace concurrent-scraper-context

Đặt concurrent-scraper-context làm bối cảnh mặc định:

  • kubectl config set-context --current --namespace=concurrent-scraper-context

Để tạo triển khai ứng dụng của bạn, bạn cần tạo một file có tên là app-deployment.yaml k8s , nhưng trước tiên, bạn phải chuyển đến folder k8s bên trong dự án của bạn . Đây là nơi bạn sẽ lưu trữ tất cả các file Kubernetes của bạn .

Đi tới folder k8s bên trong dự án của bạn:

  • cd ../k8s

Tạo file app-deployment.yaml và mở nó:

  • nano app-deployment.yaml

Viết mã sau bên trong app-deployment.yaml . Đảm bảo thay thế your_DockerHub_username bằng tên user duy nhất của bạn:

./k8s/app-deployment.yaml
apiVersion: apps/v1 kind: Deployment metadata:   name: scraper   labels:     app: scraper spec:   replicas: 5   selector:     matchLabels:       app: scraper   template:     metadata:       labels:         app: scraper     spec:       containers:       - name: concurrent-scraper         image: your_DockerHub_username/concurrent-scraper         ports:         - containerPort: 5000 

Hầu hết mã trong khối trước là tiêu chuẩn cho file deployment Kubernetes. Trước tiên, bạn đặt tên triển khai ứng dụng của bạn thành bộ scraper , sau đó bạn đặt số lượng group thành 5 và sau đó bạn đặt tên containers của bạn thành bộ concurrent-scraper . Sau đó, bạn đã chỉ định hình ảnh mà bạn muốn sử dụng để xây dựng ứng dụng của bạn làm your_DockerHub_username /concurrent-scraper , nhưng bạn sẽ sử dụng tên user Docker Hub thực của bạn . Cuối cùng, bạn đã chỉ định rằng bạn muốn ứng dụng của bạn sử dụng cổng 5000 .

Sau khi tạo file triển khai, bạn đã sẵn sàng triển khai ứng dụng cho cụm.

Triển khai ứng dụng:

  • kubectl apply -f app-deployment.yaml

Bạn có thể theo dõi trạng thái triển khai của bạn bằng cách chạy lệnh sau:

  • kubectl get deployment -w

Sau khi chạy lệnh, bạn sẽ thấy một kết quả như sau:

Output
NAME READY UP-TO-DATE AVAILABLE AGE scraper 0/5 5 0 7s scraper 1/5 5 1 23s scraper 2/5 5 2 25s scraper 3/5 5 3 25s scraper 4/5 5 4 33s scraper 5/5 5 5 33s

Sẽ mất một vài giây để tất cả các triển khai bắt đầu chạy, nhưng khi chúng bắt đầu chạy, bạn sẽ có năm version trình quét của bạn đang chạy. Mỗi version có thể cạo năm trang đồng thời, vì vậy bạn có thể cạo 25 trang đồng thời, do đó giảm thời gian cần thiết để quét tất cả 400 trang.

Để truy cập ứng dụng của bạn từ bên ngoài cụm, bạn cần tạo một service . service này sẽ là một bộ cân bằng tải và nó sẽ yêu cầu một file có tên là load-balancer.yaml .

Tạo file load-balancer.yaml và mở nó:

  • nano load-balancer.yaml

Viết mã sau bên trong load-balancer.yaml :

load-balancer.yaml
apiVersion: v1 kind: Service metadata:   name: load-balancer   labels:     app: scraper spec:   type: LoadBalancer   ports:   - port: 80     targetPort: 5000     protocol: TCP   selector:     app: scraper 

Hầu hết mã trong khối trước là tiêu chuẩn cho file service . Đầu tiên, bạn đặt tên dịch vụ của bạn thành load-balancer . Bạn đã chỉ định loại dịch vụ và sau đó bạn làm cho dịch vụ có thể truy cập được trên cổng 80 . Cuối cùng, bạn đã chỉ định rằng dịch vụ này dành cho ứng dụng, scraper .

Đến đây bạn đã tạo file load-balancer.yaml , hãy triển khai dịch vụ cho cụm.

Triển khai dịch vụ:

  • kubectl apply -f load-balancer.yaml

Chạy lệnh sau để theo dõi trạng thái dịch vụ của bạn:

  • kubectl get services -w

Sau khi chạy lệnh này, bạn sẽ thấy một kết quả như thế này, nhưng sẽ mất vài giây để IP bên ngoài xuất hiện:

Output
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE load-balancer LoadBalancer 10.245.91.92 <pending> 80:30802/TCP 10s load-balancer LoadBalancer 10.245.91.92 161.35.252.69 80:30802/TCP 69s

EXTERNAL-IPCLUSTER-IP của dịch vụ của bạn sẽ khác với những dịch vụ ở trên. Ghi lại EXTERNAL-IP của bạn. Bạn sẽ sử dụng nó trong phần tiếp theo.

Trong bước này, bạn đã triển khai ứng dụng quét vào cụm Kubernetes của bạn . Trong bước tiếp theo, bạn sẽ tạo một ứng dụng client để tương tác với ứng dụng mới triển khai của bạn .

Bước 7 - Tạo ứng dụng client

Trong bước này, bạn sẽ xây dựng ứng dụng client của bạn , ứng dụng này sẽ yêu cầu ba file sau: main.js , lowdbHelper.jsbooks.json . Tệp main.js là file chính của ứng dụng client của bạn. Nó gửi yêu cầu đến server ứng dụng của bạn và sau đó lưu dữ liệu đã truy xuất bằng phương pháp mà bạn sẽ tạo bên trong file lowdbHelper.js . Tệp lowdbHelper.js lưu dữ liệu trong một file local và truy xuất dữ liệu trong đó. Tệp books.json là file local nơi bạn sẽ lưu tất cả dữ liệu đã cóp nhặt của bạn .

Đầu tiên hãy quay lại folder client của bạn:

  • cd ../client

Vì chúng nhỏ hơn main.js nên trước tiên bạn sẽ tạo các lowdbHelper.jsbooks.json .

Tạo và mở file có tên lowdbHelper.js :

  • nano lowdbHelper.js

Thêm mã sau vào file lowdbHelper.js :

lowdbHelper.js
const lowdb = require('lowdb') const FileSync = require('lowdb/adapters/FileSync') const adapter = new FileSync('books.json') 

Trong khối mã này, bạn đã yêu cầu module lowdb và sau đó yêu cầu bộ điều hợp FileSync mà bạn cần để lưu và đọc dữ liệu. Sau đó, bạn hướng chương trình lưu trữ dữ liệu trong file JSON có tên books.json .

Thêm mã sau vào cuối file lowdbHelper.js :

lowdbHelper.js
. . . class LowDbHelper {     constructor() {         this.db = lowdb(adapter);     }      getData() {         try {             let data = this.db.getState().books             return data         } catch (error) {             console.log('error', error)         }     }      saveData(arg) {         try {             this.db.set('books', arg).write()             console.log('data saved successfully!!!')         } catch (error) {             console.log('error', error)         }     } }  module.exports = { LowDbHelper } 

Ở đây bạn đã tạo một lớp có tên là LowDbHelper . Lớp này chứa hai phương thức sau: getData()saveData() . books.json đầu tiên sẽ truy xuất sách được lưu trong file books.json và thao tác thứ hai sẽ lưu sách của bạn vào cùng một file .

lowdbHelper.js đã hoàn thành của bạn sẽ trông giống như sau:

lowdbHelper.js
const lowdb = require('lowdb') const FileSync = require('lowdb/adapters/FileSync') const adapter = new FileSync('books.json')  class LowDbHelper {     constructor() {         this.db = lowdb(adapter);     }      getData() {         try {             let data = this.db.getState().books             return data         } catch (error) {             console.log('error', error)         }     }      saveData(arg) {         try {             this.db.set('books', arg).write()             //console.log('data saved successfully!!!')         } catch (error) {             console.log('error', error)         }     }  }  module.exports = { LowDbHelper } 

Đến đây bạn đã tạo file lowdbHelper.js , đã đến lúc tạo file books.json .

Tạo file books.json và mở nó:

  • nano books.json

Thêm mã sau:

books.json
{     "books": [] } 

Tệp books.json bao gồm một đối tượng có thuộc tính là books . Giá trị ban đầu của thuộc tính này là một mảng trống. Sau đó, khi bạn truy xuất sách, đây là nơi chương trình của bạn sẽ lưu chúng.

Đến đây bạn đã tạo lowdbHelper.jsbooks.json , bạn sẽ tạo file main.js

Tạo main.js và mở nó:

  • nano main.js

Thêm mã sau vào main.js :

main.js
let axios = require('axios') let ldb = require('./lowdbHelper.js').LowDbHelper let ldbHelper = new ldb() let allBooks = ldbHelper.getData()  let server = "http://your_load_balancer_external_ip_address" let podsWorkDone = [] let booksDetails = [] let errors = [] 

Trong đoạn mã này, bạn yêu cầu file lowdbHelper.js và một module có tên là axios . Bạn sẽ sử dụng axios để gửi HTTP yêu cầu HTTP đến bộ axios của bạn; file lowdbHelper.js sẽ lưu các sách đã truy xuất và biến allBooks sẽ lưu trữ tất cả các sách được lưu trong file books.json . Trước khi lấy bất kỳ cuốn sách nào, biến này sẽ chứa một mảng trống; biến server sẽ lưu trữ EXTERNAL-IP của bộ cân bằng tải mà bạn đã tạo trong phần trước. Đảm bảo thay thế điều này bằng IP duy nhất của bạn. Biến podsWorkDone sẽ theo dõi số lượng trang mà mỗi version trình podsWorkDone của bạn đã xử lý. Biến booksDetails sẽ lưu trữ các chi tiết được truy xuất cho từng sách và biến errors sẽ theo dõi bất kỳ lỗi nào có thể xảy ra khi cố gắng truy xuất sách.

Bây giờ ta cần xây dựng một số chức năng cho từng phần của quy trình cạp.

Thêm khối mã tiếp theo vào cuối file main.js :

main.js
. . . function main() {   let execute = process.argv[2] ? process.argv[2] : 0   execute = parseInt(execute)   switch (execute) {     case 0:       getBooks()       break;     case 1:       getBooksDetails()       break;   } } 

Đến đây bạn đang tạo một hàm được gọi là main() , bao gồm một câu lệnh switch sẽ gọi hàm getBooks() hoặc getBooksDetails() dựa trên một đầu vào được truyền vào.

Thay thế chỗ break; bên dưới getBooks() với mã sau:

main.js
. . . function getBooks() {   console.log('getting books')   let data = {     url: 'http://books.toscrape.com/index.html',     nrOfPages: 20,     commands: [       {         description: 'get items metadata',         locatorCss: '.product_pod',         type: "getItems"       },       {         description: 'go to next page',         locatorCss: '.next > a:nth-child(1)',         type: "Click"       }     ],   }   let begin = Date.now();   axios.post(`${server}/api/books`, data).then(result => {     let end = Date.now();     let timeSpent = (end - begin) / 1000 + "secs";     console.log(`took ${timeSpent} to retrieve ${result.data.books.length} books`)     ldbHelper.saveData(result.data.books)   }) } 

Ở đây bạn đã tạo một hàm có tên getBooks() . Đoạn mã này gán đối tượng chứa thông tin cần thiết để quét tất cả 20 trang vào một biến được gọi là data . Đầu tiên command trong commands mảng của đối tượng này lấy tất cả 20 cuốn sách được hiển thị trên một trang, và lần thứ hai command nhấp chuột vào nút bên cạnh trên một trang, do đó làm cho chuyển trình duyệt sang trang tiếp theo. Điều này nghĩa là command đầu tiên sẽ lặp lại 20 lần và lệnh thứ hai là 19 lần. Yêu cầu POST được gửi bằng axios tới tuyến đường /api/books sẽ gửi đối tượng này đến server ứng dụng của bạn và trình quét sau đó sẽ truy xuất metadata cơ bản cho mọi cuốn sách được hiển thị trên 20 trang đầu tiên của trang web books.toscrape . Sau đó, nó lưu dữ liệu đã truy xuất bằng lớp LowDbHelper bên trong file lowdbHelper.js .

Bây giờ hãy viết mã cho hàm thứ hai, hàm này sẽ xử lý dữ liệu sách cụ thể hơn trên các trang riêng lẻ.

Thay thế chỗ break; bên dưới getBooksDetails() với mã sau:

main.js
. . .  function getBooksDetails() {   let begin = Date.now()   for (let j = 0; j < allBooks.length; j++) {     let data = {       url: allBooks[j].url,       nrOfPages: 1,       commands: [         {           description: 'get item details',           locatorCss: 'article.product_page',           type: "getItemDetails"         }       ]     }     sendRequest(data, function (result) {       parseResult(result, begin)     })   } } 

Hàm getBooksDetails() sẽ đi qua mảng allBooks , mảng này chứa tất cả các sách và cho mỗi cuốn sách bên trong mảng này và tạo một đối tượng chứa thông tin cần thiết để quét một trang. Sau khi tạo đối tượng này, nó sẽ chuyển nó đến hàm sendRequest() . Sau đó, nó sẽ sử dụng giá trị mà hàm sendRequest() trả về và chuyển giá trị này cho một hàm có tên là parseResult() .

Thêm mã sau vào cuối file main.js :

main.js
. . .  async function sendRequest(payload, cb) {   let book = payload   try {     await axios.post(`${server}/api/booksDetails`, book).then(response => {       if (Object.keys(response.data).includes('error')) {         let res = {           url: book.url,           error: response.data.error         }         cb(res)       } else {         cb(response.data)       }     })   } catch (error) {     console.log(error)     let res = {       url: book.url,       error: error     }     cb({ res })   } } 

Đến đây bạn đang tạo một hàm có tên sendRequest() . Bạn sẽ sử dụng chức năng này để gửi tất cả 400 yêu cầu đến server ứng dụng có chứa bộ quét của bạn. Đoạn mã gán đối tượng chứa thông tin cần thiết để quét một trang vào một biến được gọi là book . Sau đó, bạn gửi đối tượng này trong một yêu cầu POST đến tuyến /api/booksDetails trên server ứng dụng của bạn. Phản hồi được gửi trở lại hàm getBooksDetails() .

Bây giờ hãy tạo hàm parseResult() .

Thêm mã sau vào cuối file main.js :

main.js
. . .  function parseResult(result, begin){   try {     let end = Date.now()     let timeSpent = (end - begin) / 1000 + "secs ";     if (!Object.keys(result).includes("error")) {       let wasSuccessful = Object.keys(result.booksDetails).length > 0 ? true : false       if (wasSuccessful) {         let podID = result.hostname         let podsIDs = podsWorkDone.length > 0 ? podsWorkDone.map(pod => { return Object.keys(pod)[0]}) : []         if (!podsIDs.includes(podID)) {           let podWork = {}           podWork[podID] = 1           podsWorkDone.push(podWork)         } else {           for (let pwd = 0; pwd < podsWorkDone.length; pwd++) {             if (Object.keys(podsWorkDone[pwd]).includes(podID)) {               podsWorkDone[pwd][podID] += 1               break             }           }         }         booksDetails.push(result)       } else {         errors.push(result)       }     } else {       errors.push(result)     }     console.log('podsWorkDone', podsWorkDone, ', retrieved ' + booksDetails.length + " books, ",       "took " + timeSpent + ", ", "used " + podsWorkDone.length + " pods", " errors: " + errors.length)     saveBookDetails()   } catch (error) {     console.log(error)   } } 

parseResult() nhận result của hàm sendRequest() chứa thông tin chi tiết về sách bị thiếu. Sau đó, nó phân tích cú pháp result và truy xuất hostname của group đã xử lý yêu cầu và gán nó cho biến podID . Nó kiểm tra xem podID này đã là một phần của mảng podsWorkDone ; nếu không, nó sẽ thêm podId vào mảng podsWorkDone và đặt số lượng công việc được thực hiện thành 1. Nhưng nếu có, nó sẽ tăng số lượng công việc được thực hiện bởi group này lên 1. Sau đó, mã sẽ thêm result đến mảng booksDetails , xuất tiến trình tổng thể của hàm getBooksDetails() , rồi gọi hàm saveBookDetails() .

Bây giờ, hãy thêm đoạn mã sau để tạo hàm saveBookDetails() :

main.js
. . .  function saveBookDetails() {   let books = ldbHelper.getData()   for (let b = 0; b < books.length; b++) {     for (let d = 0; d < booksDetails.length; d++) {       let item = booksDetails[d]       if (books[b].url === item.url) {         books[b].booksDetails = item.booksDetails         break       }     }   }   ldbHelper.saveData(books) }  main() 

saveBookDetails() nhận tất cả sách được lưu trữ trong file books.json bằng cách sử dụng lớp LowDbHelper và gán nó cho một biến gọi là books . Sau đó, nó sẽ lặp qua các mảng booksbooksDetails để xem liệu nó có tìm thấy các phần tử trong cả hai mảng có cùng thuộc tính url . Nếu có, nó sẽ thêm thuộc tính booksDetails của phần tử trong mảng booksDetails và gán nó cho phần tử trong mảng books . Sau đó, nó sẽ overrides nội dung của file books.json với nội dung của mảng books lặp lại trong hàm này. Sau khi tạo hàm saveBookDetails() , mã sẽ gọi hàm main() để làm cho file này có thể sử dụng được. Nếu không, việc thực thi file này sẽ không tạo ra kết quả mong muốn.

Tệp main.js đã hoàn thành của bạn sẽ giống như sau:

main.js
let axios = require('axios') let ldb = require('./lowdbHelper.js').LowDbHelper let ldbHelper = new ldb() let allBooks = ldbHelper.getData()  let server = "http://your_load_balancer_external_ip_address" let podsWorkDone = [] let booksDetails = [] let errors = []  function main() {   let execute = process.argv[2] ? process.argv[2] : 0   execute = parseInt(execute)   switch (execute) {     case 0:       getBooks()       break;     case 1:       getBooksDetails()       break;   } }  function getBooks() {   console.log('getting books')   let data = {     url: 'http://books.toscrape.com/index.html',     nrOfPages: 20,     commands: [       {         description: 'get items metadata',         locatorCss: '.product_pod',         type: "getItems"       },       {         description: 'go to next page',         locatorCss: '.next > a:nth-child(1)',         type: "Click"       }     ],   }   let begin = Date.now();   axios.post(`${server}/api/books`, data).then(result => {     let end = Date.now();     let timeSpent = (end - begin) / 1000 + "secs";     console.log(`took ${timeSpent} to retrieve ${result.data.books.length} books`)     ldbHelper.saveData(result.data.books)   }) }  function getBooksDetails() {   let begin = Date.now()   for (let j = 0; j < allBooks.length; j++) {     let data = {       url: allBooks[j].url,       nrOfPages: 1,       commands: [         {           description: 'get item details',           locatorCss: 'article.product_page',           type: "getItemDetails"         }       ]     }     sendRequest(data, function (result) {       parseResult(result, begin)     })   } }  async function sendRequest(payload, cb) {   let book = payload   try {     await axios.post(`${server}/api/booksDetails`, book).then(response => {       if (Object.keys(response.data).includes('error')) {         let res = {           url: book.url,           error: response.data.error         }         cb(res)       } else {         cb(response.data)       }     })   } catch (error) {     console.log(error)     let res = {       url: book.url,       error: error     }     cb({ res })   } }  function parseResult(result, begin){   try {     let end = Date.now()     let timeSpent = (end - begin) / 1000 + "secs ";     if (!Object.keys(result).includes("error")) {       let wasSuccessful = Object.keys(result.booksDetails).length > 0 ? true : false       if (wasSuccessful) {         let podID = result.hostname         let podsIDs = podsWorkDone.length > 0 ? podsWorkDone.map(pod => { return Object.keys(pod)[0]}) : []         if (!podsIDs.includes(podID)) {           let podWork = {}           podWork[podID] = 1           podsWorkDone.push(podWork)         } else {           for (let pwd = 0; pwd < podsWorkDone.length; pwd++) {             if (Object.keys(podsWorkDone[pwd]).includes(podID)) {               podsWorkDone[pwd][podID] += 1               break             }           }         }         booksDetails.push(result)       } else {         errors.push(result)       }     } else {       errors.push(result)     }     console.log('podsWorkDone', podsWorkDone, ', retrieved ' + booksDetails.length + " books, ",       "took " + timeSpent + ", ", "used " + podsWorkDone.length + " pods,", " errors: " + errors.length)     saveBookDetails()   } catch (error) {     console.log(error)   } }  function saveBookDetails() {   let books = ldbHelper.getData()   for (let b = 0; b < books.length; b++) {     for (let d = 0; d < booksDetails.length; d++) {       let item = booksDetails[d]       if (books[b].url === item.url) {         books[b].booksDetails = item.booksDetails         break       }     }   }   ldbHelper.saveData(books) }  main() 

Đến đây bạn đã tạo ứng dụng client và sẵn sàng tương tác với trình quét trong cụm Kubernetes của bạn. Trong bước tiếp theo, bạn sẽ sử dụng ứng dụng client này và server ứng dụng để extract tất cả 400 cuốn sách.

Bước 8 - Chỉnh sửa trang web

Đến đây bạn đã tạo ứng dụng client và ứng dụng quét phía server , đã đến lúc quét trang web books.toscrape . Trước tiên, bạn sẽ truy xuất metadata cho tất cả 400 cuốn sách. Sau đó, bạn sẽ truy xuất các chi tiết còn thiếu cho từng cuốn sách trên trang của nó và theo dõi số lượng yêu cầu mà mỗi group đã xử lý trong thời gian thực.

Trong folder ./client , hãy chạy lệnh sau. Thao tác này sẽ truy xuất metadata cơ bản cho tất cả 400 cuốn sách và lưu nó vào file books.json của bạn:

  • npm start 0

Bạn sẽ nhận được kết quả sau:

Output
getting books took 40.323secs to retrieve 400 books

Việc truy xuất metadata cho các sách hiển thị trên tất cả 20 trang mất 40,323 giây, mặc dù giá trị này có thể khác nhau tùy thuộc vào tốc độ internet của bạn.

Đến đây bạn muốn truy xuất các chi tiết còn thiếu cho mọi cuốn sách được lưu trữ trong file books.json đồng thời theo dõi số lượng yêu cầu mà mỗi group xử lý.

Chạy lại npm start để truy xuất các chi tiết:

  • npm start 1

Bạn sẽ nhận được kết quả như thế này nhưng với các ID group khác nhau:

Output
. . . podsWorkDone [ { 'scraper-59cd578ff6-z8zdd': 69 }, { 'scraper-59cd578ff6-528gv': 96 }, { 'scraper-59cd578ff6-zjwfg': 94 }, { 'scraper-59cd578ff6-nk6fr': 80 }, { 'scraper-59cd578ff6-h2n8r': 61 } ] , retrieved 400 books, took 56.875secs , used 5 pods, errors: 0

Việc lấy các chi tiết còn thiếu cho tất cả 400 cuốn sách bằng Kubernetes chỉ mất chưa đầy 60 giây. Mỗi group chứa máy quét ít nhất 60 trang. Điều này thể hiện sự gia tăng hiệu suất lớn so với việc sử dụng một máy.

Như vậy, hãy nhân đôi số lượng group trong cụm Kubernetes của bạn để tăng tốc độ truy xuất nhiều hơn nữa:

  • kubectl scale deployment scraper --replicas=10

Sẽ mất một vài phút trước khi các group khả dụng, vì vậy hãy đợi ít nhất 10 giây trước khi chạy lệnh tiếp theo.

Chạy lại npm start để nhận được các chi tiết còn thiếu:

  • npm start 1

Bạn sẽ nhận được kết quả tương tự như sau nhưng với các ID group khác nhau:

Output
. . . podsWorkDone [ { 'scraper-59cd578ff6-z8zdd': 38 }, { 'scraper-59cd578ff6-6jlvz': 47 }, { 'scraper-59cd578ff6-g2mxk': 36 }, { 'scraper-59cd578ff6-528gv': 41 }, { 'scraper-59cd578ff6-bj687': 36 }, { 'scraper-59cd578ff6-zjwfg': 47 }, { 'scraper-59cd578ff6-nl6bk': 34 }, { 'scraper-59cd578ff6-nk6fr': 33 }, { 'scraper-59cd578ff6-h2n8r': 38 }, { 'scraper-59cd578ff6-5bw2n': 50 } ] , retrieved 400 books, took 34.925secs , used 10 pods, errors: 0

Sau khi tăng gấp đôi số trang, thời gian cần thiết để quét tất cả 400 trang giảm gần một nửa. Chỉ mất chưa đầy 35 giây để lấy lại tất cả các chi tiết còn thiếu.

Trong phần này, bạn đã gửi 400 yêu cầu đến server ứng dụng được triển khai trong cụm Kubernetes của bạn và loại bỏ 400 URL riêng lẻ trong một khoảng thời gian ngắn. Bạn cũng đã tăng số lượng group trong cụm của bạn để cải thiện hiệu suất hơn nữa.

Kết luận

Trong hướng dẫn này, bạn đã sử dụng Puppeteer, Docker và Kubernetes để tạo trình duyệt web đồng thời có khả năng quét nhanh 400 trang web. Để tương tác với trình quét, bạn đã xây dựng một ứng dụng Node.js sử dụng axios để gửi nhiều yêu cầu HTTP đến server chứa trình quét.

Puppeteer bao gồm nhiều tính năng bổ sung. Nếu bạn muốn tìm hiểu thêm, hãy xem tài liệu chính thức của Puppeteer . Để tìm hiểu thêm về Node.js, hãy xem loạt bài hướng dẫn của ta về cách viết mã trong Node.js.


Tags:

Các tin liên quan

Cách tạo ứng dụng web tiến bộ với Angular
2020-07-09
Cách cài đặt Django Web Framework trên Ubuntu 20.04
2020-07-06
Cách tạo chế độ xem để phát triển web Django
2020-05-14
Cách tạo chế độ xem để phát triển web Django
2020-05-14
Cách tạo ứng dụng web bằng Flask trong Python 3
2020-04-16
Cách tạo web server trong Node.js bằng module HTTP
2020-04-10
Mã thông báo web JSON (JWT) trong Express.js
2020-02-19
Phát triển bản địa với API thông báo web
2020-02-12
Cách tạo ứng dụng chuyển văn bản thành giọng nói với API giọng nói trên web
2019-12-12
Cách tạo băng chuyền image danh mục đầu tư với các thanh trượt được đồng bộ hóa trên trang web
2019-12-12