Puppeteer教程

作为Write for DOnations计划的一部分,作者选择了免费和开源基金来接受捐赠

介绍

网络抓取是从网络自动收集数据的过程。该过程通常会部署一个“爬虫”,它会自动浏览网页并从选定页面中抓取数据。您可能想要抓取数据的原因有很多。首先,它通过消除手动数据收集过程使数据收集速度更快。当需要或需要数据收集但网站不提供 API 时,抓取也是一种解决方案。

在本教程中,您将使用Node.jsPuppeteer构建一个 Web 抓取应用程序随着您的进步,您的应用程序将变得越来越复杂。首先,您将编写应用程序以打开Chromium并加载一个设计为网络抓取沙箱的特殊网站:books.toscrape.com在接下来的两个步骤中,您将在 books.toscrape 的单个页面上抓取所有书籍,然后在多个页面上抓取所有书籍。在剩余的步骤中,您将按图书类别过滤抓取,然后将数据保存为 JSON 文件。

警告:网络抓取的道德和合法性非常复杂且不断发展。它们还因您的位置、数据位置和相关网站而异。本教程抓取了一个特殊的网站,books.toscrape.com,该网站专门设计用于测试刮刀应用程序。抓取任何其他域不在本教程的范围内。

先决条件

  • Node.js 安装在您的开发机器上。本教程在 Node.js 版本 12.18.3 和 npm 版本 6.14.6 上进行了测试。您可以按照本指南在MacOS或Ubuntu 18.04安装Node.js的,或者您也可以按照本指南以使用PPA在Ubuntu 18.04安装Node.js的

第 1 步 – 设置网络爬虫

安装 Node.js 后,您可以开始设置您的网络爬虫。首先,您将创建一个项目根目录,然后安装所需的依赖项。本教程只需要一个依赖项,您将使用 Node.js 的默认包管理器npm安装它npm 预装了 Node.js,所以你不需要安装它。

为这个项目创建一个文件夹,然后移动到里面:

  • mkdir book-scraper
  • cd book-scraper

您将从该目录运行所有后续命令。

我们需要使用 npm 或节点包管理器安装一个包。首先初始化 npm 以创建一个packages.json文件,该文件将管理您项目的依赖项和元数据。

为您的项目初始化 npm:

  • npm init

npm 将显示一系列提示。您可以按ENTER每个提示,也可以添加个性化描述。确保ENTER在提示输入entry point:按下并保留默认值test command:或者,您可以将y标志传递npmnpm init -y— 它将为您提交所有默认值。

您的输出将如下所示:

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

键入yes并按ENTERnpm 会将此输出保存为您的package.json文件。

现在使用 npm 安装 Puppeteer:

  • npm install --save puppeteer

此命令会安装 Puppeteer 和 Puppeteer 团队知道将与他们的 API 一起使用的 Chromium 版本。

在 Linux 机器上,Puppeteer 可能需要一些额外的依赖项。

如果您使用的是 Ubuntu 18.04,请检查 Puppeteer 的故障排除文档的“Chrome headless 无法在 UNIX 上启动”部分中的“Debian 依赖项”下拉列表您可以使用以下命令来帮助查找任何缺失的依赖项:

  • ldd chrome | grep not

安装 npm、Puppeteer 和任何其他依赖项后,您的package.json文件需要在开始编码之前进行最后一次配置。在本教程中,您将从命令行使用npm run start. 您必须将有关此start脚本的一些信息添加package.json. 具体来说,您必须在scripts有关您的start命令指令下添加一行

在您首选的文本编辑器中打开文件:

  • nano package.json

找到该scripts:部分并添加以下配置。请记住在test脚本行的末尾放置一个逗号,否则您的文件将无法正确解析。

Output

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

您还会注意到puppeteernow 出现在dependencies文件末尾附近。您的package.json文件将不再需要任何修订。保存更改并关闭编辑器。

您现在已准备好开始编写刮板。在下一步中,您将设置浏览器实例并测试刮刀的基本功能。

步骤 2 — 设置浏览器实例

