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ửDOM
cụ thể. Mỗicommand
có các thuộc tính sau:description
,locatorCss
vàtype
.description
cho bạn biếtcommand
làm gì,locatorCss
tìm phần tử thích hợp trongDOM
vàtype
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ầncommands
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áccommands
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
, 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 ứ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