那些需要手动收集和处理数据来启动项目的日子早已一去不复返了。无论是电子商务网站还是潜在客户生成算法,有一点是肯定的:数据收集过程既繁琐又耗时。
在本文中,您将了解 Cheerio 如何凭借其强大的标记语言解析功能为您提供帮助,我们将先通过一些简单的示例,再结合实际应用案例进行说明。

那些需要手动收集和处理数据来启动项目的日子早已一去不复返了。无论是电子商务网站还是潜在客户生成算法,有一点是肯定的:数据收集过程既繁琐又耗时。
在本文中,您将了解 Cheerio 如何凭借其强大的标记语言解析功能为您提供帮助,我们将先通过一些简单的示例,再结合实际应用案例进行说明。
“Cheerio究竟是什么?”您可能会这样问自己。为了澄清一个常见的误解,我先从说明Cheerio“不是什么”开始:它不是浏览器。
这种混淆可能源于 Cheerio 解析标记语言文档,并提供 API 来帮助您操作生成的数据结构这一事实。但与浏览器不同,Cheerio 不会可视化渲染文档、加载 CSS 文件或执行 JavaScript。
简而言之,Cheerio 的作用是接收 HTML 或 XML 输入,解析字符串并返回 API。这使得它运行极其快速且易于使用,因此在 Node.js 开发者中广受欢迎。
现在,让我们通过一些实例来了解 Cheerio 的功能。首先,你需要确保环境已准备就绪。
毋庸置疑,您的机器上必须已安装 Node.js。若尚未安装,只需根据您的操作系统,按照其官方网站的说明进行操作即可。
请务必下载长期支持版本(LTS),并记得安装 Node.js 包管理器(NPM)。你可以运行以下命令来确认安装是否成功:
node -vnpm -v
输出结果应如下所示:
现在谈谈 IDE 的选择:在本教程中,我将使用 Visual Studio Code,因为它非常灵活且易于使用,但您也可以使用任何您喜欢的 IDE。
只需创建一个用于存放您的小型项目的文件夹,并打开终端。运行以下命令来设置 Node.js 项目:
npm init -y
这将生成一个默认版本的 package.json 文件,该文件可随时进行修改。
下一步:我将安装 TypeScript 以及 Node.js 的类型定义:
npm install typescript @types/node -save-dev
本教程选择 TypeScript,是因为它为 JavaScript 对象提供了可选的静态类型检查,这使得代码在类型错误方面更加稳健。
根据 CircleCI 近期关于最受欢迎编程语言的调查,正是这一优势使其在 JavaScript 社区中的受欢迎程度稳步提升。
要验证上一步命令是否安装正确,你可以运行:
npx tsc --version
现在,我将在项目目录根目录下创建 tsconfig.json 配置文件,用于定义编译器选项。若想更深入了解该文件及其属性,TypeScript 官方文档将为您提供详细指导。
如果不想查阅,只需复制并粘贴以下内容:
{
"compilerOptions": {
"module": "commonjs",
"esModuleInterop": true,
"target": "es6",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist"
},
"lib": ["es2015"]
}快完成了!现在你需要安装 Cheerio(这很明显):
npm install cheerio
最后但同样重要的是,创建 src 目录来存放代码文件。说到代码文件,请在 src 目录中创建并放置 index.ts 文件。
太棒了!现在你可以开始使用了。
目前,我将通过一个静态 HTML 文档来演示 Cheerio 的部分基本功能。只需将以下内容复制并粘贴到项目中的一个新 static.html 文件中:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Page Name - Static HTML Example</title>
</head>
<body>
<div class="page-heading">
<h1>Page Heading</h1>
</div>
<div class="page-container">
<div class="page-content">
<ul>
<li>
<a href="#">Item 1</a>
<p class="price">$100</p>
<p class="stock">12</p>
</li>
<li>
<a href="#">Item 2</a>
<p class="price">$200</p>
<p class="stock">422</p>
</li>
<li>
<a href="#">Item 3</a>
<p class="price">$150</p>
<p class="stock">5</p>
</li>
</ul>
</div>
</div>
<footer class="page-footer">
<p>Last Updated: Friday, September 23, 2022</p>
</footer>
</body>
</html>接下来,你需要将该 HTML 文件作为输入提供给 Cheerio,它将返回相应的 API:
import fs from 'fs'
import * as cheerio from 'cheerio'
const staticHTML = fs.readFileSync('static.html')
const $ = cheerio.load(staticHTML)如果在此步骤遇到错误,请确保输入文件包含有效的 HTML 文档,因为从 Cheerio 1.0.0 版本开始,系统也会验证这一条件。
现在您可以开始探索 Cheerio 的各项功能了。该 NPM 包以其类似 jQuery 的语法和使用 CSS 选择器来提取目标节点而闻名。您可以查阅其官方文档以获得更深入的了解。
假设您想提取页面标题:
const title = $('title').text()console.log("Static HTML page title:", title)
我们需要测试一下,对吧?既然您使用的是 TypeScript,就必须先编译代码,这会生成 dist 目录,然后执行其中的 index.js 文件。为简化起见,我将在 package.json 文件中定义以下脚本:
"scripts": {
"test": "npx tsc && node dist/index.js",
}这样,我只需运行:
npm run test
脚本就会自动处理我刚才提到的两个步骤。
好的,但如果选择器匹配了多个 HTML 元素呢?让我们尝试提取无序列表中项目名称和股价:
const itemStocks = {}
$('li').each((index, element) => {
const name = $(element).find('a').text()
const stock = $(element).find('p.stock').text()
itemStocks[name] = stock
})
console.log("All items stock:", itemStocks)现在再次运行该快捷脚本,终端输出应如下所示:
Static HTML page title: Page Name - Static HTML ExampleAll items stock: { 'Item 1': '12', 'Item 2': '422', 'Item 3': '5' }
这其实只是冰山一角。Cheerio 还能够解析 XML 文档、提取 HTML 元素的样式,甚至修改节点的属性。
但 Cheerio 在实际应用中能提供哪些帮助呢?
假设我们要收集一些数据,用于训练未来某个大型项目的机器学习模型。通常,你会通过 Google 搜索一些训练数据并下载,或者使用网站的 API。
但当你找不到相关文件,或者目标网站不提供 API、对数据有限流,或者无法获取页面上显示的全部数据时,该怎么办?
这时,网络爬虫就派上用场了。如果您想了解网络爬虫更多实际应用场景,可以阅读我们博客中这篇写得很好的文章。
言归正传,为了便于说明,假设我们正处于这种具体情境:需要数据,却无处可寻。请记住,Cheerio 并不处理 HTML 提取、CSS 加载或 JS 执行。
因此,在本教程中,我将使用 Puppeteer 访问网站,抓取 HTML 并将其保存到文件中。随后,我将重复上一节中的操作流程。
具体来说,我希望从 Reddit 上收集关于某款热门鼓机模块的公众意见,并将数据集中到一个文件中,以便后续输入到潜在的机器学习训练模型中。后续应用场景多种多样:情感分析、市场调研等等,不一而足。
让我们看看这个用例在代码中的实现。首先,你需要安装 Puppeteer NPM 包:
npm install puppeteer
为了使项目结构更清晰,我还将创建一个名为 reddit.ts 的新文件,并在 package.json 文件中定义一个新脚本:
"scripts": {
"test": "npx tsc && node dist/index.js",
"parse": "npx tsc && node dist/reddit.js"
},为了获取 HTML 文档,我将定义一个如下所示的函数:
import fs from 'fs'
import puppeteer from 'puppeteer'
import * as cheerio from 'cheerio'
async function getContent(url: string): Promise<void> {
// Open the browser and a new tab
const browser = await puppeteer.launch()
const page = await browser.newPage()
// Navigate to the URL and write the content to file
await page.goto(url)
const pageContent = await page.content()
fs.writeFileSync("reddit.html", pageContent)
// Close the browser
await browser.close()
console.log("Got the HTML. Check the reddit.html file.")
}为了快速测试,请在代码中添加一个入口点并调用该函数:
async function main() {
const targetURL = 'https://old.reddit.com/r/Drumming/comments/r3tidc/yamaha_ead10/'
await getContent(targetURL)
}
main()
.then(() => {console.log("All done!")})
.catch(e => {console.log("Unexpected error occurred:", e.message)})项目目录树中应会出现 reddit.html 文件,其中包含我们所需的 HTML 文档。
现在,进入稍具挑战性的部分:您需要识别出与我们的用例相关的节点。返回您的浏览器(真实的浏览器),访问目标 URL。将鼠标光标移至评论区,右键单击,然后选择“检查”选项。
开发者工具选项卡将打开,显示与你之前保存在本机上的完全相同的 HTML 文档。
若要仅提取评论内容,需识别出该页面区域特有的选择器。你会发现整个评论列表位于一个具有 sitetable nestedlisting 类的 div 容器内。
进一步分析,每个单独的评论都以一个 form 元素为父级,该元素带有 usertext 和 warn-on-unload 类。然后,在底部,你可以看到每个评论的文本被划分为多个 p 元素。
让我们看看代码中是如何实现的:
function parseComments(): void {
// Load the HTML document
const staticHTML = fs.readFileSync('reddit.html')
const $ = cheerio.load(staticHTML)
// Get the comments section
const commentsSection = $('div.sitetable.nestedlisting')
// Iterate each comment
const comments = []
$(commentsSection).find('form.usertext.warn-on-unload').each((index, comment) => {
let commentText = ""
// Iterate each comment section and concatenate them
$(comment).find('p').each((index, piece) => {
commentText += $(piece).text() + '\n'
})
comments.push(commentText)
})
// Write the results to external file
fs.writeFileSync("comments.json", JSON.stringify({comments}))
}好的,现在让我们用新定义的函数更新入口点,看看这段代码是如何协同工作的:
async function main() {
const targetURL = 'https://old.reddit.com/r/Drumming/comments/r3tidc/yamaha_ead10/'
await getContent(targetURL)
parseComments()
}
main()
.then(() => {console.log("All done. Check the comments.csv file.")})
.catch(e => {console.log("Unexpected error occurred:", e.message)})使用之前定义的脚本执行代码:
npm run parse
无头浏览器打开并导航至目标 URL 大约需要 5 到 10 秒。如果您对此更感兴趣,可以在每个函数的开头和结尾添加时间戳,以切实了解 Cheerio 的运行速度。
最终生成的 comments.json 文件应如下所示:
此用例可轻松扩展为解析每条评论的赞数和踩数,或获取评论的嵌套回复。可能性无穷无尽。
感谢您阅读本教程至此。希望您已了解 Cheerio 在数据提取过程中不可或缺的作用,以及如何将其快速集成到您的下一个爬取项目中。
我们在自家产品 WebScrapingAPI 中也采用了 Cheerio。如果您在网页抓取过程中遭遇诸多挑战(如 IP 封禁、机器人检测等),不妨尝试一下。

Raluca Penciuc 是 WebScrapingAPI 的全栈开发工程师,主要负责开发爬虫、优化规避机制,并探索可靠的方法以降低在目标网站上的被检测概率。