发布于 2026-01-06 0 阅读
0

使用 Node/Express 和 Puppeteer 构建基于 Google 搜索的搜索引擎 API

使用 Node/Express 和 Puppeteer 构建基于 Google 搜索的搜索引擎 API

本文将介绍如何使用 Node/Express 和 Puppeteer 构建一个搜索引擎 API。它将利用网页爬虫技术从 Google 获取搜索结果。

如果你还没看过第一篇文章,我强烈建议你去看一下!它介绍了使用 Puppeteer 进行网络爬虫的基础知识。

注意:虽然第二部分和第三部分讨论的概念仍然有效,但用于演示这些概念的示例已不再适用。这是网络爬虫的特性。如果网站更改了某个 HTML 元素的类名,那么网络爬虫也需要进行相应的调整。在本例中,我们使用了 Google 在撰写本文时使用的类名,但这些类名此后已发生更改,因此该示例不再有效。

因此,有时最好找到一种动态的方式来定位元素,这样即使类名或元素 ID 发生变化,网络爬虫仍然可以继续运行。

这是三部曲系列文章的第一部分:

  1. 第一部分:Puppeteer 的基础知识和创建一个简单的网络爬虫。
  2. 第二部分:使用 Node/Express 和 Puppeteer 创建基于 Google 搜索的搜索引擎 API。
  3. 第三部分:优化我们的 API、提高性能、故障排除基础知识以及将我们的 Puppeteer API 部署到 Web。

目录 - 第二部分

API 要求

在开始之前,了解我们要构建什么非常重要。我们将构建一个 API,它将接收搜索请求并返回JSON来自 Google 搜索结果的前几条结果。

我们最关心的结果信息:

  • 网站标题
  • 网站描述
  • 网站网址

搜索请求将是一个GET请求,我们将使用URL 查询参数来指定搜索查询。用户将发送一个/search包含搜索查询的请求searchquery=cats



localhost:3000/search?searchquery=cat


Enter fullscreen mode Exit fullscreen mode

我们的 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'
    }
]


Enter fullscreen mode Exit fullscreen mode

既然我们已经了解了需求,就可以开始构建我们的 API 了。

设置 Node/Express 服务器

如果您想跳过 Node/Express 服务器的设置,可以直接跳到编写 Puppeteer 代码来抓取 Google 的部分。不过我建议您先阅读这部分内容。

首先,我们将创建一个新的项目目录并初始化 npm:



mkdir search-engine-api
cd search-engine-api
npm init -y


Enter fullscreen mode Exit fullscreen mode

为了Express.js创建这个简单的 API,我们需要安装 `<api_name>`、`<api_name>` 和 `<api_name> express` puppeteernodemon我们将使用 ` nodemon<api_name>` 进行开发。`<api_name>`Nodemon会检测服务器文件中的任何更改并自动重启服务器。从长远来看,这将节省我们的时间。



npm i express puppeteer nodemon


Enter fullscreen mode Exit fullscreen mode

现在我们可以创建服务器文件了:



touch server.js


Enter fullscreen mode Exit fullscreen mode

完成上述步骤后,我们需要配置服务器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"
  }
}


Enter fullscreen mode Exit fullscreen mode

现在,如果我们运行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}!`));



Enter fullscreen mode Exit fullscreen mode

这会在本地机器的 3000 端口上创建一个 Express 服务器。如果有人GETlocalhost:3000/我们的服务器发送请求,服务器会返回响应。我们可以通过在浏览器中Hello World打开 URL 来查看其运行情况。localhost:3000/

我们将为搜索创建一个新的路由。在这里,我们将通过 URL 中的查询参数传递信息。例如,如果我们想要搜索“dogs”的结果,我们可以向以下地址发送请求:



localhost:3000/search?searchquery=dogs


Enter fullscreen mode Exit fullscreen mode

为了实现这一点,我们需要使用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}!`));



Enter fullscreen mode Exit fullscreen mode

现在我们有了一个可以捕获发送到该路由的请求的函数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;
});


Enter fullscreen mode Exit fullscreen mode

但是,如果此查询不存在,则我们无从搜索,因此我们可以仅在提供搜索查询时才执行操作。如果搜索查询不存在,我们可以快速结束响应,不返回任何数据。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();
  }
});


Enter fullscreen mode Exit fullscreen mode

现在我们的Node/Express服务器已经搭建完毕,可以开始编写爬虫程序的代码了。

使用 Puppeteer 创建搜索引擎 API

在进行谷歌网页抓取时,直接在谷歌搜索中搜索内容的一种方法,是将搜索查询作为 URL 查询参数传递:



https://www.google.com/search?q=cat


Enter fullscreen mode Exit fullscreen mode

这将显示我们在谷歌上搜索关键词“猫”的结果。这当然是理想的方法,但为了本文的目的,我们将采用比较繁琐的方式:打开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}!`));



Enter fullscreen mode Exit fullscreen mode

我们将创建一个名为 `.` 的新函数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}!`));



Enter fullscreen mode Exit fullscreen mode

由于该puppeteer函数是异步工作的,我们需要等待其返回结果searchGoogle。因此,我们需要添加一个回调函数.then,以确保在searchGoogle处理并获取结果后再使用它们。我们可以通过回调函数访问这些结果,该回调函数会将结果作为第一个参数。之后,我们可以使用该函数向客户端发送响应response.json()

response.json()它会向客户端返回一个响应。你可以使用不同的方法来处理这个响应。你可以在Express 官方文档JSON中了解更多信息

