如何使用Node.js和Puppeteer抓取网站
作为 Write for DOnations 计划的一部分,作者选择了 Free and Open Source Fund 来接受捐赠。
介绍
网络抓取是从网络自动收集数据的过程。 该过程通常会部署一个“爬虫”,该“爬虫”会自动在网上冲浪并从选定的页面中抓取数据。 您可能想要抓取数据的原因有很多。 首先,它通过消除手动数据收集过程使数据收集速度更快。 当需要或需要数据收集但网站不提供 API 时,抓取也是一种解决方案。
在本教程中,您将使用 Node.js 和 Puppeteer 构建 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,或者您可以 按照本指南在 Ubuntu 18.04 上使用 PPA 安装 Node.js。
第 1 步 — 设置 Web Scraper
安装 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
标志传递给 npm
—npm 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
并按 ENTER
。 npm 会将此输出保存为您的 package.json
文件。
现在使用 npm 安装 Puppeteer:
npm install --save puppeteer
此命令会安装 Puppeteer 和 Puppeteer 团队知道将与他们的 API 一起使用的 Chromium 版本。
在 Linux 机器上,Puppeteer 可能需要一些额外的依赖项。
如果您使用的是 Ubuntu 18.04, 检查 Puppeteer 故障排除文档 的“Chrome headless doesn't launch on UNIX”部分中的“Debian Dependencies”下拉菜单。 您可以使用以下命令来帮助查找任何缺少的依赖项:
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" } }
您还会注意到 puppeteer
现在出现在文件末尾附近的 dependencies
下。 您的 package.json
文件将不再需要任何修订。 保存更改并关闭编辑器。
您现在已准备好开始编写您的爬虫代码。 在下一步中,您将设置一个浏览器实例并测试您的爬虫的基本功能。
第 2 步 — 设置浏览器实例
当您打开传统浏览器时,您可以执行诸如单击按钮、使用鼠标导航、键入、打开开发工具等操作。 像 Chromium 这样的无头浏览器允许您做这些相同的事情,但是以编程方式并且没有用户界面。 在此步骤中,您将设置刮板的浏览器实例。 当您启动应用程序时,它会自动打开 Chromium 并导航到 books.toscrape.com。 这些初始操作将构成您的程序的基础。
您的网络爬虫将需要四个 .js
文件:browser.js
、index,js
、pageController.js
和 pageScraper.js
。 在此步骤中,您将创建所有四个文件,然后随着程序的复杂性不断更新它们。 以 browser.js
开头; 该文件将包含启动浏览器的脚本。
在项目的根目录中,创建并在文本编辑器中打开 browser.js
:
nano browser.js
首先,您将 require
Puppeteer 然后创建一个名为 startBrowser()
的 async
函数。 这个函数将启动浏览器并返回它的一个实例。 添加以下代码:
./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,因此您必须 确保使用 .then 或 await 块 解决 Promise。
您正在使用 await
来确保 Promise 解析,将此实例包装在 一个 try-catch 代码块 周围,然后返回浏览器的一个实例。
请注意,.launch()
方法采用带有多个值的 JSON 参数:
- headless -
false
表示浏览器将以接口运行,因此您可以观看脚本执行,而true
表示浏览器将以无头模式运行。 但是请注意,如果您想将爬虫部署到云端,请将headless
设置回true
。 大多数虚拟机是无头的,不包括用户界面,因此只能在无头模式下运行浏览器。 Puppeteer 还包括headful
模式,但仅用于测试目的。 - ignoreHTTPSErrors -
true
允许您访问未通过安全 HTTPS 协议托管的网站并忽略任何与 HTTPS 相关的错误。
保存并关闭文件。
现在创建你的第二个 .js
文件,index.js
:
nano index.js
在这里,您将 require
browser.js
和 pageController.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
是您要抓取的网页的网址,而 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 步 — 从单个页面抓取数据
在为您的爬虫应用程序添加更多功能之前,请打开您首选的网络浏览器并手动导航到 书籍以抓取主页 。 浏览该站点并了解数据的结构。
您会在左侧找到类别部分,在右侧找到书籍。 当您单击一本书时,浏览器会导航到一个新的 URL,该 URL 显示有关该特定书籍的相关信息。
在此步骤中,您将复制此行为,但使用代码; 您将自动化浏览网站和使用其数据的业务。
首先,如果您使用浏览器中的开发工具检查主页的源代码,您会注意到该页面在 section
标记下列出了每本书的数据。 在 section
标记内,每本书都在 list
(li
) 标记下,您可以在这里找到该书专用页面的链接、价格和现货供应。
您将抓取这些图书的 URL,过滤库存图书,导航到每个单独的图书页面,并抓取该图书的数据。
重新打开您的 pageScraper.js
文件:
nano pageScraper.js
添加以下突出显示的内容。 您将在 await page.goto(this.url);
内嵌套另一个 await
块:
./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() 方法 。 这等待包含所有书籍相关信息的 div 在 DOM 中呈现,然后调用 page.$$eval() 方法 。 此方法使用选择器 section ol li
获取 URL 元素(确保始终从 page.$eval()
和 page.$$eval()
方法返回字符串或数字)。
每本书都有两种状态; 一本书是 In Stock
或 Out of stock
。 你只想刮In Stock
的书。 因为 page.$$eval()
返回一个包含所有匹配元素的数组,所以您已经过滤了这个数组以确保您只使用库存书籍。 您通过搜索和评估类 .instock.availability
来做到这一点。 然后,您映射出图书链接的 href
属性并从方法中返回它。
保存并关闭文件。
重新运行您的应用程序:
npm run start
浏览器将打开,导航到网页,然后在任务完成后关闭。 现在检查你的控制台; 它将包含所有抓取的 URL:
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' ]
这是一个很好的开始,但是您想要抓取特定书籍的所有相关数据,而不仅仅是它的 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 > 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;
您有一个包含所有 URL 的数组。 您想循环访问此数组,在新页面中打开 URL,在该页面上抓取数据,关闭该页面,然后为数组中的下一个 URL 打开一个新页面。 请注意,您将此代码包装在 Promise 中。 这是因为您希望能够等待循环中的每个操作完成。 因此,每个 Promise 都会打开一个新的 URL,直到程序抓取了 URL 上的所有数据,然后该页面实例关闭后才会解析。
警告: 请注意,您使用 for-in
循环等待 Promise。 任何其他循环就足够了,但要避免使用像 forEach
这样的数组迭代方法或任何其他使用回调函数的方法来迭代你的 URL 数组。 这是因为回调函数必须首先通过回调队列和事件循环,因此,多个页面实例将同时打开。 这会给你的记忆带来更大的压力。
仔细看看你的 pagePromise
函数。 您的抓取工具首先为每个 URL 创建一个新页面,然后您使用 page.$eval()
函数将选择器定位为您希望在新页面上抓取的相关详细信息。 一些文本包含空格、制表符、换行符和其他非字母数字字符,您使用正则表达式将其删除。 然后,您将此页面中抓取的每条数据的值附加到一个对象并解析该对象。
保存并关闭文件。
再次运行脚本:
npm run start
浏览器打开主页,然后打开每个书籍页面并记录从每个页面中抓取的数据。 此输出将打印到您的控制台:
OutputOpening 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 步 — 从多个页面抓取数据
books.toscrape.com 上分页的页面在其内容下方有一个 next
按钮,而未分页的页面则没有。
您将使用此按钮的存在来确定页面是否分页。 由于每个页面上的数据具有相同的结构并具有相同的标记,因此您不会为每个可能的页面编写刮板。 相反,您将使用 递归 的做法。
首先,您需要稍微更改代码结构以适应递归导航到多个页面。
重新打开 pagescraper.js
:
nano pagescraper.js
您将在您的 scraper()
方法中添加一个名为 scrapeCurrentPage()
的新函数。 此函数将包含从特定页面抓取数据的所有代码,然后单击下一步按钮(如果存在)。 添加以下突出显示的代码:
./book-scraper/pageScraper.js scraper()
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;
您最初将 nextButtonExist
变量设置为 false,然后检查按钮是否存在。 如果 next
按钮存在,则将 nextButtonExists
设置为 true
并继续单击 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 > 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;
此代码块使用您传入的类别来获取该类别书籍所在的 URL。
page.$$eval()
可以通过将参数作为第三个参数传递给 $$eval()
方法来接收参数,并将其定义为回调中的第三个参数,如下所示:
示例页面。$$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)
首先,您需要 pageController.js
中的 Node,js 的 fs
模块。 这可确保您可以将数据保存为 JSON 文件。 然后添加代码,以便在抓取完成并关闭浏览器时,程序将创建一个名为 data.json
的新文件。 注意 data.json
的内容是 字符串化的 JSON。 因此,在读取data.json
的内容时,一定要在重用数据之前将其解析为JSON。
保存并关闭文件。
您现在已经构建了一个网络抓取应用程序,该应用程序可以跨多个类别抓取书籍,然后将抓取的数据存储在 JSON 文件中。 随着您的应用程序变得越来越复杂,您可能希望将这些抓取的数据存储在数据库中或通过 API 提供服务。 如何使用这些数据完全取决于您。
结论
在本教程中,您构建了一个网络爬虫,它以递归方式跨多个页面抓取数据,然后将其保存在 JSON 文件中。 简而言之,您学会了一种从网站自动收集数据的新方法。
Puppeteer 有很多不在本教程范围内的功能。 要了解更多信息,请查看 使用 Puppeteer 轻松控制 Headless Chrome 。 也可以访问Puppeteer的官方文档。