使用 Node/Express 和 Puppeteer 构建基于 Google 搜索的搜索引擎 API
本文将介绍如何使用 Node/Express 和 Puppeteer 构建一个搜索引擎 API。它将利用网页爬虫技术从 Google 获取搜索结果。
如果你还没看过第一篇文章,我强烈建议你去看一下!它介绍了使用 Puppeteer 进行网络爬虫的基础知识。
注意:虽然第二部分和第三部分讨论的概念仍然有效,但用于演示这些概念的示例已不再适用。这是网络爬虫的特性。如果网站更改了某个 HTML 元素的类名,那么网络爬虫也需要进行相应的调整。在本例中,我们使用了 Google 在撰写本文时使用的类名,但这些类名此后已发生更改,因此该示例不再有效。
因此,有时最好找到一种动态的方式来定位元素,这样即使类名或元素 ID 发生变化,网络爬虫仍然可以继续运行。
这是三部曲系列文章的第一部分:
- 第一部分:Puppeteer 的基础知识和创建一个简单的网络爬虫。
- 第二部分:使用 Node/Express 和 Puppeteer 创建基于 Google 搜索的搜索引擎 API。
- 第三部分:优化我们的 API、提高性能、故障排除基础知识以及将我们的 Puppeteer API 部署到 Web。
目录 - 第二部分
API 要求
在开始之前,了解我们要构建什么非常重要。我们将构建一个 API,它将接收搜索请求并返回JSON来自 Google 搜索结果的前几条结果。
我们最关心的结果信息:
- 网站标题
- 网站描述
- 网站网址
搜索请求将是一个GET请求,我们将使用URL 查询参数来指定搜索查询。用户将发送一个/search包含搜索查询的请求searchquery=cats:
localhost:3000/search?searchquery=cat
我们的 API 预计将返回 Google 搜索中关于猫的排名前列的结果JSON:
[
{
title: 'Cats Are Cool',
description: 'This website is all about cats and cats are cool',
url: 'catsarecool.com'
},
...
{
title: 'Cats funny videos',
description: 'Videos all about cats and they are funny!',
url: 'catsfunnyvideos.com'
}
]
既然我们已经了解了需求,就可以开始构建我们的 API 了。
设置 Node/Express 服务器
如果您想跳过 Node/Express 服务器的设置,可以直接跳到编写 Puppeteer 代码来抓取 Google 的部分。不过我建议您先阅读这部分内容。
首先,我们将创建一个新的项目目录并初始化 npm:
mkdir search-engine-api
cd search-engine-api
npm init -y
为了Express.js创建这个简单的 API,我们需要安装 `<api_name>`、`<api_name>` 和 `<api_name> express` puppeteer。nodemon我们将使用 ` nodemon<api_name>` 进行开发。`<api_name>`Nodemon会检测服务器文件中的任何更改并自动重启服务器。从长远来看,这将节省我们的时间。
npm i express puppeteer nodemon
现在我们可以创建服务器文件了:
touch server.js
完成上述步骤后,我们需要配置服务器package.json并添加npm start启动脚本。为了便于开发,我们可以创建一个脚本nodemon。我们将使用npm run dev该脚本来运行 nodemon 脚本:
{
"name": "search-engine-api",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"nodemon": "^2.0.2",
"puppeteer": "^2.0.0"
}
}
现在,如果我们运行npm run dev并尝试修改文件server.js,nodemon 会自动重启服务器。现在我们可以开始编写服务器代码了。
在开始构建 API 之前,我们需要搭建一个简单的Express服务器。我们将使用Express 文档Hello World提供的示例:
const express = require('express');
const app = express();
const port = 3000;
//Catches requests made to localhost:3000/
app.get('/', (req, res) => res.send('Hello World!'));
//Initialises the express server on the port 30000
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
这会在本地机器的 3000 端口上创建一个 Express 服务器。如果有人GET向localhost:3000/我们的服务器发送请求,服务器会返回响应。我们可以通过在浏览器中Hello World打开 URL 来查看其运行情况。localhost:3000/
我们将为搜索创建一个新的路由。在这里,我们将通过 URL 中的查询参数传递信息。例如,如果我们想要搜索“dogs”的结果,我们可以向以下地址发送请求:
localhost:3000/search?searchquery=dogs
为了实现这一点,我们需要使用GETExpress 创建一个新的请求函数,并且由于我们预期这是一个GET请求,因此我们可以使用app.get(route, callbackFunc)
const express = require('express');
const puppeteer = require('puppeteer');
const app = express();
const port = 3000;
//Catches requests made to localhost:3000/search
app.get('/search', (request, response) => {
//Do something when someone makes request to localhost:3000/search
//request parameter - information about the request coming in
//response parameter - response object that we can use to send a response
});
//Catches requests made to localhost:3000/
app.get('/', (req, res) => res.send('Hello World!'));
//Initialises the express server on the port 30000
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
现在我们有了一个可以捕获发送到该路由的请求的函数localhost:3000/search,接下来我们可以研究如何利用 URL 中的任何查询参数。任何发送到该路由的请求都会执行此处理程序中的回调函数。
Express 允许我们通过请求参数访问查询参数。在本例中,由于我们将查询字段命名为 `<query_field>` searchquery,因此我们可以通过该字段名称访问它:
//Catches requests made to localhost:3000/search
app.get('/search', (request, response) => {
//Holds value of the query param 'searchquery'
const searchQuery = request.query.searchquery;
});
但是,如果此查询不存在,则我们无从搜索,因此我们可以仅在提供搜索查询时才执行操作。如果搜索查询不存在,我们可以快速结束响应,不返回任何数据。response.end()
//Catches requests made to localhost:3000/search
app.get('/search', (request, response) => {
//Holds value of the query param 'searchquery'.
const searchQuery = request.query.searchquery;
//Do something when the searchQuery is not null.
if(searchQuery != null){
}else{
response.end();
}
});
现在我们的Node/Express服务器已经搭建完毕,可以开始编写爬虫程序的代码了。
使用 Puppeteer 创建搜索引擎 API
在进行谷歌网页抓取时,直接在谷歌搜索中搜索内容的一种方法,是将搜索查询作为 URL 查询参数传递:
https://www.google.com/search?q=cat
这将显示我们在谷歌上搜索关键词“猫”的结果。这当然是理想的方法,但为了本文的目的,我们将采用比较繁琐的方式:打开google.com(首页),puppeteer在搜索框中输入关键词,然后点击Enter获取结果。
我们这样做是因为并非所有网站都使用查询参数,有时要进入网站的下一步(在我们的例子中是结果页面),唯一的方法是在第一步中手动操作。
目前我们的server.js系统看起来是这样的:
const express = require('express');
const puppeteer = require('puppeteer');
const app = express();
const port = 3000;
//Catches requests made to localhost:3000/search
app.get('/search', (request, response) => {
//Holds value of the query param 'searchquery'.
const searchQuery = request.query.searchquery;
//Do something when the searchQuery is not null.
if(searchQuery != null){
}else{
response.end();
}
});
//Catches requests made to localhost:3000/
app.get('/', (req, res) => res.send('Hello World!'));
//Initialises the express server on the port 30000
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
我们将创建一个名为 `.` 的新函数searchGoogle。该函数将接收 `.`作为输入参数,并返回一个包含最佳结果的searchQuery数组。JSON
在开始编写searchGoogle代码之前puppeteer,我们要先编写函数的结构图,以便了解代码应该如何运行:
const express = require('express');
const puppeteer = require('puppeteer');
const app = express();
const port = 3000;
//Catches requests made to localhost:3000/search
app.get('/search', (request, response) => {
//Holds value of the query param 'searchquery'.
const searchQuery = request.query.searchquery;
//Do something when the searchQuery is not null.
if (searchQuery != null) {
searchGoogle(searchQuery)
.then(results => {
//Returns a 200 Status OK with Results JSON back to the client.
response.status(200);
response.json(results);
});
} else {
response.end();
}
});
//Catches requests made to localhost:3000/
app.get('/', (req, res) => res.send('Hello World!'));
//Initialises the express server on the port 30000
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
由于该puppeteer函数是异步工作的,我们需要等待其返回结果searchGoogle。因此,我们需要添加一个回调函数.then,以确保在searchGoogle处理并获取结果后再使用它们。我们可以通过回调函数访问这些结果,该回调函数会将结果作为第一个参数。之后,我们可以使用该函数向客户端发送响应response.json()。
response.json()它会向客户端返回一个响应。你可以使用不同的方法来处理这个响应。你可以在Express 官方文档JSON中了解更多信息。
现在我们可以开始编写代码并构建 Puppeteer 函数了searchGoogle。为此,我们将在同一目录下创建一个新文件。这是因为使用单独的文件可以让我们测试 Puppeteer 文件,而无需手动向服务器发出请求,这可以节省时间。我们将其命名为searchGoogle.js:
touch searchGoogle.js
现在我们需要初始化文件中的函数:
const puppeteer = require('puppeteer');
const searchGoogle = async (searchQuery) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com');
await browser.close();
};
export default searchGoogle;
现在,我们启动了一个无头 Chrome 实例并浏览 Google 网站。接下来,我们需要找到搜索栏,以便输入查询内容。为此,我们需要查看 Google 首页的源代码。
使用鼠标工具选择元素后,我们可以看到HTML此搜索栏:
我们可以看到它具有以下功能:name="q"我们可以使用它来识别和定位输入puppeteer。要输入我们的搜索查询,Puppeteer 为页面提供了一个函数page.type(selector, textToType)。有了这个函数,我们可以定位任何表单并直接输入我们的值:
const puppeteer = require('puppeteer');
const searchGoogle = async (searchQuery) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com');
//Finds input element with name attribue 'q' and types searchQuery
await page.type('input[name="q"]', searchQuery);
await browser.close();
};
export default searchGoogle;
为了确保一切正常,我们可以在输入完成后截个图:
const puppeteer = require('puppeteer');
const searchGoogle = async (searchQuery) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com');
//Finds input element with name attribue 'q' and types searchQuery
await page.type('input[name="q"]', searchQuery);
await page.screenshot({path: 'example.png'});
await browser.close();
};
//Exports the function so we can access it in our server
module.exports = searchGoogle;
searchGoogle('cats');
如您所见,在文件末尾我们调用了该searchGoogle函数。这是为了开始测试它。现在我们可以打开命令行并执行:
node searchGoogle.js
几秒钟后,文件应该执行完毕,您就可以看到屏幕截图了:
现在,我们只需要puppeteer按下键盘上的“回车”键,或者点击搜索栏下方的“谷歌搜索”按钮即可。
两种方法都可行,但为了更精确,我们将让傀儡师按下“谷歌搜索”按钮。不过,如果您按下回车键,操作方式如下:
await page.keyboard.press('Enter');
我们将再次检查该页面,查找有关“谷歌搜索”按钮的信息。检查结果如下:
我们可以看到它的名称是“btnK”。我们可以利用这个名称来选中该元素并点击它:
//Finds the first input with name 'btnK', after it is found, it executes .click() DOM Event Method
await page.$eval('input[name=btnK]', button => button.click());
已将其添加到我们的文件中:
const puppeteer = require('puppeteer');
const searchGoogle = async (searchQuery) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com');
//Finds input element with name attribue 'q' and types searchQuery
await page.type('input[name="q"]', searchQuery);
//Finds an input with name 'btnK', after so it executes .click() DOM Method
await page.$eval('input[name=btnK]', button => button.click());
await page.screenshot({path: 'example.png'});
await browser.close();
};
searchGoogle('cats');
//Exports the function so we can access it in our server
module.exports = searchGoogle;
执行该文件并查看屏幕截图后,得到以下结果:
我们需要确保等待谷歌加载完所有搜索结果后再进行任何操作。有多种方法可以做到这一点。如果我们想等待特定时间,可以使用以下方法:
await page.waitFor(durationInMilliseconds)
或者,如果我们已经知道要查找的元素,那么我们可以waitForSelector等待 Puppeteer 加载第一个具有匹配选择器的元素后再继续:
await page.waitForSelector('selector');
这将等待选择器加载完毕后再继续执行。要使用此功能,我们需要先确定selector结果的匹配项,以便 Puppeteer 可以等待结果选择器加载完毕后再继续执行。请注意,此功能只会等待找到的第一个选择器。
查看HTML搜索结果的源代码后,我发现所有搜索结果都存储在一个div带有 id 的列表中search:
因此我们可以使用waitForSelector(selector)以下方式定位和使用 div id=search:
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com');
//Finds input element with name attribue 'q' and types searchQuery
await page.type('input[name="q"]', searchQuery);
//Finds an input with name 'btnK', after so it executes .click() DOM Method
await page.$eval('input[name=btnK]', button => button.click());
//Wait until the first div element with id search laods
await page.waitForSelector('div[id=search]');
await page.screenshot({path: 'example.png'});
await browser.close();
};
searchGoogle('cats');
//Exports the function so we can access it in our server
module.exports = searchGoogle;
现在结果已经加载完毕,我们可以开始解析它们了。如果您想跳过查找包含相关信息的div元素的部分,可以直接跳到实现部分。
如果我们仔细查看源代码,理解 HTML 的含义,就会发现我们正在寻找的信息存储在带有特定 class 的 div 元素中,class=bkWMgd但是并非所有带有该 class 的 div 元素都包含相关信息,其中一些包含视频推荐、新闻报道等等。我们感兴趣的是那些带有h2标题和Web Results文本的 div 元素。
仔细观察这个 div 元素,我们会发现它的嵌套层级非常深。因此,我们需要使用特殊的选择器来定位深层子元素。主要信息存储在类名为 `<div class=" 'g'...
我们可以针对我们关心的特定div元素进行操作。我们将使用'>'称为子组合器的CSS选择器来定位嵌套信息。
我们可以像这样定位嵌套元素:
<div class='1'>
<div class='2'>
<div class='3'>
<p>Information</p>
</div>
</div>
</div>
对于结构如下的 HTML 文件,我们可以通过以下方式访问段落:
'div[class=1] > div[class=2] > div[class=3] > p'
我们可以选择包含结果的 div 元素:
//Finds the first div with class 'bkWMgd' and returns it
const parent = await page.$eval('div[class=bkWMgd]', result => result);
由于 `parent` 变量代表的是从 `<div>` 返回的 DOM 节点page.$eval(),我们可以对该对象执行 HTML DOM 方法。由于所有信息都包含在带有 `class` 的 `div` 元素中,g我们可以将 `parent` 设置为它的直接子元素。
//Sets the parent to the div with all the information
parent = parent.querySelector('div[class=g]');
有了这项功能,我们现在就可以精准定位我们关心的信息,这些信息可以在这张图片中看到:
标题
//Targets h3 Website Title i.e. 'Cats (2019 film) - Wikipedia'
const title = parent.querySelector('div[class=rc] > div[class=r] > a > h3').innerText;
URL
//Targets the <a> href link i.e. 'https://en.wikipedia.org/wiki/Cats_(2019_film)'
const url = parent.querySelector('div[class=rc] > div[class=r] > a').href;
描述
const desc = parent.querySelector('div[class=rc] > div[class=s] > div > span[class=st]').innerText;
现在我们知道了如何定位信息,可以将其添加到文件中。我们之前只研究了如何解析单个搜索结果的信息,但实际上会有多个搜索结果,所以我们需要使用 ` h2` 标签page.$$eval定位所有带有 `<h2>` 标签的 `<div>` 元素,并使用 `class` 标签定位所有带有 `<class>` 标签的 ` <div> ` 元素。我们可以看到,有些 `<div>` 元素对应多个搜索结果:Web resultsg
当有多个带有 class 的 div 元素时,g它们会嵌套在另一个带有 class 的 div 元素中srg。让我们开始将所有这些添加到代码中,以便将所有部分组合起来。请仔细阅读这段代码,它可能看起来有点复杂,但它是基于上面的屏幕截图编写的。
//Find all div elements with class 'bkWMgd'
const searchResults = await page.$$eval('div[class=bkWMgd]', results => {
//Array to hold all our results
let data = [];
//Iterate over all the results
results.forEach(parent => {
//Check if parent has h2 with text 'Web Results'
const ele = parent.querySelector('h2');
//If element with 'Web Results' Title is not found then continue to next element
if (ele === null) {
return;
}
//Check if parent contains 1 div with class 'g' or contains many but nested in div with class 'srg'
let gCount = parent.querySelectorAll('div[class=g]');
//If there is no div with class 'g' that means there must be a group of 'g's in class 'srg'
if (gCount.length === 0) {
//Targets all the divs with class 'g' stored in div with class 'srg'
gCount = parent.querySelectorAll('div[class=srg] > div[class=g]');
}
//Iterate over all the divs with class 'g'
gCount.forEach(result => {
//Target the title
const title = result.querySelector('div[class=rc] > div[class=r] > a > h3').innerText;
//Target the url
const url = result.querySelector('div[class=rc] > div[class=r] > a').href;
//Target the description
const desciption = result.querySelector('div[class=rc] > div[class=s] > div > span[class=st]').innerText;
//Add to the return Array
data.push({title, desciption, url});
});
});
//Return the search results
return data;
});
上面的代码会解析页面并将结果存储在一个数组中。现在我们可以从主函数中返回该数组searchGoogle:
const puppeteer = require('puppeteer');
const searchGoogle = async (searchQuery) => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://google.com');
//Finds input element with name attribue 'q' and types searchQuery
await page.type('input[name="q"]', searchQuery);
//Finds an input with name 'btnK', after so it executes .click() DOM Method
await page.$eval('input[name=btnK]', button => button.click());
//Wait for one of the div classes to load
await page.waitForSelector('div[id=search]');
const searchResults = await page.$$eval('div[class=bkWMgd]', results => {
//Array to hold all our results
let data = [];
...
...
//Return the search results
return data;
});
await browser.close();
return searchResults;
};
module.exports = searchGoogle;
现在我们可以删除最后一行手动调用函数的代码了。至此,搜索引擎 API 的实现就完成了!现在,我们只需要在主server.js文件中导入这个函数即可:
const express = require('express');
const app = express();
const port = 3000;
//Import puppeteer function
const searchGoogle = require('./searchGoogle');
//Catches requests made to localhost:3000/search
app.get('/search', (request, response) => {
//Holds value of the query param 'searchquery'.
const searchQuery = request.query.searchquery;
//Do something when the searchQuery is not null.
if (searchQuery != null) {
searchGoogle(searchQuery)
.then(results => {
//Returns a 200 Status OK with Results JSON back to the client.
response.status(200);
response.json(results);
});
} else {
response.end();
}
});
//Catches requests made to localhost:3000/
app.get('/', (req, res) => res.send('Hello World!'));
//Initialises the express server on the port 30000
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
现在,如果我们启动服务器npm start,然后在浏览器中浏览到:
http://localhost:3000/search?searchquery=cats
我们获取到了一个 JSON 数据!我使用了一个JSON Viewer Chrome 扩展程序,以便在浏览器中查看 JSON 数据。
该项目的代码可以在Github上找到。
不过,我们还没完成。目前,我们的 API 已经准备就绪,但速度有点慢。而且它目前只运行在本地机器上,所以我们需要把它部署到其他地方。这些内容将在第三部分中详细介绍!
第三部分将涵盖以下内容:
-
优化和提升性能
-
故障排除基础知识
-
部署 API
本文到此结束!希望您喜欢阅读本文,并觉得它对您有所帮助。敬请期待第三部分!
如果您对其他用例感兴趣,可以看看净收入计算器,它使用 Node/Express Puppeteer API 从网站抓取州税和城市平均租金等信息。您可以访问它的GitHub 代码库查看。
如果您喜欢这篇文章并想提供反馈,可以匿名在此处留言。任何反馈我们都非常欢迎!
文章来源:https://dev.to/waqasabbasi/building-a-search-engine-api-with-node-express-and-puppeteer-using-google-search-4m21







