Thứ năm, 13/08/2020 | 00:00 GMT+7

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

Lướt web là quá trình tự động thu thập dữ liệu từ web. Quá trình này thường triển khai một “trình thu thập thông tin” tự động lướt web và loại bỏ dữ liệu từ các trang đã chọn. Có nhiều lý do tại sao bạn có thể cần loại bỏ dữ liệu. Về cơ bản, nó làm cho việc thu thập dữ liệu nhanh hơn nhiều bằng cách loại bỏ quá trình thu thập dữ liệu thủ công. Scraping cũng là một giải pháp khi muốn hoặc cần thu thập dữ liệu nhưng website không cung cấp API.

Trong hướng dẫn này, bạn sẽ xây dựng một ứng dụng quét web bằng Node.jsPuppeteer . Ứng dụng của bạn sẽ ngày càng phức tạp khi bạn tiến bộ. Trước tiên, bạn sẽ viết mã ứng dụng của bạn để mở Chromium và tải một trang web đặc biệt được thiết kế như một sandbox tìm kiếm web: books.toscrape.com . Trong hai bước tiếp theo, bạn sẽ quét tất cả sách trên một trang books.toscrape và sau đó là tất cả các sách trên nhiều trang. Trong các bước còn lại, bạn sẽ lọc tìm kiếm theo danh mục sách và sau đó lưu dữ liệu dưới dạng file JSON.

Cảnh báo: Đạo đức và tính hợp lệ của hoạt động tìm kiếm trên web rất phức tạp và không ngừng 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ế đặc biệt để 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

Bước 1 - Cài đặt Web Scraper

Với Node.js được cài đặt, bạn có thể bắt đầu cài đặt trình duyệt web của bạn . Đầu tiên, bạn sẽ tạo một folder root của dự án và sau đó cài đặt các phụ thuộc cần thiết. Hướng dẫn này chỉ yêu cầu một phần phụ thuộc và bạn sẽ cài đặt nó bằng trình quản lý gói mặc định của Node.js là npm . npm được cài đặt sẵn với Node.js, vì vậy bạn không cần phải cài đặt nó.

Tạo một folder cho dự án này và sau đó chuyển vào bên trong:

  • mkdir book-scraper
  • cd book-scraper

Bạn sẽ chạy tất cả các lệnh tiếp theo từ folder này.

Ta cần cài đặt một gói bằng npm hoặc trình quản lý gói nút. NPM khởi tạo đầu tiên để tạo ra một packages.json file , mà sẽ quản lý phụ thuộc và metadata của dự án của bạn.

Khởi tạo npm cho dự án của bạn:

  • npm init

npm sẽ hiển thị một chuỗi các dấu nhắc . Bạn có thể nhấn ENTER cho mọi dấu nhắc hoặc bạn có thể thêm các mô tả được cá nhân hóa. Đảm bảo nhấn ENTER và giữ nguyên các giá trị mặc định khi được yêu cầu entry point:test command: Ngoài ra, bạn có thể chuyển cờ y sang npm - npm init -y —và nó sẽ gửi tất cả các giá trị mặc định cho bạn.

Đầu ra của bạn sẽ giống như sau:

Output
{ "name": "sammy_scraper", "version": "1.0.0", "description": "a web scraper", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "sammy the shark", "license": "ISC" } Is this OK? (yes) yes

Nhập yes và nhấn ENTER . npm sẽ lưu kết quả này dưới dạng file package.json của bạn.

Bây giờ sử dụng npm để cài đặt Puppeteer:

  • npm install --save puppeteer

Lệnh này cài đặt cả Puppeteer và version Chromium mà group Puppeteer biết sẽ hoạt động với API của họ.

Trên các máy Linux, Puppeteer có thể yêu cầu một số phụ thuộc bổ sung.

Nếu bạn đang sử dụng Ubuntu 18.04, hãy kiểm tra trình đơn thả xuống 'Phụ thuộc Debian' bên trong phần 'Chrome không có đầu không chạy trên UNIX' của tài liệu khắc phục sự cố của Puppeteer . Bạn có thể sử dụng lệnh sau để giúp tìm bất kỳ phần phụ thuộc nào bị thiếu:

  • ldd chrome | grep not

Với npm, Puppeteer và bất kỳ phần phụ thuộc bổ sung nào được cài đặt, file package.json của bạn yêu cầu một cấu hình cuối cùng trước khi bạn bắt đầu viết mã. Trong hướng dẫn này, bạn sẽ chạy ứng dụng của bạn từ dòng lệnh với npm run start . Bạn phải thêm một số thông tin về tập lệnh start này vào package.json . Cụ thể, bạn phải thêm một dòng dưới chỉ thị scripts liên quan đến lệnh start của bạn.

Mở file trong editor bạn muốn :

  • nano package.json