现在我们可以开始编写代码并构建 Puppeteer 函数了searchGoogle。为此,我们将在同一目录下创建一个新文件。这是因为使用单独的文件可以让我们测试 Puppeteer 文件,而无需手动向服务器发出请求,这可以节省时间。我们将其命名为searchGoogle.js



touch searchGoogle.js


Enter fullscreen mode Exit fullscreen mode

现在我们需要初始化文件中的函数:



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;


Enter fullscreen mode Exit fullscreen mode

现在,我们启动了一个无头 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;


Enter fullscreen mode Exit fullscreen mode

为了确保一切正常,我们可以在输入完成后截个图:



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');


Enter fullscreen mode Exit fullscreen mode

如您所见,在文件末尾我们调用了该searchGoogle函数。这是为了开始测试它。现在我们可以打开命令行并执行:



node searchGoogle.js


Enter fullscreen mode Exit fullscreen mode

几秒钟后,文件应该执行完毕,您就可以看到屏幕截图了:

屏幕截图结果

现在,我们只需要puppeteer按下键盘上的“回车”键,或者点击搜索栏下方的“谷歌搜索”按钮即可。

谷歌搜索按钮

两种方法都可行,但为了更精确,我们将让傀儡师按下“谷歌搜索”按钮。不过,如果您按下回车键,操作方式如下:



 await page.keyboard.press('Enter');


Enter fullscreen mode Exit fullscreen mode

我们将再次检查该页面,查找有关“谷歌搜索”按钮的信息。检查结果如下:
按钮代码

我们可以看到它的名称是“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());


Enter fullscreen mode Exit fullscreen mode

已将其添加到我们的文件中:



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;


Enter fullscreen mode Exit fullscreen mode

执行该文件并查看屏幕截图后,得到以下结果:

结果

我们需要确保等待谷歌加载完所有搜索结果后再进行任何操作。有多种方法可以做到这一点。如果我们想等待特定时间,可以使用以下方法:



await page.waitFor(durationInMilliseconds)


Enter fullscreen mode Exit fullscreen mode

或者,如果我们已经知道要查找的元素,那么我们可以waitForSelector等待 Puppeteer 加载第一个具有匹配选择器的元素后再继续:



await page.waitForSelector('selector');


Enter fullscreen mode Exit fullscreen mode

这将等待选择器加载完毕后再继续执行。要使用此功能,我们需要先确定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;


Enter fullscreen mode Exit fullscreen mode

现在结果已经加载完毕,我们可以开始解析它们了。如果您想跳过查找包含相关信息的div元素的部分,可以直接跳到实现部分

如果我们仔细查看源代码,理解 HTML 的含义,就会发现我们正在寻找的信息存储在带有特定 class 的 div 元素中,class=bkWMgd但是并非所有带有该 class 的 div 元素都包含相关信息,其中一些包含视频推荐、新闻报道等等。我们感兴趣的是那些带有h2标题和Web Results文本的 div 元素。

Google源代码

仔细观察这个 div 元素,我们会发现它的嵌套层级非常深。因此,我们需要使用特殊的选择器来定位深层子元素。主要信息存储在类名为 `<div class=" 'g'...

主要信息部分

我们可以针对我们关心的特定div元素进行操作。我们将使用'>'称为子组合器的CSS选择器来定位嵌套信息。

我们可以像这样定位嵌套元素:



<div class='1'>
    <div class='2'>
        <div class='3'>
            <p>Information</p>
        </div>
    </div>
</div>


Enter fullscreen mode Exit fullscreen mode

对于结构如下的 HTML 文件,我们可以通过以下方式访问段落:



'div[class=1] > div[class=2] > div[class=3] > p'


Enter fullscreen mode Exit fullscreen mode

我们可以选择包含结果的 div 元素:



//Finds the first div with class 'bkWMgd' and returns it
const parent = await page.$eval('div[class=bkWMgd]', result => result);


Enter fullscreen mode Exit fullscreen mode

由于 `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]');


Enter fullscreen mode Exit fullscreen mode

有了这项功能,我们现在就可以精准定位我们关心的信息,这些信息可以在这张图片中看到:

标题



//Targets h3 Website Title i.e. 'Cats  (2019 film)  - Wikipedia'
const title = parent.querySelector('div[class=rc] > div[class=r] > a >  h3').innerText;


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

描述



const desc = parent.querySelector('div[class=rc] > div[class=s] > div > span[class=st]').innerText;


Enter fullscreen mode Exit fullscreen mode

现在我们知道了如何定位信息,可以将其添加到文件中。我们之前只研究了如何解析单个搜索结果的信息,但实际上会有多个搜索结果,所以我们需要使用 ` h2` 标签page.$$eval定位所有带有 `<h2>` 标签的 `<div>` 元素,并使用 `class` 标签定位所有带有 `<class>` 标签的 ` <div> ` 元素。我们可以看到,有些 `<div>` 元素对应多个搜索结果:Web resultsg

包含 G 类信息的 Div

当有多个带有 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;
    });


Enter fullscreen mode Exit fullscreen mode

上面的代码会解析页面并将结果存储在一个数组中。现在我们可以从主函数中返回该数组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;


Enter fullscreen mode Exit fullscreen mode

现在我们可以删除最后一行手动调用函数的代码了。至此,搜索引擎 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}!`));


Enter fullscreen mode Exit fullscreen mode

现在,如果我们启动服务器npm start,然后在浏览器中浏览到:



http://localhost:3000/search?searchquery=cats


Enter fullscreen mode Exit fullscreen mode

我们获取到了一个 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