用node.js实现一个网页爬虫(nodejs做网站)
ccwgpt 2024-11-07 09:47 46 浏览 0 评论
本文讲解怎样用 Node.js 高效地从 Web 爬取数据。
前提条件
本文主要针对具有一定 JavaScript 经验的程序员。如果你对 Web 抓取有深刻的了解,但对 JavaScript 并不熟悉,那么本文仍然能够对你有所帮助。
- ? 会 JavaScript
- ? 会用 DevTools 提取元素选择器
- ? 会一些 ES6 (可选)
你将学到
通过本文你将学到:
- 学到更多关于 Node.js 的东西
- 用多个 HTTP 客户端来帮助 Web 抓取的过程
- 利用多个经过实践考验过的库来爬取 Web
了解 Node.js
Javascript 是一种简单的现代编程语言,最初是为了向浏览器中的网页添加动态效果。当加载网站后,Javascript 代码由浏览器的 Javascript 引擎运行。为了使 Javascript 与你的浏览器进行交互,浏览器还提供了运行时环境(document、window等)。
这意味着 Javascript 不能直接与计算机资源交互或对其进行操作。例如在 Web 服务器中,服务器必须能够与文件系统进行交互,这样才能读写文件。
Node.js 使 Javascript 不仅能够运行在客户端,而且还可以运行在服务器端。为了做到这一点,其创始人 Ryan Dahl 选择了Google Chrome 浏览器的 v8 Javascript Engine,并将其嵌入到用 C++ 开发的 Node 程序中。所以 Node.js 是一个运行时环境,它允许 Javascript 代码也能在服务器上运行。
与其他语言(例如 C 或 C++)通过多个线程来处理并发性相反,Node.js 利用单个主线程并并在事件循环的帮助下以非阻塞方式执行任务。
要创建一个简单的 Web 服务器非常简单,如下所示:
const http = require('http');
const PORT = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('Hello World');
});
server.listen(port, () => {
console.log(`Server running at PORT:${port}/`);
});
如果你已安装了 Node.js,可以试着运行上面的代码。Node.js 非常适合 I/O 密集型程序。
HTTP 客户端:访问 Web
HTTP 客户端是能够将请求发送到服务器,然后接收服务器响应的工具。下面提到的所有工具底的层都是用 HTTP 客户端来访问你要抓取的网站。
Request
Request 是 Javascript 生态中使用最广泛的 HTTP 客户端之一,但是 Request 库的作者已正式声明弃用了。不过这并不意味着它不可用了,相当多的库仍在使用它,并且非常好用。用 Request 发出 HTTP 请求是非常简单的:
const request = require('request')
request('https://www.reddit.com/r/programming.json', function ( error,
response,
body) {
console.error('error:', error)
console.log('body:', body)
})
你可以在 Github 上找到 Request 库,安装它非常简单。你还可以在 https://github.com/request/request/issues/3142 找到弃用通知及其含义。
Axios
Axios 是基于 promise 的 HTTP 客户端,可在浏览器和 Node.js 中运行。如果你用 Typescript,那么 axios 会为你覆盖内置类型。通过 Axios 发起 HTTP 请求非常简单,默认情况下它带有 Promise 支持,而不是在 Request 中去使用回调:
const axios = require('axios')
axios
.get('https://www.reddit.com/r/programming.json')
.then((response) => {
console.log(response)
})
.catch((error) => {
console.error(error)
});
如果你喜欢 Promises API 的 async/await 语法糖,那么你也可以用,但是由于顶级 await 仍处于 stage 3 ,所以我们只好先用异步函数来代替:
async function getForum() {
try {
const response = await axios.get(
'https://www.reddit.com/r/programming.json'
)
console.log(response)
} catch (error) {
console.error(error)
}
}
你所要做的就是调用 getForum!可以在 https://github.com/axios/axios 上找到Axios库。
Superagent
与 Axios 一样,Superagent 是另一个强大的 HTTP 客户端,它支持 Promise 和 async/await 语法糖。它具有像 Axios 这样相当简单的 API,但是 Superagent 由于存在更多的依赖关系并且不那么流行。
用 promise、async/await 或回调向 Superagent 发出HTTP请求看起来像这样:
const superagent = require("superagent")
const forumURL = "https://www.reddit.com/r/programming.json"
// callbacks
superagent
.get(forumURL)
.end((error, response) => {
console.log(response)
})
// promises
superagent
.get(forumURL)
.then((response) => {
console.log(response)
})
.catch((error) => {
console.error(error)
})
// promises with async/await
async function getForum() {
try {
const response = await superagent.get(forumURL)
console.log(response)
} catch (error) {
console.error(error)
}
}
可以在 https://github.com/visionmedia/superagent 找到 Superagent。
正则表达式:艰难的路
在没有任何依赖性的情况下,最简单的进行网络抓取的方法是,使用 HTTP 客户端查询网页时,在收到的 HTML 字符串上使用一堆正则表达式。正则表达式不那么灵活,而且很多专业人士和业余爱好者都难以编写正确的正则表达式。
让我们试一试,假设其中有一个带有用户名的标签,我们需要该用户名,这类似于你依赖正则表达式时必须执行的操作
const htmlString = '<label>Username: John Doe</label>'
const result = htmlString.match(/<label>(.+)<\/label>/)
console.log(result[1], result[1].split(": ")[1])
// Username: John Doe, John Doe
在 Javascript 中,match() 通常返回一个数组,该数组包含与正则表达式匹配的所有内容。第二个元素(在索引1中)将找到我们想要的 <label> 标记的 textContent 或 innerHTML。但是结果中包含一些不需要的文本( “Username: “),必须将其删除。
如你所见,对于一个非常简单的用例,步骤和要做的工作都很多。这就是为什么应该依赖 HTML 解析器的原因,我们将在后面讨论。
Cheerio:用于遍历 DOM 的核心 JQuery
Cheerio 是一个高效轻便的库,它使你可以在服务器端使用 JQuery 的丰富而强大的 API。如果你以前用过 JQuery,那么将会对 Cheerio 感到很熟悉,它消除了 DOM 所有不一致和与浏览器相关的功能,并公开了一种有效的 API 来解析和操作 DOM。
const cheerio = require('cheerio')
const nbsp;= cheerio.load('<h2 class="title">Hello world</h2>')
$('h2.title').text('Hello there!')
$('h2').addClass('welcome')
$.html()
// <h2 class="title welcome">Hello there!</h2>
如你所见,Cheerio 与 JQuery 用起来非常相似。
但是,尽管它的工作方式不同于网络浏览器,也就这意味着它不能:
- 渲染任何解析的或操纵 DOM 元素
- 应用 CSS 或加载外部资源
- 执行 JavaScript
因此,如果你尝试爬取的网站或 Web 应用是严重依赖 Javascript 的(例如“单页应用”),那么 Cheerio 并不是最佳选择,你可能不得不依赖稍后讨论的其他选项。
为了展示 Cheerio 的强大功能,我们将尝试在 Reddit 中抓取 r/programming 论坛,尝试获取帖子名称列表。
首先,通过运行以下命令来安装 Cheerio 和 axios:npm install cheerio axios。
然后创建一个名为 crawler.js 的新文件,并复制粘贴以下代码:
const axios = require('axios');
const cheerio = require('cheerio');
const getPostTitles = async () => {
try {
const { data } = await axios.get(
'https://old.reddit.com/r/programming/'
);
const nbsp;= cheerio.load(data);
const postTitles = [];
$('div > p.title > a').each((_idx, el) => {
const postTitle = $(el).text()
postTitles.push(postTitle)
});
return postTitles;
} catch (error) {
throw error;
}
};
getPostTitles()
.then((postTitles) => console.log(postTitles));
getPostTitles() 是一个异步函数,将对旧的 reddit 的 r/programming 论坛进行爬取。首先,用带有 axios HTTP 客户端库的简单 HTTP GET 请求获取网站的 HTML,然后用 cheerio.load() 函数将 html 数据输入到 Cheerio 中。
然后在浏览器的 Dev Tools 帮助下,可以获得可以定位所有列表项的选择器。如果你使用过 JQuery,则必须非常熟悉 $('div> p.title> a')。这将得到所有帖子,因为你只希望单独获取每个帖子的标题,所以必须遍历每个帖子,这些操作是在 each() 函数的帮助下完成的。
要从每个标题中提取文本,必须在 Cheerio 的帮助下获取 DOM元素( el 指代当前元素)。然后在每个元素上调用 text() 能够为你提供文本。
现在,打开终端并运行 node crawler.js,然后你将看到大约存有标题的数组,它会很长。尽管这是一个非常简单的用例,但它展示了 Cheerio 提供的 API 的简单性质。
如果你的用例需要执行 Javascript 并加载外部源,那么以下几个选项将很有帮助。
JSDOM:Node 的 DOM
JSDOM 是在 Node.js 中使用的文档对象模型的纯 Javascript 实现,如前所述,DOM 对 Node 不可用,但是 JSDOM 是最接近的。它或多或少地模仿了浏览器。
由于创建了 DOM,所以可以通过编程与要爬取的 Web 应用或网站进行交互,也可以模拟单击按钮。如果你熟悉 DOM 操作,那么使用 JSDOM 将会非常简单。
const { JSDOM } = require('jsdom')
const { document } = new JSDOM(
'<h2 class="title">Hello world</h2>'
).window
const heading = document.querySelector('.title')
heading.textContent = 'Hello there!'
heading.classList.add('welcome')
heading.innerHTML
// <h2 class="title welcome">Hello there!</h2>
代码中用 JSDOM 创建一个 DOM,然后你可以用和操纵浏览器 DOM 相同的方法和属性来操纵该 DOM。
为了演示如何用 JSDOM 与网站进行交互,我们将获得 Reddit r/programming 论坛的第一篇帖子并对其进行投票,然后验证该帖子是否已被投票。
首先运行以下命令来安装 jsdom 和 axios:npm install jsdom axios
然后创建名为 crawler.js的文件,并复制粘贴以下代码:
const { JSDOM } = require("jsdom")
const axios = require('axios')
const upvoteFirstPost = async () => {
try {
const { data } = await axios.get("https://old.reddit.com/r/programming/");
const dom = new JSDOM(data, {
runScripts: "dangerously",
resources: "usable"
});
const { document } = dom.window;
const firstPost = document.querySelector("div > div.midcol > div.arrow");
firstPost.click();
const isUpvoted = firstPost.classList.contains("upmod");
const msg = isUpvoted
? "Post has been upvoted successfully!"
: "The post has not been upvoted!";
return msg;
} catch (error) {
throw error;
}
};
upvoteFirstPost().then(msg => console.log(msg));
upvoteFirstPost() 是一个异步函数,它将在 r/programming 中获取第一个帖子,然后对其进行投票。axios 发送 HTTP GET 请求获取指定 URL 的HTML。然后通过先前获取的 HTML 来创建新的 DOM。JSDOM 构造函数把HTML 作为第一个参数,把 option 作为第二个参数,已添加的 2 个 option 项执行以下功能:
- runScripts:设置为 dangerously 时允许执行事件 handler 和任何 Javascript 代码。如果你不清楚将要运行的脚本的安全性,则最好将 runScripts 设置为“outside-only”,这会把所有提供的 Javascript 规范附加到 “window” 对象,从而阻止在 inside 上执行的任何脚本。
- resources:设置为“usable”时,允许加载用 <script> 标记声明的任何外部脚本(例如:从 CDN 提取的 JQuery 库)
创建 DOM 后,用相同的 DOM 方法得到第一篇文章的 upvote 按钮,然后单击。要验证是否确实单击了它,可以检查 classList 中是否有一个名为 upmod 的类。如果存在于 classList 中,则返回一条消息。
打开终端并运行 node crawler.js,然后会看到一个整洁的字符串,该字符串将表明帖子是否被赞过。尽管这个例子很简单,但你可以在这个基础上构建功能强大的东西,例如,一个围绕特定用户的帖子进行投票的机器人。
如果你不喜欢缺乏表达能力的 JSDOM ,并且实践中要依赖于许多此类操作,或者需要重新创建许多不同的 DOM,那么下面将是更好的选择。
Puppeteer:无头浏览器
顾名思义,Puppeteer 允许你以编程方式操纵浏览器,就像操纵木偶一样。它通过为开发人员提供高级 API 来默认控制无头版本的 Chrome。
Puppeteer 比上述工具更有用,因为它可以使你像真正的人在与浏览器进行交互一样对网络进行爬取。这就具备了一些以前没有的可能性:
- 你可以获取屏幕截图或生成页面 PDF。
- 可以抓取单页应用并生成预渲染的内容。
- 自动执行许多不同的用户交互,例如键盘输入、表单提交、导航等。
它还可以在 Web 爬取之外的其他任务中发挥重要作用,例如 UI 测试、辅助性能优化等。
通常你会想要截取网站的屏幕截图,也许是为了了解竞争对手的产品目录,可以用 puppeteer 来做到。首先运行以下命令安装 puppeteer,:npm install puppeteer
这将下载 Chromium 的 bundle 版本,根据操作系统的不同,该版本大约 180 MB 至 300 MB。如果你要禁用此功能。
让我们尝试在 Reddit 中获取 r/programming 论坛的屏幕截图和 PDF,创建一个名为 crawler.js的新文件,然后复制粘贴以下代码:
const puppeteer = require('puppeteer')
async function getVisual() {
try {
const URL = 'https://www.reddit.com/r/programming/'
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.goto(URL)
await page.screenshot({ path: 'screenshot.png' })
await page.pdf({ path: 'page.pdf' })
await browser.close()
} catch (error) {
console.error(error)
}
}
getVisual()
getVisual() 是一个异步函数,它将获 URL 变量中 url 对应的屏幕截图和 pdf。首先,通过 puppeteer.launch() 创建浏览器实例,然后创建一个新页面。可以将该页面视为常规浏览器中的选项卡。然后通过以 URL 为参数调用 page.goto() ,将先前创建的页面定向到指定的 URL。最终,浏览器实例与页面一起被销毁。
完成操作并完成页面加载后,将分别使用 page.screenshot() 和 page.pdf() 获取屏幕截图和 pdf。你也可以侦听 javascript load 事件,然后执行这些操作,在生产环境级别下强烈建议这样做。
在终端上运行 node crawler.js ,几秒钟后,你会注意到已经创建了两个文件,分别名为 screenshot.jpg 和 page.pdf。
Nightmare:Puppeteer 的替代者
Nightmare 是类似 Puppeteer 的高级浏览器自动化库,该库使用 Electron,但据说速度是其前身 PhantomJS 的两倍。
如果你在某种程度上不喜欢 Puppeteer 或对 Chromium 捆绑包的大小感到沮丧,那么 nightmare 是一个理想的选择。首先,运行以下命令安装 nightmare 库:npm install nightmare
然后,一旦下载了 nightmare,我们将用它通过 Google 搜索引擎找到 ScrapingBee 的网站。创建一个名为crawler.js的文件,然后将以下代码复制粘贴到其中:
const Nightmare = require('nightmare')
const nightmare = Nightmare()
nightmare
.goto('https://www.google.com/')
.type("input[title='Search']", 'ScrapingBee')
.click("input[value='Google Search']")
.wait('#rso > div:nth-child(1) > div > div > div.r > a')
.evaluate(
() =>
document.querySelector(
'#rso > div:nth-child(1) > div > div > div.r > a'
).href
)
.end()
.then((link) => {
console.log('Scraping Bee Web Link': link)
})
.catch((error) => {
console.error('Search failed:', error)
})
首先创建一个 Nighmare 实例,然后通过调用 goto() 将该实例定向到 Google 搜索引擎,加载后,使用其选择器获取搜索框,然后使用搜索框的值(输入标签)更改为“ScrapingBee”。完成后,通过单击 “Google搜索” 按钮提交搜索表单。然后告诉 Nightmare 等到第一个链接加载完毕,一旦完成,它将使用 DOM 方法来获取包含该链接的定位标记的 href 属性的值。
最后,完成所有操作后,链接将打印到控制台。
总结
- ? Node.js 是 Javascript 在服务器端的运行时环境。由于事件循环机制,它具有“非阻塞”性质。
- ? HTTP客户端(例如 Axios、Superagent 和 Request)用于将 HTTP 请求发送到服务器并接收响应。
- ? Cheerio 把 JQuery 的优点抽出来,在服务器端 进行 Web 爬取是唯一的目的,但不执行 Javascript 代码。
- ? JSDOM 根据标准 Javascript规范 从 HTML 字符串中创建一个 DOM,并允许你对其执行DOM操作。
- ? Puppeteer and Nightmare 是高级(high-level )浏览器自动化库,可让你以编程方式去操作 Web 应用,就像真实的人正在与之交互一样。
相关推荐
- 一个基于.Net Core遵循Clean Architecture原则开源架构
-
今天给大家推荐一个遵循CleanArchitecture原则开源架构。项目简介这是基于Asp.netCore6开发的,遵循CleanArchitecture原则,可以高效、快速地构建基于Ra...
- AI写代码翻车无数次,我发现只要提前做好这3步,bug立减80%
-
写十万行全是bug之后终于找到方法了开发"提示词管理助手"新版本那会儿,我差点被bug整崩溃。刚开始两周,全靠AI改代码架构,结果十万行程序漏洞百出。本来以为AI说没问题就稳了,结果...
- OneCode低代码平台的事件驱动设计:架构解析与实践
-
引言:低代码平台的事件驱动范式在现代软件开发中,事件驱动架构(EDA)已成为构建灵活、松耦合系统的核心范式。OneCode低代码平台通过创新性的注解驱动设计,将事件驱动理念深度融入平台架构,实现了业务...
- 国内大厂AI插件评测:根据UI图生成Vue前端代码
-
在IDEA中安装大厂的AI插件,打开ruoyi增强项目:yudao-ui-admin-vue31.CodeBuddy插件登录腾讯的CodeBuddy后,大模型选择deepseek-v3,输入提示语:...
- AI+低代码技术揭秘(二):核心架构
-
本文档介绍了为VTJ低代码平台提供支持的基本架构组件,包括Engine编排层、Provider服务系统、数据模型和代码生成管道。有关UI组件库和widget系统的信息,请参阅UI...
- GitDiagram用AI把代码库变成可视化架构图
-
这是一个名为gitdiagram的开源工具,可将GitHub仓库实时转换为交互式架构图,帮助开发者快速理解代码结构。核心功能一键可视化:替换GitHubURL中的"hub...
- 30天自制操作系统:第六天:代码架构整理与中断处理
-
1.拆开bootpack.c文件。根据设计模式将对应的功能封装成独立的文件。2.初始化pic:pic(可编程中断控制器):在设计上,cpu单独只能处理一个中断。而pic是将8个中断信号集合成一个中断...
- AI写代码越帮越忙?2025年研究揭露惊人真相
-
近年来,AI工具如雨后春笋般涌现,许多人开始幻想程序员的未来就是“对着AI说几句话”,就能轻松写出完美的代码。然而,2025年的一项最新研究却颠覆了这一期待,揭示了一个令人意外的结果。研究邀请了16位...
- 一键理解开源项目:两个自动生成GitHub代码架构图与说明书工具
-
一、GitDiagram可以一键生成github代码仓库的架构图如果想要可视化github开源项目:https://github.com/luler/reflex_ai_fast,也可以直接把域名替换...
- 5分钟掌握 c# 网络通讯架构及代码示例
-
以下是C#网络通讯架构的核心要点及代码示例,按协议类型分类整理:一、TCP协议(可靠连接)1.同步通信//服务器端usingSystem.Net.Sockets;usingTcpListene...
- 从复杂到优雅:用建造者和责任链重塑代码架构
-
引用设计模式是软件开发中的重要工具,它为解决常见问题提供了标准化的解决方案,提高了代码的可维护性和可扩展性,提升了开发效率,促进了团队协作,提高了软件质量,并帮助开发者更好地适应需求变化。通过学习和应...
- 低代码开发当道,我还需要学习LangChain这些框架吗?| IT杂谈
-
专注LLM深度应用,关注我不迷路前两天有位兄弟问了个问题:当然我很能理解这位朋友的担忧:期望效率最大化,时间用在刀刃上,“不要重新发明轮子”嘛。铺天盖地的AI信息轰炸与概念炒作,很容易让人浮躁与迷茫。...
- 框架设计并不是简单粗暴地写代码,而是要先弄清逻辑
-
3.框架设计3.框架设计本节我们要开发一个UI框架,底层以白鹭引擎为例。框架设计的第一步并不是直接撸代码,而是先想清楚设计思想,抽象。一个一个的UI窗口是独立的吗?不是的,...
- 大佬用 Avalonia 框架开发的 C# 代码 IDE
-
AvalonStudioAvalonStudio是一个开源的跨平台的开发编辑器(IDE),AvalonStudio的目标是成为一个功能齐全,并且可以让开发者快速使用的IDE,提高开发的生产力。A...
- 轻量级框架Lagent 仅需20行代码即可构建自己的智能代理
-
站长之家(ChinaZ.com)8月30日消息:Lagent是一个专注于基于LLM模型的代理开发的轻量级框架。它的设计旨在简化和提高这种模型下代理的开发效率。LLM模型是一种强大的工具,可以...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- 框架图 (58)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- shiro框架 (61)
- 定时任务框架 (56)
- java日志框架 (61)
- mfc框架 (52)
- abb框架断路器 (48)
- beego框架 (52)
- java框架spring (58)
- grpc框架 (65)
- tornado框架 (48)
- 前端框架bootstrap (54)
- orm框架有哪些 (51)
- 知识框架图 (52)
- ppt框架 (55)
- 框架图模板 (59)
- 内联框架 (52)
- cad怎么画框架 (58)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)