Tìm phần scripts: và thêm các cấu hình sau. Hãy nhớ đặt dấu phẩy ở cuối dòng tập lệnh test , nếu không file của bạn sẽ không phân tích cú pháp chính xác.

Output
{ . . . "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "start": "node index.js" }, . . . "dependencies": { "puppeteer": "^5.2.1" } }

Bạn cũng sẽ nhận thấy rằng trình điều khiển puppeteer hiện xuất hiện dưới phần dependencies gần cuối file . Tệp package.json của bạn sẽ không yêu cầu bất kỳ bản sửa đổi nào nữa. Lưu các thay đổi và đóng editor .

Đến đây bạn đã sẵn sàng để bắt đầu mã hóa máy quét của bạn . Trong bước tiếp theo, bạn sẽ cài đặt một version trình duyệt và kiểm tra chức năng cơ bản của trình quét của bạn.

Bước 2 - Cài đặt version trình duyệt

Khi bạn mở trình duyệt truyền thống, bạn có thể thực hiện những việc như nhấp vào nút, chuyển bằng chuột, nhập, mở công cụ dành cho nhà phát triển, v.v. Một trình duyệt không có đầu như Chromium cho phép bạn thực hiện những điều tương tự nhưng theo chương trình và không có giao diện user . Trong bước này, bạn sẽ cài đặt version trình duyệt của trình quét của bạn. Khi bạn chạy ứng dụng của bạn , ứng dụng sẽ tự động mở Chromium và chuyển đến books.toscrape.com. Những hành động ban đầu này sẽ tạo cơ sở cho chương trình của bạn.

Trình duyệt web sẽ yêu cầu bốn file .js : browser.js , index,js , pageController.jspageScraper.js . Trong bước này, bạn sẽ tạo tất cả bốn file và sau đó liên tục cập nhật chúng khi chương trình của bạn ngày càng phát triển. Bắt đầu với browser.js ; file này sẽ chứa tập lệnh khởi động trình duyệt của bạn.

Từ folder root của dự án, hãy tạo và mở browser.js trong editor :

  • nano browser.js

Trước tiên, bạn sẽ require khiển rối và sau đó tạo ra một async chức năng gọi startBrowser() . Hàm này sẽ khởi động trình duyệt và trả về một version của nó. Thêm mã sau:

./book-scraper/browser.js
const puppeteer = require('puppeteer');  async function startBrowser(){     let browser;     try {         console.log("Opening the browser......");         browser = await puppeteer.launch({             headless: false,             args: ["--disable-setuid-sandbox"],             'ignoreHTTPSErrors': true         });     } catch (err) {         console.log("Could not create a browser instance => : ", err);     }     return browser; }  module.exports = {     startBrowser }; 

Puppeteer có phương thức .launch() để chạy một version của trình duyệt. Phương thức này trả về một Promise , vì vậy bạn phải đảm bảo Promise phân giải bằng cách sử dụng khối .then hoặc await .

Bạn đang sử dụng await đảm bảo Promise giải quyết xong, gói version này xung quanh một khối mã try-catch , sau đó trả về một version của trình duyệt.

Lưu ý phương thức .launch() nhận một tham số JSON với một số giá trị:

  • headless - false nghĩa là trình duyệt sẽ chạy với Giao diện để bạn có thể xem tập lệnh của bạn thực thi, trong khi true nghĩa là trình duyệt sẽ chạy ở chế độ không đầu. Lưu ý tốt, tuy nhiên, nếu bạn muốn triển khai scraper bạn lên cloud , cài đặt headless trở lại true . Hầu hết các máy ảo không có đầu và không bao gồm giao diện user , do đó chỉ có thể chạy trình duyệt ở chế độ không đầu. Puppeteer cũng bao gồm một chế độ headful , nhưng chế độ đó chỉ nên được sử dụng cho mục đích thử nghiệm.
  • ignoreHTTPSErrors - true cho phép bạn truy cập các trang web không được lưu trữ qua giao thức HTTPS an toàn và bỏ qua bất kỳ lỗi nào liên quan đến HTTPS.

Lưu và đóng file .

Bây giờ, hãy tạo file .js thứ hai của bạn, index.js :

  • nano index.js

Ở đây bạn sẽ require browser.jspageController.js . Sau đó, bạn sẽ gọi hàm startBrowser() và chuyển version trình duyệt đã tạo cho bộ điều khiển trang của ta , hàm này sẽ chỉ đạo các hành động của nó. Thêm mã sau:

./book-scraper/index.js
const browserObject = require('./browser'); const scraperController = require('./pageController');  //Start the browser and create a browser instance let browserInstance = browserObject.startBrowser();  // Pass the browser instance to the scraper controller scraperController(browserInstance) 

Lưu và đóng file .

Tạo file .js thứ ba của bạn, pageController.js :

  • nano pageController.js

pageController.js kiểm soát quá trình cạo của bạn. Nó sử dụng version trình duyệt để kiểm soát file pageScraper.js , đây là nơi thực thi tất cả các tập lệnh pageScraper.js . Cuối cùng, bạn sẽ sử dụng nó để chỉ định loại sách bạn muốn loại bỏ. Tuy nhiên, hiện tại, bạn chỉ muốn đảm bảo bạn có thể mở Chromium và chuyển đến một trang web:

./book-scraper/pageController.js
const pageScraper = require('./pageScraper'); async function scrapeAll(browserInstance){     let browser;     try{         browser = await browserInstance;         await pageScraper.scraper(browser);       }     catch(err){         console.log("Could not resolve the browser instance => ", err);     } }  module.exports = (browserInstance) => scrapeAll(browserInstance) 

Mã này xuất một hàm có trong version trình duyệt và chuyển nó đến một hàm có tên là scrapeAll() . Đến lượt nó, hàm này chuyển thể hiện này đến pageScraper.scraper() như một đối số sử dụng nó để quét các trang.

Lưu và đóng file .

Cuối cùng, tạo file .js cuối cùng của bạn, pageScraper.js :

  • nano pageScraper.js

Ở đây bạn sẽ tạo một đối tượng theo nghĩa đen với thuộc tính url và phương thức scraper() . url là URL của trang web bạn muốn cạo, trong khi phương thức scraper() chứa mã sẽ thực hiện việc quét thực tế của bạn, mặc dù ở giai đoạn này, nó chỉ chuyển đến một URL. Thêm mã sau:

./book-scraper/pageScraper.js
const scraperObject = {     url: 'http://books.toscrape.com',     async scraper(browser){         let page = await browser.newPage();         console.log(`Navigating to ${this.url}...`);         await page.goto(this.url);      } }  module.exports = scraperObject; 

Puppeteer có phương thức newPage() để tạo một version trang mới trong trình duyệt và các version trang này có thể thực hiện một số việc. Trong phương thức scraper() của ta , bạn đã tạo một version trang và sau đó sử dụng phương thức page.goto() để chuyển đến trang chủ books.toscrape.com .

Lưu và đóng file .

Cấu trúc file chương trình của bạn đã hoàn tất. Cấp đầu tiên của cây folder dự án của bạn sẽ trông như thế này:

Output
. ├── browser.js ├── index.js ├── node_modules ├── package-lock.json ├── package.json ├── pageController.js └── pageScraper.js

Bây giờ hãy chạy lệnh npm run start và xem ứng dụng cạp của bạn thực thi:

  • npm run start

Nó sẽ tự động mở một version trình duyệt Chromium, mở một trang mới trong trình duyệt và chuyển đến books.toscrape.com.

Trong bước này, bạn đã tạo một ứng dụng Puppeteer để mở Chromium và tải trang chủ cho một cửa hàng sách trực tuyến giả—books.toscrape.com. Trong bước tiếp theo, bạn sẽ thu thập dữ liệu cho mọi cuốn sách trên trang chủ đó.

Bước 3 - Thu thập dữ liệu từ một trang

Trước khi thêm nhiều chức năng hơn vào ứng dụng quét của bạn, hãy mở trình duyệt web bạn muốn và chuyển theo cách thủ công đến sách để quét trang chủ . Duyệt qua trang web và hiểu cách dữ liệu được cấu trúc.

Sách để loại bỏ hình ảnh trang web

Bạn sẽ tìm thấy một phần danh mục ở bên trái và sách được hiển thị ở bên phải. Khi bạn nhấp vào một cuốn sách, trình duyệt sẽ chuyển đến một URL mới hiển thị thông tin có liên quan về cuốn sách cụ thể đó.

Trong bước này, bạn sẽ sao chép hành vi này, nhưng với mã; bạn sẽ tự động hóa công việc chuyển trang web và sử dụng dữ liệu của nó.

Đầu tiên, nếu bạn kiểm tra mã nguồn của trang chủ bằng Công cụ Dev bên trong trình duyệt của bạn , bạn sẽ nhận thấy rằng trang liệt kê dữ liệu của từng cuốn sách dưới thẻ section . Bên trong thẻ section mọi cuốn sách đều nằm dưới thẻ list ( li ) và chính tại đây, bạn sẽ tìm thấy liên kết đến trang dành riêng của cuốn sách, giá cả và tình trạng còn hàng trong repository .

Mã nguồn books.toscrape được xem trong các công cụ dành cho nhà phát triển

Bạn sẽ rà soát các URL sách này, lọc các sách còn trong repository , chuyển đến từng trang sách riêng lẻ và quét dữ liệu của sách đó.

Mở lại file pageScraper.js của bạn:

  • nano pageScraper.js

Thêm nội dung được đánh dấu sau. Bạn sẽ lồng một khối await khác vào bên trong await page.goto(this.url); :

./book-scraper/pageScraper.js
 const scraperObject = {     url: 'http://books.toscrape.com',     async scraper(browser){         let page = await browser.newPage();         console.log(`Navigating to ${this.url}...`);         // Navigate to the selected page         await page.goto(this.url);         // Wait for the required DOM to be rendered         await page.waitForSelector('.page_inner');         // Get the link to all the required books         let urls = await page.$$eval('section ol > li', links => {             // Make sure the book to be scraped is in stock             links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")             // Extract the links from the data             links = links.map(el => el.querySelector('h3 > a').href)             return links;         });         console.log(urls);     } }  module.exports = scraperObject;  

Trong khối mã này, bạn đã gọi phương thức page.waitForSelector() . Thao tác này đợi div chứa tất cả thông tin liên quan đến sách được hiển thị trong DOM và sau đó bạn gọi phương thức page.$$eval() . Phương thức này nhận phần tử URL với phần selector section ol li (hãy đảm bảo bạn luôn chỉ trả về một chuỗi hoặc một số từ page.$eval()page.$$eval() ).

Mỗi cuốn sách đều có hai trạng thái; một cuốn sách Còn In Stock hoặc Out of stock . Bạn chỉ muốn loại bỏ những cuốn sách còn In Stock . Vì page.$$eval() trả về một mảng gồm tất cả các phần tử phù hợp, bạn đã lọc mảng này đảm bảo rằng bạn chỉ làm việc với sách còn hàng. Bạn đã làm điều này bằng cách tìm kiếm và đánh giá lớp .instock.availability . Sau đó, bạn lập bản đồ thuộc tính href của các liên kết sách và trả lại nó từ phương thức.

Lưu và đóng file .

Chạy lại ứng dụng của bạn:

  • npm run start

Trình duyệt sẽ mở ra, chuyển đến trang web, sau đó đóng khi tác vụ hoàn thành. Bây giờ hãy kiểm tra console của bạn; nó sẽ chứa tất cả các URL cóp nhặt:

Output
> book-scraper@1.0.0 start /Users/sammy/book-scraper > node index.js Opening the browser...... Navigating to http://books.toscrape.com... [ 'http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html', 'http://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html', 'http://books.toscrape.com/catalogue/soumission_998/index.html', 'http://books.toscrape.com/catalogue/sharp-objects_997/index.html', 'http://books.toscrape.com/catalogue/sapiens-a-brief-history-of-humankind_996/index.html', 'http://books.toscrape.com/catalogue/the-requiem-red_995/index.html', 'http://books.toscrape.com/catalogue/the-dirty-little-secrets-of-getting-your-dream-job_994/index.html', 'http://books.toscrape.com/catalogue/the-coming-woman-a-novel-based-on-the-life-of-the-infamous-feminist-victoria-woodhull_993/index.html', 'http://books.toscrape.com/catalogue/the-boys-in-the-boat-nine-americans-and-their-epic-quest-for-gold-at-the-1936-berlin-olympics_992/index.html', 'http://books.toscrape.com/catalogue/the-black-maria_991/index.html', 'http://books.toscrape.com/catalogue/starving-hearts-triangular-trade-trilogy-1_990/index.html', 'http://books.toscrape.com/catalogue/shakespeares-sonnets_989/index.html', 'http://books.toscrape.com/catalogue/set-me-free_988/index.html', 'http://books.toscrape.com/catalogue/scott-pilgrims-precious-little-life-scott-pilgrim-1_987/index.html', 'http://books.toscrape.com/catalogue/rip-it-up-and-start-again_986/index.html', 'http://books.toscrape.com/catalogue/our-band-could-be-your-life-scenes-from-the-american-indie-underground-1981-1991_985/index.html', 'http://books.toscrape.com/catalogue/olio_984/index.html', 'http://books.toscrape.com/catalogue/mesaerion-the-best-science-fiction-stories-1800-1849_983/index.html', 'http://books.toscrape.com/catalogue/libertarianism-for-beginners_982/index.html', 'http://books.toscrape.com/catalogue/its-only-the-himalayas_981/index.html' ]

Đây là một khởi đầu tuyệt vời, nhưng bạn muốn thu thập tất cả dữ liệu có liên quan cho một cuốn sách cụ thể chứ không chỉ URL của nó. Bây giờ, bạn sẽ sử dụng các URL này để mở từng trang và tìm tên sách, tác giả, giá cả, tình trạng còn hàng, UPC, mô tả và URL hình ảnh.

Mở lại pageScraper.js :

  • nano pageScraper.js

Thêm mã sau, mã này sẽ lặp lại qua từng liên kết cóp nhặt, mở một version trang mới, sau đó truy xuất dữ liệu có liên quan:

./book-scraper/pageScraper.js
const scraperObject = {     url: 'http://books.toscrape.com',     async scraper(browser){         let page = await browser.newPage();         console.log(`Navigating to ${this.url}...`);         // Navigate to the selected page         await page.goto(this.url);         // Wait for the required DOM to be rendered         await page.waitForSelector('.page_inner');         // Get the link to all the required books         let urls = await page.$$eval('section ol > li', links => {             // Make sure the book to be scraped is in stock             links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")             // Extract the links from the data             links = links.map(el => el.querySelector('h3 > a').href)             return links;         });           // Loop through each of those links, open a new page instance and get the relevant data from them         let pagePromise = (link) => new Promise(async(resolve, reject) => {             let dataObj = {};             let newPage = await browser.newPage();             await newPage.goto(link);             dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);             dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);             dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {                 // Strip new line and tab spaces                 text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, "");                 // Get the number of stock available                 let regexp = /^.*\((.*)\).*$/i;                 let stockAvailable = regexp.exec(text)[1].split(' ')[0];                 return stockAvailable;             });             dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);             dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);             dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);             resolve(dataObj);             await newPage.close();         });          for(link in urls){             let currentPageData = await pagePromise(urls[link]);             // scrapedData.push(currentPageData);             console.log(currentPageData);         }      } }  module.exports = scraperObject;  

Bạn có một loạt các URL. Bạn muốn lặp qua mảng này, mở URL trong một trang mới, quét dữ liệu trên trang đó, đóng trang đó và mở một trang mới cho URL tiếp theo trong mảng. Lưu ý bạn đã gói mã này trong một Lời hứa. Điều này là do bạn muốn có thể đợi mỗi hành động trong vòng lặp của bạn hoàn thành. Do đó, mỗi Lời hứa sẽ mở ra một URL mới và sẽ không giải quyết cho đến khi chương trình đã quét tất cả dữ liệu trên URL và sau đó version trang đó đã đóng lại.

Cảnh báo: lưu ý bạn đã đợi Lời hứa bằng vòng lặp for-in . Bất kỳ vòng lặp nào khác sẽ là đủ nhưng tránh lặp lại các mảng URL của bạn bằng cách sử dụng phương pháp lặp mảng như forEach hoặc bất kỳ phương thức nào khác sử dụng hàm gọi lại. Điều này là do hàm gọi lại sẽ phải đi qua hàng đợi gọi lại và vòng lặp sự kiện trước, do đó, nhiều cá thể trang sẽ mở tất cả cùng một lúc. Điều này sẽ khiến bộ nhớ của bạn căng thẳng hơn nhiều.

Hãy xem xét kỹ hơn chức năng pagePromise của bạn. Trước tiên, người quét của bạn đã tạo một trang mới cho mỗi URL, sau đó bạn sử dụng hàm page.$eval() để nhắm đến các bộ chọn cho các chi tiết có liên quan mà bạn muốn extract trên trang mới. Một số văn bản chứa khoảng trắng, tab, dòng mới và các ký tự không phải chữ và số khác mà bạn đã loại bỏ bằng biểu thức chính quy. Sau đó, bạn nối giá trị cho mọi phần dữ liệu được thu thập trong trang này vào một Đối tượng và giải quyết đối tượng đó.

Lưu và đóng file .

Chạy lại tập lệnh:

  • npm run start

Trình duyệt mở trang chủ, sau đó mở từng trang sách và ghi lại dữ liệu đã được cạo từ mỗi trang đó. Đầu ra này sẽ in ra console của bạn:

Output
Opening the browser...... Navigating to http://books.toscrape.com... { bookTitle: 'A Light in the Attic', bookPrice: '£51.77', noAvailable: '22', imageUrl: 'http://books.toscrape.com/media/cache/fe/72/fe72f0532301ec28892ae79a629a293c.jpg', bookDescription: "It's hard to imagine a world without A Light in the Attic. [...]', upc: 'a897fe39b1053632' } { bookTitle: 'Tipping the Velvet', bookPrice: '£53.74', noAvailable: '20', imageUrl: 'http://books.toscrape.com/media/cache/08/e9/08e94f3731d7d6b760dfbfbc02ca5c62.jpg', bookDescription: `"Erotic and absorbing...Written with starling power."--"The New York Times Book Review " Nan King, an oyster girl, is captivated by the music hall phenomenon Kitty Butler [...]`, upc: '90fa61229261140a' } { bookTitle: 'Soumission', bookPrice: '£50.10', noAvailable: '20', imageUrl: 'http://books.toscrape.com/media/cache/ee/cf/eecfe998905e455df12064dba399c075.jpg', bookDescription: 'Dans une France assez proche de la nôtre, [...]', upc: '6957f44c3847a760' } ...

Trong bước này, bạn đã thu thập dữ liệu có liên quan cho mọi cuốn sách trên trang chủ books.toscrape.com, nhưng bạn có thể thêm nhiều chức năng hơn. Ví dụ, mỗi trang sách đều được đánh số trang; làm thế nào để bạn lấy sách từ những trang khác? Ngoài ra, ở phía bên trái của trang web, bạn tìm thấy các danh mục sách; Điều gì sẽ xảy ra nếu bạn không muốn tất cả các cuốn sách, nhưng bạn chỉ muốn những cuốn sách từ một thể loại cụ thể? Đến đây bạn sẽ thêm các tính năng này.

Bước 4 - Thu thập dữ liệu từ nhiều trang

Các trang trên books.toscrape.com được đánh số trang có nút next bên dưới nội dung của chúng, trong khi các trang không được đánh số trang thì không.

Bạn sẽ sử dụng sự hiện diện của nút này để xác định xem trang có được phân trang hay không. Vì dữ liệu trên mỗi trang có cùng cấu trúc và có đánh dấu giống nhau, bạn sẽ không viết một bảng nháp cho mọi trang có thể. Đúng hơn, bạn sẽ sử dụng thực hành đệ quy .

Trước tiên, bạn cần thay đổi cấu trúc mã của bạn một chút để phù hợp với việc chuyển đệ quy đến một số trang.

Mở lại pagescraper.js :

  • nano pagescraper.js

Bạn sẽ thêm một hàm mới có tên là scrapeCurrentPage() vào phương thức scraper() . Hàm này sẽ chứa tất cả mã quét dữ liệu từ một trang cụ thể và sau đó nhấp vào nút tiếp theo nếu nó tồn tại. Thêm mã được đánh dấu sau:

./book-scraper/pageScraper.js scper ()
const scraperObject = {     url: 'http://books.toscrape.com',     async scraper(browser){         let page = await browser.newPage();         console.log(`Navigating to ${this.url}...`);         // Navigate to the selected page         await page.goto(this.url);         let scrapedData = [];         // Wait for the required DOM to be rendered         async function scrapeCurrentPage(){             await page.waitForSelector('.page_inner');             // Get the link to all the required books             let urls = await page.$$eval('section ol > li', links => {                 // Make sure the book to be scraped is in stock                 links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")                 // Extract the links from the data                 links = links.map(el => el.querySelector('h3 > a').href)                 return links;             });             // Loop through each of those links, open a new page instance and get the relevant data from them             let pagePromise = (link) => new Promise(async(resolve, reject) => {                 let dataObj = {};                 let newPage = await browser.newPage();                 await newPage.goto(link);                 dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);                 dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);                 dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {                     // Strip new line and tab spaces                     text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, "");                     // Get the number of stock available                     let regexp = /^.*\((.*)\).*$/i;                     let stockAvailable = regexp.exec(text)[1].split(' ')[0];                     return stockAvailable;                 });                 dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);                 dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);                 dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);                 resolve(dataObj);                 await newPage.close();             });              for(link in urls){                 let currentPageData = await pagePromise(urls[link]);                 scrapedData.push(currentPageData);                 // console.log(currentPageData);             }             // When all the data on this page is done, click the next button and start the scraping of the next page             // You are going to check if this button exist first, so you know if there really is a next page.             let nextButtonExist = false;             try{                 const nextButton = await page.$eval('.next > a', a => a.textContent);                 nextButtonExist = true;             }             catch(err){                 nextButtonExist = false;             }             if(nextButtonExist){                 await page.click('.next > a');                    return scrapeCurrentPage(); // Call this function recursively             }             await page.close();             return scrapedData;         }         let data = await scrapeCurrentPage();         console.log(data);         return data;     } }  module.exports = scraperObject;  

Ban đầu, bạn đặt biến nextButtonExist thành false, sau đó kiểm tra xem nút có tồn tại hay không. Nếu nút next tồn tại, bạn đặt nextButtonExists thành true và tiến hành nhấp vào nút next , sau đó gọi hàm này một cách đệ quy.

Nếu nextButtonExists là false, nó sẽ trả về mảng scrapedData như bình thường.

Lưu và đóng file .

Chạy lại tập lệnh của bạn:

  • npm run start

Quá trình này có thể mất một lúc để hoàn thành; xét cho cùng, ứng dụng của bạn hiện đang lấy dữ liệu từ hơn 800 cuốn sách. Vui lòng đóng trình duyệt hoặc nhấn CTRL + C để hủy quá trình.

Đến đây bạn đã tối đa hóa khả năng của máy quét của bạn , nhưng bạn đã tạo ra một vấn đề mới trong quá trình này. Bây giờ vấn đề không phải là quá ít dữ liệu mà là quá nhiều dữ liệu. Trong bước tiếp theo, bạn sẽ tinh chỉnh ứng dụng của bạn để lọc các mẩu tin lưu niệm theo danh mục sách.

Bước 5 - Scraping Data theo Category

Để loại dữ liệu theo danh mục, bạn cần sửa đổi cả file pageScraper.js và file pageController.js của bạn.

Mở pageController.js trong editor :

nano pageController.js 

Gọi máy cạo để nó chỉ quét sách du lịch. Thêm mã sau:

./book-scraper/pageController.js
const pageScraper = require('./pageScraper'); async function scrapeAll(browserInstance){     let browser;     try{         browser = await browserInstance;         let scrapedData = {};         // Call the scraper for different set of books to be scraped         scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');         await browser.close();         console.log(scrapedData)     }     catch(err){         console.log("Could not resolve the browser instance => ", err);     } } module.exports = (browserInstance) => scrapeAll(browserInstance) 

Bây giờ, bạn đang chuyển hai tham số vào phương thức pageScraper.scraper() , với tham số thứ hai là danh mục sách bạn muốn lấy ra, trong ví dụ này là Travel . Nhưng file pageScraper.js của bạn chưa nhận ra thông số này. Bạn cũng cần điều chỉnh file này.

Lưu và đóng file .

Mở pageScraper.js :

  • nano pageScraper.js

Thêm mã sau, mã này sẽ thêm thông số danh mục của bạn, chuyển đến trang danh mục đó và sau đó bắt đầu tìm kiếm thông qua các kết quả được phân trang:

./book-scraper/pageScraper.js
const scraperObject = {     url: 'http://books.toscrape.com',     async scraper(browser, category){         let page = await browser.newPage();         console.log(`Navigating to ${this.url}...`);         // Navigate to the selected page         await page.goto(this.url);         // Select the category of book to be displayed         let selectedCategory = await page.$$eval('.side_categories > ul > li > ul > li > a', (links, _category) => {              // Search for the element that has the matching text             links = links.map(a => a.textContent.replace(/(\r\n\t|\n|\r|\t|^\s|\s$|\B\s|\s\B)/gm, "") === _category ? a : null);             let link = links.filter(tx => tx !== null)[0];             return link.href;         }, category);         // Navigate to the selected category         await page.goto(selectedCategory);         let scrapedData = [];         // Wait for the required DOM to be rendered         async function scrapeCurrentPage(){             await page.waitForSelector('.page_inner');             // Get the link to all the required books             let urls = await page.$$eval('section ol > li', links => {                 // Make sure the book to be scraped is in stock                 links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")                 // Extract the links from the data                 links = links.map(el => el.querySelector('h3 > a').href)                 return links;             });             // Loop through each of those links, open a new page instance and get the relevant data from them             let pagePromise = (link) => new Promise(async(resolve, reject) => {                 let dataObj = {};                 let newPage = await browser.newPage();                 await newPage.goto(link);                 dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);                 dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);                 dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {                     // Strip new line and tab spaces                     text = text.textContent.replace(/(\r\n\t|\n|\r|\t)/gm, "");                     // Get the number of stock available                     let regexp = /^.*\((.*)\).*$/i;                     let stockAvailable = regexp.exec(text)[1].split(' ')[0];                     return stockAvailable;                 });                 dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);                 dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);                 dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);                 resolve(dataObj);                 await newPage.close();             });              for(link in urls){                 let currentPageData = await pagePromise(urls[link]);                 scrapedData.push(currentPageData);                 // console.log(currentPageData);             }             // When all the data on this page is done, click the next button and start the scraping of the next page             // You are going to check if this button exist first, so you know if there really is a next page.             let nextButtonExist = false;             try{                 const nextButton = await page.$eval('.next > a', a => a.textContent);                 nextButtonExist = true;             }             catch(err){                 nextButtonExist = false;             }             if(nextButtonExist){                 await page.click('.next > a');                    return scrapeCurrentPage(); // Call this function recursively             }             await page.close();             return scrapedData;         }         let data = await scrapeCurrentPage();         console.log(data);         return data;     } }  module.exports = scraperObject; 

Khối mã này sử dụng danh mục mà bạn đã chuyển vào để lấy URL chứa sách thuộc danh mục đó.

page.$$eval() có thể nhận đối số bằng cách chuyển đối số làm tham số thứ ba cho phương thức $$eval() và xác định nó là tham số thứ ba trong lệnh gọi lại như sau:

trang ví dụ. Hàm $$ eval ()
page.$$eval('selector', function(elem, args){     // ....... }, args) 

Đây là những gì bạn đã làm trong mã của bạn ; bạn đã chuyển danh mục sách mà bạn muốn rà soát, ánh xạ qua tất cả các danh mục để kiểm tra danh mục nào phù hợp và sau đó trả về URL của danh mục này.

Sau đó, URL này được sử dụng để chuyển đến trang hiển thị danh mục sách bạn muốn page.goto(selectedCategory) phương pháp page.goto(selectedCategory) .

Lưu và đóng file .

Chạy lại ứng dụng của bạn. Bạn sẽ nhận thấy rằng nó chuyển đến danh mục Travel , mở đệ quy các sách trong danh mục đó từng trang và ghi lại kết quả:

  • npm run start

Trong bước này, bạn cóp nhặt dữ liệu trên nhiều trang và sau đó thu thập dữ liệu trên nhiều trang từ một danh mục cụ thể. Trong bước cuối cùng, bạn sẽ sửa đổi tập lệnh của bạn để thu thập dữ liệu trên nhiều danh mục và sau đó lưu dữ liệu cóp nhặt này vào một file JSON được xâu chuỗi.

Bước 6 - Thu thập dữ liệu từ nhiều danh mục và lưu dữ liệu dưới dạng JSON

Trong bước cuối cùng này, bạn sẽ làm cho tập lệnh của bạn loại bỏ dữ liệu của bao nhiêu danh mục tùy thích và sau đó thay đổi cách xuất của bạn. Thay vì ghi lại kết quả, bạn sẽ lưu chúng trong một file có cấu trúc được gọi là data.json .

Bạn có thể nhanh chóng thêm nhiều danh mục khác để cạo; làm như vậy chỉ cần một dòng bổ sung cho mỗi thể loại.

Mở pageController.js :

  • nano pageController.js

Điều chỉnh mã của bạn để bao gồm các danh mục bổ sung. Ví dụ bên dưới thêm HistoricalFictionMystery vào danh mục Travel hiện có của ta :

./book-scraper/pageController.js
const pageScraper = require('./pageScraper'); async function scrapeAll(browserInstance){     let browser;     try{         browser = await browserInstance;         let scrapedData = {};         // Call the scraper for different set of books to be scraped         scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');         scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction');         scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery');         await browser.close();         console.log(scrapedData)     }     catch(err){         console.log("Could not resolve the browser instance => ", err);     } }  module.exports = (browserInstance) => scrapeAll(browserInstance) 

Lưu và đóng file .

Chạy lại tập lệnh và xem nó quét dữ liệu cho cả ba danh mục:

  • npm run start

Với máy quét có đầy đủ chức năng, bước cuối cùng của bạn là lưu dữ liệu của bạn ở định dạng hữu ích hơn. Đến đây bạn sẽ lưu trữ nó trong một file JSON bằng cách sử dụng mô-đun fs trong Node.js.

Đầu tiên, mở lại pageController.js :

  • nano pageController.js

Thêm mã được đánh dấu sau:

./book-scraper/pageController.js
const pageScraper = require('./pageScraper'); const fs = require('fs'); async function scrapeAll(browserInstance){     let browser;     try{         browser = await browserInstance;         let scrapedData = {};         // Call the scraper for different set of books to be scraped         scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');         scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction');         scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery');         await browser.close();         fs.writeFile("data.json", JSON.stringify(scrapedData), 'utf8', function(err) {             if(err) {                 return console.log(err);             }             console.log("The data has been scraped and saved successfully! View it at './data.json'");         });     }     catch(err){         console.log("Could not resolve the browser instance => ", err);     } }  module.exports = (browserInstance) => scrapeAll(browserInstance) 

Đầu tiên, bạn đang yêu cầu Node, module fs của js trong pageController.js . Điều này đảm bảo bạn có thể lưu dữ liệu của bạn dưới dạng file JSON. Sau đó, bạn đang thêm mã để khi quá trình cạo hoàn tất và trình duyệt đóng lại, chương trình sẽ tạo một file mới có tên là data.json . Lưu ý nội dung của data.jsonJSON được data.json . Do đó, khi đọc nội dung của data.json , hãy luôn phân tích cú pháp nó dưới dạng JSON trước khi sử dụng lại dữ liệu.

Lưu và đóng file .

Như vậy, bạn đã xây dựng một ứng dụng duyệt web để quét sách trên nhiều danh mục và sau đó lưu trữ dữ liệu cóp nhặt của bạn trong một file JSON. Khi ứng dụng của bạn ngày càng phức tạp, bạn có thể cần lưu trữ dữ liệu cóp nhặt này trong database hoặc phân phát nó qua API. Dữ liệu này được sử dụng như thế nào thực sự tùy thuộc vào bạn.

Kết luận

Trong hướng dẫn này, bạn đã xây dựng một trình thu thập dữ liệu web để quét dữ liệu trên nhiều trang một cách đệ quy và sau đó lưu nó vào một file JSON. Nói tóm lại, bạn đã học được một cách mới để tự động thu thập dữ liệu từ các trang web.

Puppeteer có khá nhiều tính năng không nằm trong phạm vi của hướng dẫn này. Để tìm hiểu thêm, hãy xem Sử dụng Puppeteer để dễ dàng điều khiển trên Chrome không đầu . Bạn cũng có thể truy cập tài liệu chính thức của Puppeteer .


Tags:

Các tin liên quan