当您打开传统浏览器时,您可以执行诸如单击按钮、使用鼠标导航、键入、打开开发工具等操作。像 Chromium 这样的无头浏览器可以让你做同样的事情,但是以编程方式并且没有用户界面。在这一步中,您将设置刮刀的浏览器实例。当您启动应用程序时,它会自动打开 Chromium 并导航到 books.toscrape.com。这些初始操作将构成您的程序的基础。

您的网站刮板将需要四个.js文件:browser.jsindex,jspageController.js,和pageScraper.js在这一步中,您将创建所有四个文件,然后随着程序的复杂性不断更新它们。browser.js;开始 此文件将包含启动浏览器的脚本。

从项目的根目录中,创建并browser.js在文本编辑器中打开

  • nano browser.js

首先,您将requirePuppeteer,然后创建一个async名为startBrowser(). 此函数将启动浏览器并返回它的一个实例。添加以下代码:

./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 有一个.launch()方法可以启动浏览器的实例。此方法返回一个Promise,因此您必须确保 Promise 使用 a.thenawaitblock解析

您正在使用await,以确保承诺做出决议,周围环绕这种情况下一个try-catch代码块,然后返回浏览器的一个实例。

请注意,该.launch()方法采用具有多个值的 JSON 参数:

  • headlessfalse意味着浏览器将使用接口运行,以便您可以观看脚本执行,同时true意味着浏览器将以无头模式运行。但是请注意,如果您想将抓取工具部署到云中,请headless重新设置true. 大多数虚拟机都是无头的,不包含用户界面,因此只能在无头模式下运行浏览器。Puppeteer 还包括一个headful模式,但它应该仅用于测试目的。
  • ignoreHTTPSErrorstrue允许您访问未通过安全 HTTPS 协议托管的网站,并忽略任何与 HTTPS 相关的错误。

保存并关闭文件。

现在创建您的第二个.js文件,index.js

  • nano index.js

在这里你会require browser.jspageController.js然后您将调用该startBrowser()函数并将创建的浏览器实例传递给我们的页面控制器,这将指导其操作。添加以下代码:

./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)

保存并关闭文件。

创建您的第三个.js文件pageController.js

  • nano pageController.js

pageController.js控制您的抓取过程。它使用浏览器实例来控制pageScraper.js文件,这是所有抓取脚本执行的地方。最终,您将使用它来指定要抓取的图书类别。但是,现在,您只想确保可以打开 Chromium 并导航到网页:

./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)

此代码导出一个函数,该函数接受浏览器实例并将其传递给名为 的函数scrapeAll()反过来,此函数将此实例pageScraper.scraper()作为参数传递给,该参数使用它来抓取页面。

保存并关闭文件。

最后,创建您的最后一个.js文件pageScraper.js

  • nano pageScraper.js

