Cheerio 网页抓取简介

网络数据抓取是一种从特定网站收集数据的技术手段。网站的内容通常使用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 抓取的数据,你可以创建各种可视化效果,例如图表、图形和词云,从而以更易于理解的方式呈现信息和趋势。

你还可以抓取用户个人资料,根据点赞和评论等指标分析用户在平台上的声誉。