Puppeteer是对无头Chrome浏览器的一个高级抽象,具有广泛的API。这使得它能够非常方便地实现与网页的自动交互。
本文将向您介绍一个用例,我们将在GitHub上搜索一个关键词,并获取第一个结果的标题。
这是一个基本的例子,纯粹是为了演示,甚至不用Puppeteer也能完成。因为关键词可以在GitHub的页面上的URL和页面列表中出现,所以可以直接导航到结果。
但是,假设你在网页上的交互并没有反映在页面的URL中,也没有公共的API来获取数据,那么通过Puppeteer实现的自动化就会很方便。
设置Puppeteer和Node.js
让我们在一个文件夹内初始化一个Node.js项目。在你的系统终端,导航到你想要的项目文件夹,并运行以下命令。
npm init -y
复制代码
这将生成一个package.json
文件。接下来,安装Puppeteer的npm包。
npm install --save puppeteer
复制代码
现在,创建一个名为service.mjs
的文件。这个文件格式允许我们使用ES模块,并将负责通过使用Puppeteer来搜刮页面。让我们用Puppeteer完成一个快速测试,看看它是否有效。
首先,我们启动一个Chrome实例,并通过headless: false
参数来显示它,而不是在没有GUI的情况下无头运行它。现在用newPage
方法创建一个页面,用goto
方法导航到作为参数传递的URL。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: false
});
const page = await browser.newPage();
await page.goto('https://www.github.com');
复制代码
当您运行这段代码时,应弹出一个Chrome窗口,并在新标签页中导航到URL。
与Puppeteer一起使用自动化
为了让Puppeteer与页面交互,我们需要手动检查页面,并指定要针对的DOM元素。
我们需要确定选择器,即类名、id、元素类型,或其中几个的组合。如果我们需要高度的特殊性,我们可以用各种Puppeteer方法来使用这些选择器。
现在,让我们用浏览器来检查www.github.com。我们需要能够关注页面顶部的搜索输入栏,然后输入我们想要搜索的关键词。然后我们需要在键盘上GitEnter
按钮。
在你喜欢的浏览器上打开www.github.com–我用的是Chrome,但任何浏览器都可以–在页面上点击右键,点击检查。然后,在元素标签下,你可以看到DOM树。使用检查窗格左上角的检查工具,你可以点击元素,在DOM树中突出显示它们。
我们感兴趣的输入字段元素有几个类名,但只针对.header-search-input
,就足够了。为了确保我们所指的是正确的元素,我们可以在浏览器控制台快速测试。点击控制台标签,在Document
对象上使用querySelector
方法。
document.querySelector('.header-search-input')
复制代码
如果这能返回正确的元素,那么我们就知道它能与Puppeteer一起工作。
注意,可能有几个元素与同一个选择器相匹配。在这种情况下,querySelector
返回第一个匹配的元素。为了引用正确的元素,你需要使用querySelectorAll
,然后从返回的元素的NodeList
中挑选正确的索引。
这里有一件事我们应该注意。如果你调整GitHub网页的大小,输入栏就会变得不可见,而在汉堡包菜单中可以看到。
因为除非汉堡包菜单打开,否则它是不可见的,所以我们无法关注它。为了确保输入框是可见的,我们可以通过将defaultViewport
对象传递给设置,明确地设置浏览器窗口大小。
const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 1920,
height: 1080
}
});
复制代码
现在是时候使用查询来锁定该元素了。在尝试与该元素进行交互之前,我们必须确保它在页面上已被渲染并准备好。Puppeteer有waitForSelector
方法,就是这个原因。
它把选择器字符串作为第一个参数,把选项对象作为第二个参数。因为我们要与该元素进行交互,即聚焦,然后在输入框中输入,我们需要让它在页面上可见,因此有了visible: true
这个选项。
const inputField = '.header-search-input';
await page.waitForSelector(inputField, { visible: true });
复制代码
如前所述,我们需要聚焦于输入栏元素,然后模拟打字。为了这些目的,Puppeteer有以下方法。
const keyword = 'react';
await page.focus(inputField);
await page.keyboard.type(keyword);
复制代码
到目前为止,service.mjs
,看起来如下。
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: false,
defaultViewport: {
width: 1920,
height: 1080
}
});
const page = await browser.newPage();
await page.goto('https://www.github.com');
const inputField = '.header-search-input';
const keyword = 'react'
await page.waitForSelector(inputField);
await page.focus(inputField);
await page.keyboard.type(keyword);
复制代码
当你运行代码时,你应该看到搜索字段被聚焦,而且它有输入的react
关键字。
现在,模拟在键盘上按下Enter
键。
await page.keyboard.press('Enter');
复制代码
在我们按下回车键后,Chrome会导航到一个新的页面。如果我们手动搜索关键词并检查我们被导航到的页面,我们会发现我们感兴趣的元素的选择器是.repo-list
。
在这一点上,我们需要确保导航到新页面是完整的。要做到这一点,有一个page.waitForNavigation
方法。在导航之后,我们再次需要通过使用page.waitForSelector
方法来等待该元素。
然而,如果我们只对从该元素中刮取一些数据感兴趣,我们不需要等到它在视觉上可见。所以,这一次,我们可以省略{ visible: true }
,该选项默认设置为false
。
const repoList = '.repo-list';
await page.waitForNavigation();
await page.waitForSelector(repoList);
复制代码
一旦我们知道.repo-list
选择器在DOM树中,那么我们就可以通过使用page.evaluate
方法来搜刮标题了。
首先,我们通过将repoList
变量传递给querySelector
来选择.repo-list
。然后我们级联querySelectorAll
,得到所有的li
元素,并从NodeList
的元素中选择第一个元素。
最后,我们添加另一个querySelector
,目标是.f4.text-normal
查询,它有我们通过innerText
访问的标题。
const title = await page.evaluate((repoList) => (
document
.querySelector(repoList)
.querySelectorAll('li')[0]
.querySelector('.f4.text-normal')
.innerText
), repoList);
复制代码
现在我们可以把所有的东西都包在一个函数里面,并把它导出到另一个文件中使用,在那里我们将为Express服务器设置一个端点来提供数据。
service.mjs
的最终版本返回一个异步函数,该函数将关键字作为输入。在该函数内部,我们使用一个try…catch
块来捕捉并返回任何错误。最后,我们调用browser.close
来关闭我们所启动的浏览器。
import puppeteer from 'puppeteer';
const service = async (keyword) => {
const browser = await puppeteer.launch({
headless: true,
defaultViewport: {
width: 1920,
height: 1080
}
});
const inputField = '.header-search-input';
const repoList = '.repo-list';
try {
const page = await browser.newPage();
await page.goto('https://www.github.com');
await page.waitForSelector(inputField);
await page.focus(inputField);
await page.keyboard.type(keyword);
await page.keyboard.press('Enter');
await page.waitForNavigation();
await page.waitForSelector(repoList);
const title = await page.evaluate((repoList) => (
document
.querySelector(repoList)
.querySelectorAll('li')[0]
.querySelector('.f4.text-normal')
.innerText
), repoList);
await browser.close();
return title;
} catch (e) {
throw e;
}
}
export default service;
复制代码
创建一个Express服务器
我们需要一个单一的端点来提供数据,在这里我们捕获要搜索的关键词作为一个路由参数。因为我们将路由路径定义为/:keyword
,所以它在req.params
对象中以keyword
为键暴露出来。接下来,我们调用service
函数,将这个关键词作为输入参数传递给Puppeteer运行。
server.mjs
的内容如下。
import express from 'express';
import service from './service.mjs';
const app = express();
app.listen(5000);
app.get('/:keyword', async (req, res) => {
const { keyword } = req.params;
try {
const response = await service(keyword);
res.status(200).send(response);
} catch (e) {
res.status(500).send(e);
}
});
复制代码
在终端中,运行node server.mjs
,启动服务器。在另一个终端窗口中,通过使用curl
,向端点发送一个请求。这应该返回条目标题中的字符串值。
curl localhost:5000/react
复制代码
请注意,这个服务器是最基本的。在生产中,你应该保护你的端点并设置CORS,以防你需要从浏览器而不是服务器发送请求。
部署到谷歌云功能
现在我们要把这个服务部署到无服务器的云函数上。云函数和服务器的主要区别在于,云函数在请求时被快速调用,并保持一段时间以响应后续请求,而服务器则始终处于运行状态。
部署到谷歌云功能是非常直接的。然而,为了成功运行Puppeteer,你应该注意一些设置。
首先,为你的云功能分配足够的内存。根据我的测试,512MB对Puppeteer来说是足够的,但如果你遇到了与内存有关的问题,请分配更多。
package.json
的内容应该如下。
{
"name": "puppeteer-example",
"version": "0.0.1",
"type": "module",
"dependencies": {
"puppeteer": "^10.2.0",
"express": "^4.17.1"
}
}
复制代码
我们将puppeteer
和express
作为依赖项,并设置 “类型”:”模块”,以便使用ES6语法。
现在创建一个名为service.js
的文件,并将我们在service.mjs
中使用的内容填入其中。
index.js
的内容如下。
import express from 'express';
import service from './service.js';
const app = express();
app.get('/:keyword', async (req, res) => {
const { keyword } = req.params;
try {
const response = await service(keyword);
res.status(200).send(response);
} catch (e) {
res.status(500).send(e);
}
});
export const run = app;
复制代码
在这里,我们从express
包和我们的函数导入了server.js
。
与我们在localhost上测试的服务器代码不同,我们不需要监听一个端口,因为这一点是自动处理的。
而且,与localhost不同,我们需要在云端函数中导出app
对象。将入口点设置为run
或任何你要导出的变量名称,如下图所示。默认情况下,这被设置为helloWorld
。
为了方便测试我们的云函数,让我们把它公开。选择云功能*(注意:它旁边有一个复选框*),然后点击顶栏菜单中的权限按钮。这将显示一个侧板,你可以在那里添加负责人。点击添加委托人,在新委托人字段中搜索allUsers
。最后,选择Cloud Function Invoker
作为角色。
请注意,一旦添加了这个委托人,任何拥有触发链接的人都可以调用这个功能。对于测试来说,这很好,但要确保为你的云功能实现认证,以避免不受欢迎的调用,这将反映在账单上。
现在点击你的函数,查看函数的细节。导航到触发器标签,在那里你会找到触发器的URL。点击这个链接来调用这个函数,它返回列表中第一个资源库的标题。现在你可以在你的应用程序中使用这个链接来获取数据。
总结
我们已经介绍了如何使用Puppeteer来自动实现与网页的基本交互,并通过Node服务器上的Express框架来刮取内容,为其服务。然后,我们将其部署在Google Cloud Functions上,以使其成为一个微服务,然后可以在另一个应用程序中集成和使用。