网络数据抓取是一种从特定网站收集数据的技术手段。网站的内容通常使用HTML进行结构化描述。当HTML代码清晰且具有语义时,我们可以方便地从中提取有用的数据信息。
网络数据抓取工具常被用于获取、监控数据,以及追踪数据在未来的变化趋势。
使用 Cheerio 前应掌握的 jQuery 概念
jQuery 是目前最流行的 JavaScript 库之一,它简化了对文档对象模型 (DOM) 的操作、事件处理、动画效果等功能。Cheerio 是一个基于 jQuery 构建的网络抓取库,它复用了 jQuery 的语法和API,使得解析 HTML 或 XML 文档变得更加便捷。
在学习如何使用 Cheerio 之前,理解 jQuery 如何选择 HTML 元素至关重要。幸运的是,jQuery 支持大部分 CSS3 选择器,这使得从 DOM 中定位元素变得更加简单。例如以下代码:
$("#container");
这段代码中,jQuery 选中了id为“container”的元素。如果使用原生 JavaScript 实现,代码如下所示:
document.querySelectorAll("#container");
对比以上两段代码,可以看到第一段代码更易读。这就是 jQuery 的优势所在。
jQuery 还提供了一些实用的方法,如 `text()` 和 `html()`,可以用来操作 HTML 元素。此外,你还可以使用 `parent()`、`siblings()`、`prev()` 和 `next()` 等方法来遍历 DOM。
jQuery 中的 `each()` 方法在许多 Cheerio 项目中非常常用。它可以用来迭代对象和数组。 `each()` 方法的语法如下:
$(<element>).each(<array or object>, callback)
这段代码表示,回调函数将会在数组或对象的每次迭代中被执行。
使用 Cheerio 加载 HTML
要使用 Cheerio 解析 HTML 或 XML 数据,你需要使用 `Cheerio.load()` 方法。以下是一个示例:
const $ = cheerio.load('<html><body><h1>你好,世界!</h1></body></html>');
console.log($('h1').text())
这段代码使用 jQuery 的 `text()` 方法来获取 `h1` 元素的文本内容。 `load()` 方法的完整语法如下:
load(content, options, mode)
`content` 参数是指传递给 `load()` 方法的 HTML 或 XML 数据。 `options` 是一个可选对象,用于修改方法的行为。默认情况下,如果缺少 `html`、`head` 和 `body` 元素,`load()` 方法会添加它们。如果你想禁止此行为,需要将 `mode` 设置为 `false`。
使用 Cheerio 抓取 Hacker News
本示例项目的代码可以在 GitHub 仓库中找到,并且在 MIT 许可证下免费使用。
现在是将你所学的知识付诸实践,创建一个简单的网络抓取工具的时候了。Hacker News 是一个深受企业家和创新者欢迎的网站。它也是一个非常适合进行网络抓取练习的网站,因为它加载速度快,界面简洁,并且没有广告。
请确保你的计算机上已安装 Node.js 和 Node Package Manager (npm)。创建一个空文件夹,并创建一个 `package.json` 文件,内容如下:
{
"name": "web-scraper",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon index.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"cheerio": "^1.0.0-rc.12",
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.1"
}
}
创建完成后,打开终端并运行:
npm i
此命令会安装构建抓取工具所需的依赖包。这些包包括用于解析 HTML 的 Cheerio,用于创建服务器的 ExpressJS,以及用于监听项目更改并自动重启服务器的开发依赖项 Nodemon。
配置并创建必要的功能
创建一个 `index.js` 文件,在该文件中创建一个名为 `PORT` 的常量变量,并将其设置为 5500(或者你选择的任何端口号)。然后,分别导入 Cheerio 和 Express 包。
const PORT = 5500;
const cheerio = require("cheerio");
const express = require("express");
const app = express();
接下来,定义三个变量: `url`、`html` 和 `finishedPage`。 将 `url` 设置为 Hacker News 的网址。
const url="https://news.ycombinator.com";
let html;
let finishedPage;
现在创建一个名为 `getHeader()` 的函数,该函数返回浏览器应呈现的一些 HTML 内容。
function getHeader(){
return `
<div style="display:flex; flex-direction:column; align-items:center;">
<h1 style="text-transform:capitalize">抓取新闻</h1>
<div style="display:flex; gap:10px; align-items:center;">
<a href="https://www.makeuseof.com/" id="news" onClick='showLoading()'>首页</a>
<a href="https://wilku.top/best" id="best" onClick='showLoading()'>最佳</a>
<a href="https://wilku.top/newest" id="newest" onClick='showLoading()'>最新</a>
<a href="https://wilku.top/ask" id="ask" onClick='showLoading()'>提问</a>
<a href="https://wilku.top/jobs" id="jobs" onClick='showLoading()'>招聘</a>
</div>
<p class="loading" style="display:none;">加载中...</p>
</div>
`}
创建另一个函数 `getScript()`,它返回一些 JavaScript 代码供浏览器执行。确保在调用时将变量类型作为参数传递。
function getScript(type){
return `
<script>
document.title = "${type.substring(1)}"window.addEventListener("DOMContentLoaded", (e) => {
let navLinks = [...document.querySelectorAll("a")];
let current = document.querySelector("#${type.substring(1)}");
document.body.style = "margin:0 auto; max-width:600px;";
navLinks.forEach(x => x.style = "color:black; text-decoration:none;");
current.style.textDecoration = "underline";
current.style.color = "black";
current.style.padding = "3px";
current.style.pointerEvents = "none";
})function showLoading(e){
document.querySelector(".loading").style.display = "block";
document.querySelector(".loading").style.textAlign = "center";
}
</script>`
}
最后,创建一个名为 `fetchAndRenderPage()` 的异步函数。该函数的功能正如你所想,它会从 Hacker News 中抓取页面,使用 Cheerio 解析并格式化内容,然后将 HTML 返回到客户端进行渲染。
async function fetchAndRenderPage(type, res) {
const response = await fetch(`${url}${type}`)
html = await response.text();
}
在 Hacker News 上,有不同类型的帖子。例如,首页显示的是“新闻”,带有 “提问” 标签的帖子是向 Hacker News 用户寻求解答的问题。热门帖子带有 “最佳” 标签,最新帖子带有 “最新” 标签,而招聘信息带有 “职位” 标签。
`fetchAndRenderPage()` 函数会根据作为参数传入的类型从 Hacker News 页面获取帖子列表。如果获取成功,该函数会将 HTML 响应文本绑定到 `html` 变量。
接下来,向函数中添加以下代码:
res.set('Content-Type', 'text/html');
res.write(getHeader());const $ = cheerio.load(html);
const articles = [];
let i = 1;
上述代码块中, `set()` 方法设置 HTTP 头部字段,`write()` 方法负责发送响应体的一部分, `load()` 函数接收 html 作为参数。
接下来,添加以下代码,选择类名为 `titleline` 的所有元素的子元素。
$('.titleline').children('a').each(function(){
let title = $(this).text();
articles.push(`<h4>${i}. ${title}</h4>`);
i++;
})
此代码块中,每次迭代都会获取目标 HTML 元素的文本内容并将其存储在 `title` 变量中。
接下来,将 `getScript()` 函数的响应推送到 `articles` 数组中。然后创建一个 `finishedPage` 变量,它将保存要发送到浏览器的完整 HTML 代码。最后,使用 `write()` 方法发送 `finishedPage` 代码块,并使用 `end()` 方法结束响应过程。
articles.push(getScript(type))
finishedPage = articles.reduce((c, n) => c + n);
res.write(finishedPage);
res.end();
定义处理 GET 请求的路由
在 `fetchAndRenderPage` 函数下方,使用 Express 的 `get()` 方法为不同类型的帖子定义各自的路由。然后使用 `listen` 方法监听本地网络上指定端口的连接。
app.get("https://www.makeuseof.com/", (req, res) => {
fetchAndRenderPage('/news', res);
})app.get("https://wilku.top/best", (req, res) => {
fetchAndRenderPage("https://wilku.top/best", res);
})app.get("https://wilku.top/newest", (req, res) => {
fetchAndRenderPage("https://wilku.top/newest", res);
})app.get("https://wilku.top/ask", (req, res) => {
fetchAndRenderPage("https://wilku.top/ask", res);
})app.get("https://wilku.top/jobs", (req, res) => {
fetchAndRenderPage("https://wilku.top/jobs", res);
})app.listen(PORT)
在以上代码块中,每个 `get` 方法都带有一个回调函数,该函数调用 `fetchAndRenderPage` 函数,并传入对应的类型和 `res` 对象。
当你在终端中运行 `npm run start` 时,服务器应该启动,然后你可以在浏览器中访问 `localhost:5500` 查看结果。
恭喜你!你已经成功抓取了 Hacker News 并获取了帖子标题,而无需使用任何外部 API。
网络抓取的进一步应用
利用从 Hacker News 抓取的数据,你可以创建各种可视化效果,例如图表、图形和词云,从而以更易于理解的方式呈现信息和趋势。
你还可以抓取用户个人资料,根据点赞和评论等指标分析用户在平台上的声誉。