如何使用Node构建Web搜寻器
由JordanIraborrab️撰写
介绍
Web爬虫通常被简化为爬虫,有时也称为蜘蛛机器人,它是一种通常会出于Web索引目的而系统地浏览Internet的机器人。搜索引擎可以使用这些互联网机器人来提高用户搜索结果的质量。除了索引万维网之外,爬网还可以用于收集数据(称为网络抓取)。
取决于网站的结构和提取的数据的复杂性,网络抓取过程在CPU上的工作量非常大。为了优化和加快此过程,我们将使用节点工作程序(线程),它们对于CPU密集型操作非常有用。
在本文中,我们将学习如何构建一个Web爬网程序,该爬网程序可以抓取网站并将数据存储在数据库中。此搜寻器漫游器将使用Node worker执行两项操作。
先决条件
- Node.js的基础知识
-
纱线或NPM(我们将使用纱线)
- 配置为运行节点代码的系统(最好是版本10.5.0或更高版本)
安装
启动终端并为本教程创建一个新目录:
$ mkdir worker-tutorial
$ cd worker-tutorial
通过运行以下命令初始化目录:
$ yarn init -y
我们需要以下软件包来构建搜寻器:
-
Axios-针对浏览器和Node.js的基于HTTP的承诺客户端
-
Cheerio — jQuery的轻量级实现,使我们可以访问服务器上的DOM
-
Firebase数据库-云托管的NoSQL数据库。如果您不熟悉设置Firebase数据库,请查看文档并按照步骤1-3进行操作
让我们使用以下命令安装上面列出的软件包:
$ yarn add axios cheerio firebase-admin
你好工人
在开始使用工人构建爬虫之前,让我们先了解一些基础知识。您可以创建一个测试文件 hello.js
在项目的根目录中运行以下代码段。
注册工人
可以通过从计算机导入工作程序类来初始化(注册)工作程序。 worker_threads
像这样的模块:
// hello.js
const { Worker } = require('worker_threads');
new Worker("./worker.js");
你好,世界
打印出来 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'
}
该代码段引入了工人阶级和 isMainThread
来自的对象 worker_threads
模块:
-
isMainThread
帮助我们知道何时在主线程或工作线程中运行 -
new Worker(__filename)
向…注册新员工__filename
在这种情况下是hello.js
与工人沟通
产生新的工作程序(线程)时,将有一个消息传递端口允许线程间通信。下面的代码片段显示了如何在工作线程(线程)之间传递消息:
// 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!");
});
}
在上面的代码段中,我们使用以下命令向父线程发送消息 parentPort.postMessage()
在初始化工作线程之后。然后我们使用以下方法侦听来自父线程的消息 parentPort.once()
。我们还使用以下方式向工作线程发送消息 worker.postMessage()
并使用以下方法侦听来自工作线程的消息 worker.once()
。
运行代码将产生以下输出:
Main Thread: Hi!
Worker thread: Hello!
构建爬虫
让我们构建一个基本的Web爬网程序,该爬网程序使用Node worker爬网并写入数据库。搜寻器将按以下顺序完成其任务:
- 从网站获取(请求)HTML
- 从响应中提取HTML
- 遍历DOM并提取包含汇率的表
- 格式化表格元素(
tbody
,tr
和td
)并提取汇率值 - 将汇率值存储在一个对象中,并将其发送给使用
worker.postMessage()
- 接受来自工作线程中父线程的消息,使用
parentPort.on()
- 将消息存储在Firestore(firebase数据库)中
让我们在项目目录中创建两个新文件:
-
main.js
–用于主线程 -
dbWorker.js
–用于工作线程
本教程的源代码可在GitHub上找到。随意克隆,派生或提交问题。
主线程(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;
}
使用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
}
在上面的代码段中,我们要做的不仅仅是数据格式化;之后 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))
});
注意:要在Firebase上设置数据库,请访问Firebase文档并按照步骤1-3开始。
跑步 main.js
(包括 dbWorker.js
)与Node一起将给出以下输出:
现在,您可以检查您的Firebase数据库,并将看到以下已爬网数据:
最后的笔记
尽管网络爬网很有趣,但是如果您使用数据来侵犯版权,也可能违法。通常建议您阅读要爬网的站点的条款和条件,以事先了解其数据爬网策略。您可以在此页面的“抓取政策”部分中了解更多信息。
使用辅助线程并不能保证您的应用程序会更快,但是如果有效地使用它,就可以显示出这种幻影,因为它可以通过减少CPU密集型任务在主线程上的繁琐工作来释放主线程。
结论
在本教程中,我们学习了如何构建可抓取货币汇率并将其保存到数据库的网络搜寻器。我们还学习了如何使用辅助线程来运行这些操作。
GitHub上提供了以下每个片段的源代码。随意克隆,派生或提交问题。
进一步阅读
有兴趣了解更多有关工作者线程的信息吗?您可以查看以下链接:
- 工作线程
- Node.js多线程:什么是工作线程,为什么如此重要?
- 使用Node.js实现多线程
- Node.js工作线程中的简单双向消息传递
仅限200:监视生产中失败和缓慢的网络请求
部署基于节点的Web应用程序或网站很容易。确保您的Node实例继续为您的应用程序提供资源是一件很困难的事情。如果您希望确保成功完成对后端或第三方服务的请求,请尝试LogRocket。
LogRocket使用您的应用程序来记录基线性能计时,例如页面加载时间,到第一个字节的时间,缓慢的网络请求,并记录Redux,NgRx和Vuex操作/状态。免费开始监视。
LogRocket Blog上首先出现了如何使用Node构建网络爬虫。