在这里,您将创建一个带有url属性和scraper()方法的对象字面量url是你要刮网页的Web URL,而scraper()方法包含将执行实际拼抢的代码,但在这个阶段,只是导航到一个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}...`);
        await page.goto(this.url);

    }
}

module.exports = scraperObject;

Puppeteer 有一个newPage()方法可以在浏览器中创建一个新的页面实例,这些页面实例可以做很多事情。在我们的scraper()方法中,您创建了一个页面实例,然后使用该page.goto()方法导航到books.toscrape.com 主页

保存并关闭文件。

您的程序的文件结构现已完成。项目目录树的第一级将如下所示:

Output

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

现在运行命令npm run start并观察您的刮刀应用程序执行:

  • npm run start

它将自动打开一个 Chromium 浏览器实例,在浏览器中打开一个新页面,然后导航到 books.toscrape.com。

在这一步中,您创建了一个 Puppeteer 应用程序,该应用程序打开 Chromium 并加载了一个虚拟在线书店的主页——books.toscrape.com。在下一步中,您将抓取该主页上每本书的数据。

第 3 步 — 从单个页面中抓取数据

在向您的抓取应用程序添加更多功能之前,打开您首选的 Web 浏览器并手动导航到书籍以抓取主页浏览网站并了解数据的结构。

Puppeteer教程

您会在左侧找到类别部分,右侧显示书籍。当您单击一本书时,浏览器会导航到一个新 URL,该 URL 显示有关该特定书籍的相关信息。

在这一步中,您将复制此行为,但使用代码;您将自动化浏览网站和使用其数据的业务。

首先,如果您使用浏览器中的开发工具检查主页的源代码,您会注意到该页面在section标签下列出了每本书的数据section标签内,每本书都有一个list( li) 标签,您可以在此处找到该书专用页面的链接、价格和库存状况。

Puppeteer教程

您将抓取这些图书 URL,过滤有库存的图书,导航到每个单独的图书页面,并抓取该图书的数据。

重新打开您的pageScraper.js文件:

  • nano pageScraper.js

添加以下突出显示的内容。你将await在里面嵌套另一个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;

在此代码块中,您调用page.waitForSelector()方法此等待它包含了所有在DOM中呈现的书相关信息的DIV,然后你打电话page.$$eval()方法此方法通过选择器获取 URL 元素section ol li(确保始终只从page.$eval()page.$$eval()方法返回字符串或数字)。

每本书都有两种状态;一本书是In StockOut of stock您只想抓取In Stock. 因为page.$$eval()返回一个包含所有匹配元素的数组,所以您已经过滤了这个数组以确保您只处理有库存的书籍。您通过搜索和评估 class 来做到这一点.instock.availability然后您绘制了href书籍链接属性并从方法中返回它。

保存并关闭文件。

重新运行您的应用程序:

  • npm run start

浏览器将打开,导航到网页,然后在任务完成后关闭。现在检查你的控制台;它将包含所有抓取的 URL:

Output

> 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' ]

这是一个很好的开始,但您想要抓取特定书籍的所有相关数据,而不仅仅是其 URL。您现在将使用这些 URL 打开每一页并抓取图书的标题、作者、价格、可用性、UPC、描述和图像 URL。

重新开放pageScraper.js

  • nano pageScraper.js

添加以下代码,它将循环遍历每个抓取的链接,打开一个新的页面实例,然后检索相关数据:

./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 > h2', 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; 

您有一个包含所有 URL 的数组。您希望遍历此数组,在新页面中打开 URL,在该页面上抓取数据,关闭该页面,然后为数组中的下一个 URL 打开一个新页面。请注意,您将此代码包装在 Promise 中。这是因为您希望能够等待循环中的每个操作完成。因此,每个 Promise 都会打开一个新的 URL,并且在程序抓取 URL 上的所有数据之前不会解析,然后该页面实例关闭。

警告:请注意,您使用for-in循环等待 Promise 任何其他循环都足够了,但要避免使用像 那样的数组迭代方法forEach或任何其他使用回调函数的方法来迭代 URL 数组这是因为回调函数必须首先通过回调队列和事件循环,因此,多个页面实例将同时打开。这会给你的记忆带来更大的压力。

仔细看看你的pagePromise函数。您的抓取工具首先为每个 URL 创建一个新页面,然后您使用该page.$eval()函数将选择器定位为您想要在新页面上抓取的相关详细信息。某些文本包含空格、制表符、换行符和其他非字母数字字符,您可以使用正则表达式将其去除。然后,您将在此页面中抓取的每条数据的值附加到一个对象并解析该对象。

保存并关闭文件。

再次运行脚本:

  • npm run start

浏览器打开主页,然后打开每个书页并记录从每个页面中抓取的数据。此输出将打印到您的控制台:

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' } ...

在这一步中,您在 books.toscrape.com 的主页上抓取了每本书的相关数据,但您可以添加更多功能。例如,每一页书都有分页;你如何从这些其他页面获取书籍?此外,在网站的左侧,您可以找到书籍类别;如果您不想要所有的书,而只想要特定类型的书怎么办?您现在将添加这些功能。

第 4 步 – 从多个页面抓取数据

book.toscrape.com 上分页的next页面在其内容下方有一个按钮,而未分页的页面则没有。

您将使用此按钮的存在来确定页面是否已分页。由于每个页面上的数据具有相同的结构并具有相同的标记,因此您不会为每个可能的页面编写一个抓取工具。相反,您将使用递归的做法

首先,您需要稍微更改代码结构以适应递归导航到多个页面。

重新开放pagescraper.js

  • nano pagescraper.js

您将添加一个新函数调用scrapeCurrentPage()您的scraper()方法。此函数将包含从特定页面抓取数据的所有代码,然后单击下一步按钮(如果存在)。添加以下突出显示的代码:

./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);
        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 > h2', 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;

nextButtonExist最初变量设置为 false,然后检查按钮是否存在。如果next按钮存在,则设置nextButtonExiststrue并继续单击该next按钮,然后递归调用此函数。

如果nextButtonExists为假,它会scrapedData像往常一样返回数组。

保存并关闭文件。

再次运行你的脚本:

  • npm run start

这可能需要一段时间才能完成;毕竟,您的应用程序现在正在从 800 多本书中抓取数据。随意关闭浏览器或按CTRL + C取消该过程。

您现在已经最大限度地发挥了刮板的功能,但在此过程中又产生了一个新问题。现在的问题不是数据太少,而是数据太多。在下一步中,您将微调您的应用程序以按书籍类别过滤您的抓取。

第 5 步 — 按类别抓取数据

要按类别抓取数据,您需要修改pageScraper.js文件和pageController.js文件。

pageController.js在文本编辑器中打开

nano pageController.js

调用刮刀,使其只刮旅行书。添加以下代码:

./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)

您现在将两个参数传递到您的pageScraper.scraper()方法中,第二个参数是您要抓取的书籍类别,在本例中为Travel. 但是你的pageScraper.js文件还不能识别这个参数。您也需要调整此文件。

保存并关闭文件。

打开pageScraper.js:

  • nano pageScraper.js

添加以下代码,这将添加您的类别参数,导航到该类别页面,然后开始抓取分页结果:

./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 > h2', 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;

此代码块使用您传入的类别来获取该类别图书所在的 URL。

所述page.$$eval()可以通过将参数作为第三个参数采取在参数$$eval()的方法,和将其定义为在回调作为这样的第三个参数:

示例 page.$$eval() 函数

page.$$eval('selector', function(elem, args){
    // .......
}, args)

这就是您在代码中所做的;您传递了要抓取的书籍类别,映射所有类别以检查哪一个匹配,然后返回该类别的 URL。

然后使用此 URL 导航到显示您要使用该page.goto(selectedCategory)方法抓取的书籍类别的页面

保存并关闭文件。

再次运行您的应用程序。您会注意到它导航到该Travel类别,递归地逐页打开该类别中的书籍,并记录结果:

  • npm run start

在此步骤中,您跨多个页面抓取数据,然后跨多个页面抓取来自一个特定类别的数据。在最后一步中,您将修改脚本以跨多个类别抓取数据,然后将此抓取的数据保存到字符串化的 JSON 文件中。

第 6 步 – 从多个类别中抓取数据并将数据保存为 JSON

在这最后一步中,您将使脚本根据需要从任意数量的类别中刮取数据,然后更改输出方式。不是记录结果,而是将它们保存在一个名为data.json.

您可以快速添加更多类别进行抓取;这样做每个流派只需要一个额外的行。

打开pageController.js:

  • nano pageController.js

调整您的代码以包含其他类别。下面的示例将HistoricalFiction添加Mystery到我们现有的Travel类别中:

./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)

保存并关闭文件。

再次运行脚本并观察它抓取所有三个类别的数据:

  • npm run start

随着刮刀功能齐全,您的最后一步涉及以更有用的格式保存数据。您现在将使用Node.js 中fs模块将其存储在 JSON 文件

首先,重新打开pageController.js

  • nano pageController.js

添加以下突出显示的代码:

./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)

First, you are requiring Node,js’s fs module in pageController.js. This ensures that you can save your data as a JSON file. Then you are adding code so that when the scraping completes and the browser closes, the program will create a new file called data.json. Note that the contents of data.json are stringified JSON. Therefore, when reading the content of data.json, always parse it as JSON before reusing the data.

Save and close the file.

You have now built a web-scraping application that scrapes books across multiple categories and then stores your scraped data in a JSON file. As your application grows in complexity, you might want to store this scraped data in a database or serve it over an API. How this data is consumed is really up to you.

Conclusion

在本教程中,您构建了一个网络爬虫,它以递归方式跨多个页面抓取数据,然后将其保存在 JSON 文件中。简而言之,您学到了一种从网站自动收集数据的新方法。

Puppeteer 有很多不在本教程范围内的功能。要了解更多信息,请查看使用 Puppeteer 轻松控制 Headless Chrome您还可以访问Puppeteer 的官方文档