与使用商业工具相比,许多开发者更倾向于自己编写网页爬虫。虽然市面上的产品功能更为完善,但我们无法否认这些爬虫带来的成效,以及亲手制作带来的乐趣。
在接下来的文章中,您将了解如何使用 Node.js 和 Puppeteer 构建自己的网页爬虫。我们将编写一个应用程序,它能加载网站、截取屏幕截图、使用无头浏览器登录网站,并从多个页面中抓取数据。随着学习的深入,您的应用程序将变得越来越复杂。

与使用商业工具相比,许多开发者更倾向于自己编写网页爬虫。虽然市面上的产品功能更为完善,但我们无法否认这些爬虫带来的成效,以及亲手制作带来的乐趣。
在接下来的文章中,您将了解如何使用 Node.js 和 Puppeteer 构建自己的网页爬虫。我们将编写一个应用程序,它能加载网站、截取屏幕截图、使用无头浏览器登录网站,并从多个页面中抓取数据。随着学习的深入,您的应用程序将变得越来越复杂。
Google 设计 Puppeteer 的初衷,是希望在 Node.js 中提供一个简单而强大的接口,利用 Chromium 浏览器引擎实现自动化测试和各类任务。它默认以无头模式运行,但也可以配置为运行完整的 Chrome 或 Chromium 浏览器。
Puppeteer 团队构建的 API 利用 DevTools 协议来控制 Chrome 等网页浏览器,并执行各种任务,例如:
您在浏览器中手动能完成的大多数操作,都可以通过 Puppeteer 实现。此外,这些操作还可以被自动化,从而为您节省更多时间,让您能专注于其他事务。
Puppeteer的设计初衷也是为了方便开发者使用。熟悉其他流行测试框架(如Mocha)的人会很快适应Puppeteer,并发现一个活跃的社区为Puppeteer提供支持。这使得它在开发者中的受欢迎程度大幅提升。
当然,Puppeteer 不仅适用于测试。毕竟,既然它能完成标准浏览器能做的一切,那么对于网页爬虫来说,它就极具价值。具体而言,它能协助执行 JavaScript 代码,使爬虫能够获取页面的 HTML 内容,并通过滚动页面或点击随机区域来模拟正常用户的行为。
这些至关重要的功能使得无头浏览器成为任何商业数据提取工具的核心组件,同时也适用于除最简单的自制网页爬虫以外的所有爬虫工具。
首先,请确保您的机器上已安装最新版本的 Node.js 和 Puppeteer。若尚未安装,可按照以下步骤安装所有必要组件。
您可以从这里下载并安装 Node.js。Node.js 默认自带其包管理器 npm。
要安装 Puppeteer 库,您可以在项目根目录下运行以下命令:
npm install puppeteer# or "yarn add puppeteer"
请注意,安装 Puppeteer 时,系统会同时下载最新版本的 Chromium,以确保其与 API 兼容。
该库支持多种不同的操作。由于我们的重点在于网页抓取,我们将重点介绍那些最可能引起您兴趣的用例,特别是当您需要提取网页数据时。
让我们从一个基础示例开始。我们将编写一个脚本,用于截取指定网站的屏幕截图。
请注意,Puppeteer 是一个基于 Promise 的库(它在后台会对无头 Chrome 实例执行异步调用)。因此,为了保持代码简洁,我们将使用 async/await。
首先,在项目根目录下创建一个名为 index.js 的新文件。
在该文件中,我们需要定义一个异步函数,并将所有 Puppeteer 代码封装其中。
const puppeteer = require('puppeteer')
async function snapScreenshot() {
try {
const URL = 'https://old.reddit.com/'
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto(URL)
await page.screenshot({ path: 'screenshot.png' })
await browser.close()
} catch (error) {
console.error(error)
}
}
snapScreenshot()首先,使用 puppeteer.launch() 命令启动浏览器实例。接着,利用该浏览器实例创建一个新页面。要导航至目标网站,我们可以使用 goto() 方法,并将 URL 作为参数传入。要截取屏幕截图,我们将使用 screenshot() 方法。同时,还需要指定图片的保存路径。
请注意,Puppeteer 默认将页面大小设置为 800×600 像素,这也决定了截图的尺寸。你可以使用 setViewport() 方法自定义页面大小。
别忘了关闭浏览器实例。然后,您只需在终端中运行 `node index.js` 即可。
真的就是这么简单!此时您应该会在项目文件夹中看到一个名为 screenshot.png 的新文件。
如果由于某种原因,您想要抓取的网站在未登录时不显示内容,您可以使用 Puppeteer 自动化登录过程。
首先,我们需要检查要抓取的网站并找到登录字段。可以通过右键点击该元素并选择“检查”选项来实现。
在我的示例中,这些输入框位于类名为 login-form 的表单内。我们可以使用 type() 方法输入登录凭据。
此外,若想确保操作正确执行,可在启动 Puppeteer 实例时添加 headless 参数并将其设为 false。随后你将看到 Puppeteer 如何为你完成整个过程。
const puppeteer = require('puppeteer')
async function login() {
try {
const URL = 'https://old.reddit.com/'
const browser = await puppeteer.launch({headless: false})
const page = await browser.newPage()
await page.goto(URL)
await page.type('.login-form input[name="user"]', 'EMAIL@gmail.com')
await page.type('.login-form input[name="passwd"]', 'PASSWORD')
await Promise.all([
page.click('.login-form .submit button'),
page.waitForNavigation(),
]);
await browser.close()
} catch (error) {
console.error(error)
}
}
login()要模拟鼠标点击,我们可以使用 click() 方法。点击登录按钮后,我们需要等待页面加载完成。这可以通过 waitForNavigation() 方法实现。
如果输入的凭据正确,现在应该已经登录成功了!
本文将以 /r/learnprogramming 子版块为例。我们需要导航至该网站,并获取每条帖子的标题和 URL。为此,我们将使用 evaluate() 方法。
代码应如下所示:
const puppeteer = require('puppeteer')
async function tutorial() {
try {
const URL = 'https://old.reddit.com/r/learnprogramming/'
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto(URL)
let data = await page.evaluate(() => {
let results = []
let items = document.querySelectorAll('.thing')
items.forEach((item) => {
results.push({
url: item.getAttribute('data-url'),
title: item.querySelector('.title').innerText,
})
})
return results
})
console.log(data)
await browser.close()
} catch (error) {
console.error(error)
}
}
tutorial()利用前面介绍的 Inspect 方法,我们可以针对 .thing 选择器抓取所有帖子。我们遍历这些帖子,对每一条获取其 URL 和标题,并将它们推入数组中。
整个过程完成后,你可以在控制台中查看结果。
太好了,我们已经抓取了第一页。但如何抓取这个子版块的多个页面呢?
这比你想象的要简单。代码如下:
const puppeteer = require('puppeteer')
async function tutorial() {
try {
const URL = 'https://old.reddit.com/r/learnprogramming/'
const browser = await puppeteer.launch({headless: false})
const page = await browser.newPage()
await page.goto(URL)
let pagesToScrape = 5;
let currentPage = 1;
let data = []
while (currentPage <= pagesToScrape) {
let newResults = await page.evaluate(() => {
let results = []
let items = document.querySelectorAll('.thing')
items.forEach((item) => {
results.push({
url: item.getAttribute('data-url'),
text: item.querySelector('.title').innerText,
})
})
return results
})
data = data.concat(newResults)
if (currentPage < pagesToScrape) {
await page.click('.next-button a')
await page.waitForSelector('.thing')
await page.waitForSelector('.next-button a')
}
currentPage++;
}
console.log(data)
await browser.close()
} catch (error) {
console.error(error)
}
}
tutorial()我们需要一个变量来指定要抓取的总页数,以及另一个变量来记录当前页码。当当前页码小于或等于目标页数时,我们获取该页面上每条帖子的 URL 和标题。每页抓取完成后,我们将新结果与已抓取的数据合并。
随后点击“下一页”按钮,重复抓取过程,直到达到预设的页面数量。每次处理完一页后,还需将当前页码递增。
恭喜!您已成功使用 Puppeteer 构建了自己的网页抓取工具。希望您喜欢这篇教程!
尽管如此,本指南中创建的脚本还无法胜任繁重的工作。它缺少一些关键功能,而这些功能正是让网页抓取变得流畅无瑕的关键。使用移动或住宅代理以及破解验证码,只是其中缺失的几项功能。
如果您正在寻找更专业的数据提取方案,不妨了解一下 WebScrapingAPI 能实现哪些功能,看看它是否适合您的需求。该服务提供免费套餐,您只需投入 30 分钟时间即可体验。
祝您网页抓取顺利!

加布里埃尔·乔奇(Gabriel Cioci)是 WebScrapingAPI 的全栈开发工程师,负责构建和维护该平台的网站、用户面板以及面向用户的核心功能模块。