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

如何使用 Node 构建网络爬虫

如何使用 Node 构建网络爬虫

作者:乔丹·伊拉博尔✏️

介绍

网络爬虫(通常简称为爬虫或有时被称为蜘蛛机器人)是一种系统性地浏览互联网的机器人,其主要目的是对网页进行索引。搜索引擎可以利用这些网络机器人来提高用户搜索结果的质量。除了索引万维网之外,爬虫还可以用于收集数据(称为网络抓取)。

网络爬虫过程对 CPU 的消耗相当大,具体取决于网站的结构和提取数据的复杂程度。为了优化和加速这一过程,我们将使用Node 工作线程(线程),它们非常适合处理 CPU 密集型操作。

本文将介绍如何构建一个网络爬虫,用于抓取网站并将数据存储到数据库中。该爬虫机器人将使用 Node 工作节点来执行这两项操作。

LogRocket 免费试用横幅

先决条件

  1. 具备Node.js基础知识
  2. YarnNPM(我们将使用 Yarn)
  3. 配置为运行Node代码的系统(最好是10.5.0或更高版本)

安装

启动终端并创建一个新目录用于本教程:

$ mkdir worker-tutorial
$ cd worker-tutorial
Enter fullscreen mode Exit fullscreen mode

运行以下命令初始化目录:

$ yarn init -y
Enter fullscreen mode Exit fullscreen mode

我们需要以下软件包来构建爬虫:

  • Axios——一个基于 Promise 的 HTTP 客户端,适用于浏览器和 Node.js。
  • Cheerio是一个轻量级的 jQuery 实现,它允许我们访问服务器端的 DOM。
  • Firebase 数据库——一个云端托管的 NoSQL 数据库。如果您不熟悉如何设置 Firebase 数据库,请查看文档按照步骤 1-3 开始操作。

让我们使用以下命令安装上面列出的软件包:

$ yarn add axios cheerio firebase-admin
Enter fullscreen mode Exit fullscreen mode

各位员工好

在开始使用 worker 构建爬虫之前,我们先来了解一些基础知识。你可以hello.js在项目根目录创建一个测试文件来运行以下代码片段。

注册工人

可以通过从模块导入 worker 类来初始化(注册)一个 worker,worker_threads如下所示:

// hello.js

const { Worker } = require('worker_threads');

new Worker("./worker.js");
Enter fullscreen mode Exit fullscreen mode

你好世界

使用 Workers进行打印输出Hello World非常简单,只需运行以下代码片段即可:

// hello.js

const { Worker, isMainThread }  = require('worker_threads');
if(isMainThread){
    new Worker(__filename);
} else{
    console.log("Worker says: Hello World"); // prints 'Worker says: Hello World'
}
Enter fullscreen mode Exit fullscreen mode

这段代码片段从模块中引入工作类和isMainThread对象worker_threads

  • isMainThread帮助我们了解当前是在主线程还是工作线程中运行。
  • new Worker(__filename)使用__filename变量注册一个新的工作进程,在本例中,该变量为hello.js

与员工沟通

当一个新的工作线程(worker)被创建时,会有一个消息端口允许线程间通信。以下代码片段展示了如何在工作线程之间传递消息:

// hello.js

const { Worker, isMainThread, parentPort }  = require('worker_threads');

if (isMainThread) {
    const worker =  new Worker(__filename);
    worker.once('message', (message) => {
        console.log(message); // prints 'Worker thread: Hello!'
    });
    worker.postMessage('Main Thread: Hi!');
} else {
    parentPort.once('message', (message) => {
        console.log(message) // prints 'Main Thread: Hi!'
        parentPort.postMessage("Worker thread: Hello!");
    });
}
Enter fullscreen mode Exit fullscreen mode

在上面的代码片段中,我们在初始化工作线程后,使用 `send` 函数向父线程发送消息parentPort.postMessage()。然后,我们使用 `listen` 函数监听来自父线程的消息parentPort.once()。我们还使用 `send` 函数向工作线程发送消息worker.postMessage(),并使用 `listen` 函数监听来自工作线程的消息worker.once()

