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ó:
- Docker đã được cài đặt. Làm theo hướng dẫn của ta vềcách cài đặt và sử dụng Docker để được hướng dẫn. Trang web của Docker cung cấp hướng dẫn cài đặt cho các hệ điều hành khác như macOS và Windows.
- Một account tại Docker Hub để lưu trữ Docker image của bạn.
-  Một cụm Kubernetes 1.17+ với cấu hình kết nối của bạn được đặt làm mặc định kubectl. Để tạo một cụm Kubernetes trên DigitalOcean, hãy đọc Phần khởi động nhanh Kubernetes của ta . Để kết nối với cụm, hãy đọc Cách kết nối với một cụm DigitalOcean Kubernetes .
-  kubectlđã được cài đặt. Làm theo hướng dẫn này để bắt đầu với Kubernetes: A kubectl Cheat Sheet để cài đặt nó.
- Node.js được cài đặt trên máy phát triển của bạn. Hướng dẫn này đã được thử nghiệm trên Node.js version 12.18.3 và npm version 6.14.6. Làm theo hướng dẫn này để cài đặt Node.js trên macOS hoặc làm theo hướng dẫn này để cài đặt Node.js trên các bản phân phối Linux khác nhau .
- Nếu bạn đang sử dụng DigitalOcean Kubernetes, thì bạn cũng cần Mã truy cập cá nhân. Để tạo một mã, bạn có thể làm theo hướng dẫn của ta về cách tạo Mã truy cập cá nhân .Lưu mã thông báo này ở một nơi an toàn; nó cung cấp toàn quyền truy cập vào account của bạn.
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.
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.
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:
 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.
 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 , client và k8s :
- 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:
{   "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 main và scripts , đồ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:
{   "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 main và scripts .
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 axios và lowdb . 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.js và server.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:
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ửDOMcụ thể. Mỗicommandcó các thuộc tính sau:description,locatorCssvàtype.descriptioncho bạn biếtcommandlàm gì,locatorCsstìm phần tử thích hợp trongDOMvàtypechọ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ầncommandssẽ 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áccommandshiệ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 , existingCommands và nrOfPages . Sau đó, bạn tạo thêm hai biến: allBooks và booksDetails . 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() và 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 PuppeteerManager là runPuppeteer() . Đ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:
. . .     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 commands vì command 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:
. . .     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:
. . .     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:
. . .     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, timeout và commandIndex . 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:
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:
. . .     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 , getItems và getItemDetails .
 Xác định trường hợp click .
 Thay thế break; bên dưới case "click": với mã sau:
    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:
    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:
    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() và .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:
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:
. . .     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() và getBooksDetails() .
 Mã hóa các phương thức getAllBooks() và 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:
. . .     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:
. . .     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:
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:
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  express và body-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 và /api/booksDetails .
 Ở cuối file  server.js của bạn, hãy xác định / route bằng mã sau:
. . .  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:
. . .  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:
. . .  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() và sleep() .
 Bắt đầu với hàm getBooksHandler() .
 Ở cuối file  server.js của bạn, hãy thêm mã sau:
. . .  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() :
. . .  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() :
  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:
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 :
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 :
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.js và server.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:
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:
OutputNAME      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 :
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:
OutputNAME            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-IP và CLUSTER-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.js và books.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.js và books.json .
 Tạo và mở file  có tên lowdbHelper.js :
- nano lowdbHelper.js 
Thêm mã sau vào file  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 :
. . . 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() và 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:
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": [] } 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.js và books.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 :
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 :
. . . 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:
. . . 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:
. . .  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 :
. . .  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 :
. . .  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() :
. . .  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 books và booksDetails để 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:
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:
Outputgetting 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.
Các tin liên quan
Cách tạo ứng dụng web tiến bộ với Angular2020-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 thông báo trên web bằng kênh Laravel và Pusher
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
 