运行代码后会产生以下输出:

Main Thread: Hi!
Worker thread: Hello!
Enter fullscreen mode Exit fullscreen mode

构建爬行器

让我们构建一个基本的网络爬虫,它使用 Node 工作节点来爬取网页并写入数据库。爬虫将按以下顺序完成任务:

  1. 从网站获取(请求)HTML
  2. 从响应中提取 HTML
  3. 遍历 DOM 并提取包含汇率的表格
  4. 格式化表格元素(tbody、、trtd),并提取汇率值
  5. 将汇率值存储在对象中,并使用以下方式将其发送到工作线程:worker.postMessage()
  6. 在工作线程中接受来自父线程的消息parentPort.on()
  7. 将消息存储到 Firestore(Firebase 数据库)中

让我们在项目目录中创建两个新文件:

  1. main.js——关于主线
  2. dbWorker.js– 对于工作线程

本教程的源代码已上传至GitHub,欢迎克隆、fork 或提交 issue。

主线程(main.js)

在主线程中,我们将抓取IBAN 网站的数据,获取常用货币兑美元的当前汇率。我们将导入axios这些数据,并使用简单的请求从该网站获取 HTML 代码GET

我们还将使用cheerio遍历 DOM 并从表格元素中提取数据。为了确定要提取的确切元素,我们将在浏览器中打开IBAN 网站并加载开发者工具

开发者工具

从上图中,我们可以看到table带有类名的元素table table-bordered table-hover downloads。这将是一个很好的起点,我们可以将其传递给cheerio根元素选择器:

// main.js

const axios = require('axios');
const cheerio = require('cheerio');
const url = "https://www.iban.com/exchange-rates";

fetchData(url).then( (res) => {
    const html = res.data;
    const $ = cheerio.load(html);
    const statsTable = $('.table.table-bordered.table-hover.downloads > tbody > tr');
    statsTable.each(function() {
        let title = $(this).find('td').text();
        console.log(title);
    });
})

async function fetchData(url){
    console.log("Crawling data...")
    // make http call to url
    let response = await axios(url).catch((err) => console.log(err));

    if(response.status !== 200){
        console.log("Error occurred while fetching data");
        return;
    }
    return response;
}
Enter fullscreen mode Exit fullscreen mode

使用 Node 运行上述代码将得到以下输出:

从示例中抓取数据

接下来,我们将更新该main.js文件,以便正确格式化输出并将其发送到我们的工作线程。

更新主帖

为了正确格式化输出,我们需要删除空格和制表符,因为我们将最终输出存储在文件中JSON。让我们main.js相应地更新文件:

// main.js
[...]
let workDir = __dirname+"/dbWorker.js";

const mainFunc = async () => {
  const url = "https://www.iban.com/exchange-rates";
  // fetch html data from iban website
  let res = await fetchData(url);
  if(!res.data){
    console.log("Invalid data Obj");
    return;
  }
  const html = res.data;
  let dataObj = new Object();
  // mount html page to the root element
  const $ = cheerio.load(html);

  let dataObj = new Object();
  const statsTable = $('.table.table-bordered.table-hover.downloads > tbody > tr');
  //loop through all table rows and get table data
  statsTable.each(function() {
    let title = $(this).find('td').text(); // get the text in all the td elements
    let newStr = title.split("\t"); // convert text (string) into an array
    newStr.shift(); // strip off empty array element at index 0
    formatStr(newStr, dataObj); // format array string and store in an object
  });

  return dataObj;
}

mainFunc().then((res) => {
    // start worker
    const worker = new Worker(workDir); 
    console.log("Sending crawled data to dbWorker...");
    // send formatted data to worker thread 
    worker.postMessage(res);
    // listen to message from worker thread
    worker.on("message", (message) => {
        console.log(message)
    });
});

[...]

function formatStr(arr, dataObj){
    // regex to match all the words before the first digit
    let regExp = /[^A-Z]*(^\D+)/ 
    let newArr = arr[0].split(regExp); // split array element 0 using the regExp rule
    dataObj[newArr[1]] = newArr[2]; // store object 
}
Enter fullscreen mode Exit fullscreen mode

在上面的代码片段中,我们做的不仅仅是数据格式化;在问题mainFunc()解决后,我们将格式化的数据传递给worker线程进行存储。

工作线程(dbWorker.js)

在这个工作线程中,我们将初始化 Firebase 并监听来自主线程的爬取数据。当数据到达时,我们将把它存储到数据库中,并向主线程发送消息以确认数据存储成功。

下面这段代码片段负责执行上述操作:

// dbWorker.js

const { parentPort } = require('worker_threads');
const admin = require("firebase-admin");

//firebase credentials
let firebaseConfig = {
    apiKey: "XXXXXXXXXXXX-XXX-XXX",
    authDomain: "XXXXXXXXXXXX-XXX-XXX",
    databaseURL: "XXXXXXXXXXXX-XXX-XXX",
    projectId: "XXXXXXXXXXXX-XXX-XXX",
    storageBucket: "XXXXXXXXXXXX-XXX-XXX",
    messagingSenderId: "XXXXXXXXXXXX-XXX-XXX",
    appId: "XXXXXXXXXXXX-XXX-XXX"
};

// Initialize Firebase
admin.initializeApp(firebaseConfig);
let db = admin.firestore();
// get current data in DD-MM-YYYY format
let date = new Date();
let currDate = `${date.getDate()}-${date.getMonth()}-${date.getFullYear()}`;
// recieve crawled data from main thread
parentPort.once("message", (message) => {
    console.log("Recieved data from mainWorker...");
    // store data gotten from main thread in database
    db.collection("Rates").doc(currDate).set({
        rates: JSON.stringify(message)
    }).then(() => {
        // send data back to main thread if operation was successful
        parentPort.postMessage("Data saved successfully");
    })
    .catch((err) => console.log(err))    
});
Enter fullscreen mode Exit fullscreen mode

注意:要在 Firebase 上设置数据库,请访问Firebase 文档并按照步骤 1-3 开始操作。

使用 Node运行main.js(包括dbWorker.js)将产生以下输出:

节点输出

您现在可以查看您的 Firebase 数据库,并会看到以下已抓取的数据:

Firebase 数据展示

最后说明

虽然网络爬虫很有趣,但如果您利用数据侵犯版权,则可能触犯法律。通常建议您事先阅读目标网站的条款和条件,了解其数据爬取政策。您可以在本页面的“爬取政策”部分了解更多信息。

使用工作线程并不能保证应用程序运行速度更快,但如果使用得当,它可以营造出速度更快的假象,因为它通过减轻主线程上 CPU 密集型任务的负担来释放主线程。

结论

在本教程中,我们学习了如何构建一个网络爬虫,用于抓取货币汇率并将其保存到数据库中。我们还学习了如何使用工作线程来执行这些操作。

以下每个代码片段的源代码都可以在GitHub上找到。欢迎克隆、fork 或提交 issue。

延伸阅读

想了解更多关于工作线程的信息?您可以查看以下链接:


仅限 200 版本:监控生产环境中失败和缓慢的网络请求

部署基于 Node 的 Web 应用或网站并不难,难的是确保 Node 实例持续为应用提供资源。如果您希望确保对后端或第三方服务的请求都能成功,不妨试试 LogRocket。

替代文字

LogRocket 会对您的应用进行检测,记录页面加载时间、首字节到达时间、慢网络请求等基准性能指标,并记录 Redux、NgRx 和 Vuex 的操作/状态。立即免费开始监控。


文章《如何使用Node构建网络爬虫》最初发表于LogRocket博客

文章来源:https://dev.to/bnevilleoneill/how-to-build-a-web-crawler-with-node-3hn2