服务端5月29日 00:52
Cheerio 性能怎么优化?大文件和高并发场景怎么处理?Cheerio 性能优化抓住三个方向:选择器、内存、并发。选择器方面:用 `.find()` 配合具体 class 替代深层后代选择器,缓存 `$container` 后链式调用避免重复查询。内存方面:大文件用 stream 分块解析代替一次 load,批量 DOM 操作先拼字符串再一次性 `.html()` 插入,用完的 `$` 引用及时置空触发 GC。并发方面:多 URL 用 `Promise.all` 并行请求 + 逐个解析,超大数据集用 Worker 线程分片处理。load 选项中 `decodeEntities: false` 和 `withDomLvl1: false` 也能减少不必要的解析开销。
## 追问
**为什么 .find() 比层级选择器快?**
`$('.container .item .title')` 每次都从根节点全量匹配三层;`$('.container').find('.item').find('.title')` 先锁定容器再在子集中查找,搜索范围逐层缩小。差距在元素数量大时(万级以上)才明显。
**大文件怎么避免内存溢出?**
不要 `cheerio.load(wholeFile)`,改用 stream 按 `</item>` 等边界标签分割,每块单独 load 解析后立即释放。内存占用从 O(n) 降到 O(chunk),10MB 文件也不会爆。
**批量插入 DOM 为什么不能逐个 append?**
每次 `.append()` 都触发内部 DOM 树重建,1000 次就是 1000 次重建。正确做法是先用数组拼 HTML 字符串,最后 `.html(str)` 一次性写入,从 O(n) 次操作降到 1 次。
**load 选项哪些影响性能?**
`decodeEntities: false` 跳过 HTML 实体解码(不需要中文转义时关闭);`withDomLvl1: false` 跳过 DOM Level 1 兼容处理;`normalizeWhitespace: false` 跳过空白合并。三个都关掉可提速 15-20%。
**多 URL 并发爬取怎么做?**
用 p-limit 或手动分批 `Promise.all`:`for (let i = 0; i < urls.length; i += 5)` 每批 5 个并发,避免同时发起数百请求被限流或打挂目标服务器。
## 写段代码
```javascript
// 流式解析大文件,内存恒定
const results = [];
let buf = '';
fs.createReadStream('big.html')
.on('data', chunk => {
buf += chunk;
const matches = buf.match(/<item[\s\S]*?<\/item>/g) || [];
matches.forEach(m => {
const $ = cheerio.load(m);
results.push($('name').text());
});
buf = buf.slice(buf.lastIndexOf('</item>') + 7);
});
```标签
Cheerio
Cheerio是一个基于Node.js的快速、灵活、功能强大的HTML解析器和DOM操作库,类似于jQuery,但主要用于服务器端的Web应用程序。Cheerio可以像jQuery一样使用CSS选择器、DOM遍历、事件处理等功能,可以方便地从HTML文档中提取数据、修改内容、操纵DOM等。Cheerio的核心代码非常小,只有几百行代码,因此它非常快速、轻量级、易于使用。Cheerio还支持多种插件和扩展,如cheerio-tableparser、cheerio-eq等,可以扩展其功能以满足各种需求。由于Cheerio的性能和易用性,它已经成为Node.js中最受欢迎的HTML解析和DOM操纵库之一,并被广泛用于Web爬虫、数据挖掘、数据抓取等应用程序的开发。

服务端5月29日 00:51
如何开发 Cheerio 插件?有哪些实用插件模式?Cheerio 插件的核心模式是扩展 `cheerio.prototype`(即 `$.fn`),给选择器结果集添加自定义方法。基本写法:`module.exports = function(cheerio) { cheerio.prototype.myMethod = function() { return this; } }`,然后 `cheerio.use(pluginFn)` 加载。插件方法内部通过 `this` 访问当前选中的元素集合,用 `cheerio(el)` 包装后即可调用所有原生方法。返回 `this` 支持链式调用,返回 `.get()` 则输出普通数组。
## 追问
**插件方法怎么访问当前选中的元素?**
方法内 `this` 就是 cheerio 对象,`this.each((i, el) => ...)` 遍历,`cheerio(el)` 包装单个元素。`this.length` 获取匹配数量,`this[i]` 直接取原生节点。
**怎么让插件支持配置参数?**
导出一个工厂函数而非直接函数:`module.exports = (options) => (cheerio) => { ... }`,调用时 `cheerio.use(myPlugin({ trim: true }))`。内部用默认参数合并:`const opts = { trim: true, ...options }`。
**有哪些实用的插件场景?**
最常见三类:①文本清洗——批量 removeTags/cleanText,去掉 script/style/注释;②结构化提取——tableToArray 把表格转二维数组,tableToObjects 按表头生成对象数组;③URL 标准化——resolveUrls 把相对路径转绝对路径,处理懒加载 data-src。
**插件方法和独立工具函数怎么选?**
需要操作 DOM 节点、支持链式调用、依赖当前选择器上下文时用插件;纯数据转换、不涉及 DOM 操作时用普通函数。插件的优势是和 cheerio API 风格统一,缺点是污染原型。
**开发插件要注意什么?**
方法名加业务前缀避免冲突(如 `seo_extractLinks` 而非 `extractLinks`);返回 `this` 保持链式;用 try-catch 包裹核心逻辑防止异常中断;在 peerDependencies 声明 cheerio 版本。
## 写段代码
```javascript
// 表格转对象数组插件
module.exports = function(cheerio) {
cheerio.prototype.tableToObjects = function() {
const headers = this.find('th').map((i, el) => cheerio(el).text().trim()).get();
return this.find('tbody tr').map((i, tr) => {
const obj = {};
cheerio(tr).find('td').each((j, td) => {
obj[headers[j]] = cheerio(td).text().trim();
});
return obj;
}).get();
};
};
```服务端5月29日 00:51
Cheerio 爬取动态页面为什么拿不到数据?怎么解决?Cheerio 只是 HTML 解析器,不执行 JavaScript,所以通过 JS 动态渲染的内容(SPA、AJAX 加载、无限滚动)用 Cheerio 直接解析会拿到空壳 HTML。解决方案有三条路径:1) 用 Puppeteer/Playwright 先渲染页面拿到完整 HTML 再交给 Cheerio 解析;2) 拦截网络请求直接找到数据 API 端点,用 axios 请求 JSON;3) 分析页面源码中的内联数据(如 window.__INITIAL_STATE__)直接提取。
## 追问
**方案 2(直接请求 API)相比方案 1 有什么优势?**
速度快、资源消耗低、无需启动浏览器。缺点是 API 可能有签名/鉴权,需要逆向分析请求参数。
**Puppeteer 和 Playwright 选哪个?**
Playwright API 更现代,原生支持多浏览器,自动等待机制更智能;Puppeteer 生态更成熟、Chrome 专项优化更好。新项目推荐 Playwright。
**如何优化 Puppeteer + Cheerio 方案的性能?**
拦截非必要资源(图片/字体/CSS)不加载,复用浏览器实例而非每次新建,设置合理的 waitForSelector 超时而非固定等待。
**怎么判断页面是动态渲染还是静态 HTML?**
curl 获取 HTML 后检查目标元素是否存在,如果 curl 拿不到但浏览器能看到,就是动态渲染。
## 写段代码
```javascript
const puppeteer = require('puppeteer');
const cheerio = require('cheerio');
async function scrape(url) {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const $ = cheerio.load(await page.content());
await browser.close();
return $('.item').map((i, el) => $(el).text()).get();
}
```服务端5月29日 00:51
Cheerio 有哪些 DOM 操作方法?修改和读取分别怎么用?Cheerio 的 DOM 操作分两大类:读取和修改。读取用 `.text()` `.html()` `.attr()` `.val()` 获取内容;修改用 `.append()` `.prepend()` `.after()` `.before()` 插入节点,`.remove()` `.empty()` 删除,`.replaceWith()` 替换。关键是区分内部插入(append/prepend,插入子节点)和外部插入(after/before,插入兄弟节点)。遍历用 `.find()` `.children()` `.parent()` `.closest()` `.siblings()` 在节点树中移动。
## 追问
**append 和 after 有什么区别?**
`.append()` 是内部插入,把新节点加为目标元素的最后一个子节点;`.after()` 是外部插入,把新节点放在目标元素后面作为兄弟节点。记忆方式:append 是往肚子里塞,after 是站在旁边。
**remove 和 empty 有什么区别?**
`.remove()` 连元素本身一起删除;`.empty()` 只清空内容,元素标签保留。爬虫场景中 `.empty()` 常用于清空某个容器再重新填入清洗后的数据。
**怎么批量提取数据并转成数组?**
用 `.map()` + `.get()` 组合:`$('.item').map((i, el) => $(el).text()).get()`。`.map()` 返回 cheerio 对象,`.get()` 转为普通数组,否则无法用 Array 方法。
**.find() 和 .children() 有什么区别?**
`.find()` 搜索所有后代元素(含孙子节点),`.children()` 只找直接子元素。性能上 `.children()` 更快,层级确定时优先使用。
**怎么克隆一个节点并插入到其他位置?**
`.clone()` 深拷贝节点,返回独立的 cheerio 对象,可链式调用 `.appendTo()` 或 `.insertAfter()` 插入。注意 clone 不会复制 data 和事件绑定。
## 写段代码
```javascript
const $ = cheerio.load(html);
// 读取:提取所有文章标题和链接
const articles = $('.post').map((i, el) => ({
title: $(el).find('h2').text(),
link: $(el).find('a').attr('href')
})).get();
// 修改:给外链加 target 并移除广告
$('a[href^="http"]').attr('target', '_blank');
$('.ad').remove();
```服务端5月29日 00:50
Cheerio 中如何高效使用选择器提取数据?Cheerio 支持 CSS 选择器 + jQuery 扩展伪类,核心选择器分四类:基本(元素/ID/类)、层级(后代/子元素/兄弟)、属性(= ^= $= *=)、伪类(:first/:eq(n)/:contains()/:has())。高效用法的关键是缩小搜索范围:先选父容器再 .find(),缓存 $ 变量避免重复查找,用具体选择器替代通配符。
## 追问
**.find() 和层级选择器 $('div p') 哪个更快?**
性能差异极小,但 .find() 可读性更好且支持链式调用,推荐优先使用。已缓存的父元素上 .find() 还能跳过全局搜索。
**如何获取元素的文本、属性和 HTML?**
.text() 取文本(含子元素文本拼接),.attr('href') 取属性值,.html() 取内部 HTML,.val() 取表单值。注意 .text() 会递归合并所有子节点文本。
**.each() 和 .map() 有什么区别?**
.each() 遍历不返回值(适合副作用操作),.map() 返回数组(适合数据提取)。.map() 需链式调用 .get() 才能得到原生数组。
**:contains() 和 .filter() 如何选择?**
:contains('text') 在选择器阶段过滤,写法简洁但不支持正则;.filter(fn) 在回调中可写任意判断逻辑,更灵活。
## 写段代码
```javascript
const $ = cheerio.load(html);
const $list = $('#article-list');
const titles = $list.find('.title').map((i, el) =>
$(el).text().trim()
).get();
```服务端5月29日 00:50
Cheerio 使用中有哪些常见坑?怎么解决?最常见的五大坑:①中文乱码——需要用 iconv-lite 按实际编码解码,别依赖 axios 自动处理;②选择器找不到元素——先 `$.html()` 检查 HTML 是否完整加载,再逐步缩小选择器范围调试;③拿不到动态内容——Cheerio 不执行 JS,要么换 Puppeteer 要么直接调后端 API;④大文件内存溢出——分批处理或流式解析,别一次全 load 进来;⑤XML 解析报错——必须加 `{ xmlMode: true }` 选项,否则自闭合标签和命名空间都会出问题。
## 追问
**中文乱码具体怎么处理?**
axios 设置 `responseType: 'arraybuffer'` 拿到原始字节,先从 Content-Type 或 meta 标签检测编码,再用 `iconv.decode(buf, charset)` 转码。GBK 页面不转码必乱。
**选择器没匹配到元素,排查步骤是什么?**
第一步 `console.log($.html())` 看 HTML 是否完整;第二步从最外层选择器开始逐步缩小;第三步用 `:contains()` 按文本内容定位;第四步确认元素不在 iframe 或 shadow DOM 中。
**处理大文件怎么避免内存泄漏?**
分批 `.slice(i, i+batchSize)` 处理,每批处理完置空引用;更优方案是用 stream 按 `</item>` 分割,逐块 `cheerio.load` 解析,内存恒定。
**XML 模式和 HTML 模式有什么区别?**
`xmlMode: true` 会保留大小写(HTML 模式全部转小写)、保留自闭合标签、不补全缺失的闭合标签。解析 RSS/SVG/配置文件必须开启,否则数据丢失。
**提取的文本满是空白字符怎么办?**
`.text()` 结果链式调用 `.replace(/\s+/g, ' ').trim()`,或在 load 时加 `{ normalizeWhitespace: true }`,但后者会把换行也合并成空格,按需选择。
## 写段代码
```javascript
// 正确处理 GBK 编码页面
const resp = await axios.get(url, { responseType: 'arraybuffer' });
const charset = resp.headers['content-type']?.match(/charset=([^;]+)/i)?.[1] || 'utf-8';
const html = charset.toLowerCase() === 'utf-8'
? resp.data.toString()
: iconv.decode(Buffer.from(resp.data), charset);
const $ = cheerio.load(html);
```服务端5月29日 00:50
Cheerio 加载 HTML 有哪几种方式?核心方法是 cheerio.load(html, options),支持三种输入源:HTML 字符串、fs.readFileSync 读取的文件内容、Buffer。第二个参数控制解析行为:xmlMode 用于 XML 文档,decodeEntities 控制是否解码 HTML 实体(如 & → &),withDomLvl1 决定是否遵循 DOM Level1 规范。对大文件可用流式读取拼接后再 load。
## 追问
**xmlMode 什么时候必须开启?**
解析 XML/RSS/Atom feed 时必须开启,否则标签大小写会被统一转小写,XML 的自闭合标签也会被错误处理。
**decodeEntities 设为 false 有什么用?**
保留原始 HTML 实体不解码,适合需要原样输出 HTML 的场景,如内容编辑器。默认 true 会把 & 转成 &。
**能直接加载 URL 吗?**
不能。Cheerio 是纯解析库,没有网络能力。需先用 axios/fetch 获取 HTML 字符串,再传入 load()。
**cheerio.load() 返回的 $ 是什么?**
返回一个函数,既可当选择器 $(selector) 使用,也挂载了 .html()、.xml() 等静态方法,本质是包装后的 DOM 操作入口。
**Buffer 输入和字符串输入有区别吗?**
功能一致,Buffer 会被内部自动转字符串。用 Buffer 可避免编码问题,尤其处理非 UTF-8 内容时更安全。
## 写段代码
```javascript
const cheerio = require('cheerio');
const $ = cheerio.load('<root><item>值</item></root>', {
xmlMode: true,
decodeEntities: false
});
console.log($('item').text()); // 值
```服务端5月29日 00:50
如何用 Cheerio 实现网页爬虫和数据抓取?核心流程三步走:用 axios/fetch 获取 HTML → cheerio.load() 加载 → 选择器提取数据。Cheerio 只负责解析,HTTP 请求需搭配其他库。关键要注意设置 User-Agent、处理相对 URL 转绝对路径、以及加入请求延迟避免被封禁。
## 追问
**如何处理分页爬取?**
循环拼接页码参数逐页请求,每页之间加 1-2 秒延迟,收集数据后统一存储。注意检测是否有下一页(如"下一页"按钮或总页数标识)来决定终止条件。
**爬取的数据如何清洗?**
用 .trim() 去空白,正则移除 HTML 标签和特殊字符,parseFloat/parseInt 转数字类型,过滤空值和无效条目。
**如何实现请求重试机制?**
封装 fetchWithRetry 函数,失败后指数退避等待重试(1s→2s→4s),超过最大次数抛出异常。
**并发爬取如何控制?**
用 p-limit 等并发控制库限制同时请求数,或手动实现队列,避免瞬间大量请求导致 IP 被封。
## 写段代码
```javascript
const axios = require('axios');
const cheerio = require('cheerio');
async function scrape(url) {
const { data } = await axios.get(url);
const $ = cheerio.load(data);
const items = [];
$('.item').each((i, el) => {
items.push({ title: $(el).find('h2').text().trim() });
});
return items;
}
```服务端5月29日 00:50
Cheerio 和 Puppeteer 有什么区别?爬虫场景怎么选?Cheerio 是纯 HTML 解析器,不执行 JavaScript,解析速度比 Puppeteer 快 100 倍以上,内存占用极低;Puppeteer 启动真实 Chromium,能执行 JS、处理动态渲染、模拟用户交互,但资源消耗大、速度慢。选择依据很简单:目标页面内容是否由 JS 动态生成——查看网页源码(Ctrl+U)能看到数据就用 Cheerio,看不到就用 Puppeteer。
## 追问
**Puppeteer 能不能用 Cheerio 解析页面?**
可以,这是常见的混合模式:Puppeteer 负责加载动态页面拿到渲染后的 HTML,再传给 Cheerio 做数据提取,兼顾动态能力与解析效率。
**怎么判断一个网站是否需要 Puppeteer?**
右键查看网页源码,搜索目标数据。如果源码中没有,说明是 JS 动态加载,需要 Puppeteer;源码中已有,直接 Cheerio 即可。也可以用 `curl` 请求看返回的原始 HTML。
**Cheerio 能否处理需要登录的页面?**
Cheerio 本身只做解析,登录请求通过 axios/fetch 带 cookie 发送即可,不需要 Puppeteer。只有登录过程涉及 JS 渲染或验证码交互时才需要浏览器。
**两者的资源消耗差异有多大?**
Cheerio 解析一个页面通常 <10ms、内存 <50MB;Puppeteer 启动浏览器就要 500ms+、内存 100-300MB。批量爬取时差异是数量级的,100 个页面 Cheerio 几秒搞定,Puppeteer 可能要几分钟。
## 写段代码
```javascript
// Puppeteer 拿动态页面 + Cheerio 解析
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2' });
const html = await page.content();
await browser.close();
const $ = cheerio.load(html);
$('.item').each((i, el) => {
console.log($(el).text());
});
```服务端5月29日 00:50
Cheerio 和 jQuery 有什么区别?Cheerio 是 Node.js 端的 HTML 解析库,实现了 jQuery 核心 API,但剥离了浏览器依赖——不渲染 DOM、不处理 CSS 布局、不执行 JavaScript,因此比 jQuery 轻量且解析速度快 8 倍以上。jQuery 运行在浏览器中操作真实 DOM,Cheerio 则在服务端解析 HTML 字符串构建虚拟 DOM 树,只做数据提取与结构操作。
## 追问
**Cheerio 为什么比 jsdom 快?**
jsdom 会构建完整的浏览器环境(CSSOM、布局计算等),Cheerio 仅构建 DOM 树,跳过了渲染管线,所以解析速度快一个数量级。
**Cheerio 能替代 jQuery 做前端开发吗?**
不能。Cheerio 没有 render 树,修改 DOM 后不会触发页面重绘,也无法绑定事件,它只适合服务端数据提取场景。
**Cheerio 的底层解析器是什么?**
默认使用 htmlparser2,也可切换到 parse5。htmlparser2 速度更快,parse5 则更严格遵循 HTML 规范。
**什么场景下必须用 jQuery 而非 Cheerio?**
需要事件监听、CSS 动画、视口计算、用户交互等浏览器原生能力时,必须用 jQuery。
## 写段代码
```javascript
const cheerio = require('cheerio');
const $ = cheerio.load('<ul><li>A</li><li>B</li></ul>');
$('li').each((i, el) => console.log($(el).text()));
```服务端5月27日 23:41
Cheerio 和 jsdom 有什么区别?如何选择使用?Cheerio 和 jsdom 是 Node.js 中处理 HTML 的两种常见方案,核心区别在于:Cheerio 是轻量解析器,只做 DOM 遍历和数据提取;jsdom 是完整浏览器环境模拟器,能执行 JavaScript 并提供 window、localStorage 等浏览器 API。选错工具会导致性能浪费或功能缺失,以下是关键对比。## 核心架构差异Cheerio 基于 htmlparser2 构建,实现了一套精简的 DOM 模型,仅保留节点遍历、属性读写和 CSS 选择器能力。它不解析 CSS、不执行脚本、不渲染页面,因此体积小、速度快。jsdom 基于 WHATWG DOM 标准实现,构造了完整的 `window` 对象,包括 `document`、`location`、`localStorage`、`fetch` 等 API。它内置 JavaScript 引擎,可通过 `runScripts: 'dangerously'` 执行页面脚本,还能加载外部资源。简单说:Cheerio 把 HTML 当字符串解析,jsdom 把 HTML 当浏览器渲染。## 功能与性能对比| 维度 | Cheerio | jsdom ||------|---------|-------|| 解析速度 | 快(约 5-10ms/万节点) | 慢(约 100-500ms/万节点) || 内存占用 | 低 | 高(约 8-10 倍) || CSS 选择器 | jQuery 风格,支持链式调用 | 标准 querySelector/querySelectorAll || JavaScript 执行 | 不支持 | 支持 || 浏览器 API | 无 | window/document/localStorage/fetch/Canvas || 事件系统 | 无 | 完整 DOM 事件冒泡机制 || HTML 容错性 | 高(htmlparser2 宽容解析) | 低(严格按标准解析) |Cheerio 的性能优势在批量处理时尤为明显。解析同一份万级节点的 HTML,Cheerio 通常比 jsdom 快 8 倍以上,内存占用低一个数量级。## 选择决策**选 Cheerio 的场景:**- 爬虫抓取静态页面数据(标题、链接、正文)- 批量处理 HTML 文档(清洗标签、提取字段)- 服务端模板渲染后的 HTML 后处理- Serverless 等资源受限环境**选 jsdom 的场景:**- 前端组件单元测试(模拟 DOM 环境)- 服务端渲染(SSR)需要执行客户端脚本- 处理依赖 JavaScript 动态渲染的页面- 需要浏览器 API 的 Node.js 代码(如 window.matchMedia)**一个实用判断标准:** 如果你只需要 `querySelector` + `textContent`,用 Cheerio;如果你需要 `window` 对象,用 jsdom。## 常见坑点**Cheerio 陷阱:** 静态抓取 SPA 页面会拿到空壳 HTML。此时需要搭配 Puppeteer 等无头浏览器先渲染,再用 Cheerio 解析结果,而非换用 jsdom——jsdom 执行 JS 的能力有限,对复杂 SPA 支持不完善。**jsdom 陷阱:** 默认不执行脚本(需手动开启 `runScripts`),且开启后存在安全风险,不要用 jsdom 执行不可信来源的 HTML。另外 jsdom 不支持 `requestAnimationFrame`、`IntersectionObserver` 等部分现代 API,Jest 等测试框架通常会补充 polyfill。## 代码示例Cheerio 快速提取数据:```javascriptconst cheerio = require('cheerio');const $ = cheerio.load(html);// jQuery 风格 APIconst title = $('h1').text();const links = $('a').map((i, el) => $(el).attr('href')).get();const cleaned = (() => { $('script, style').remove(); return $.html();})();```jsdom 模拟浏览器环境:```javascriptconst { JSDOM } = require('jsdom');const dom = new JSDOM(html, { runScripts: 'dangerously' });const document = dom.window.document;const title = document.querySelector('h1').textContent;// 访问浏览器 APIconst storage = dom.window.localStorage;const location = dom.window.location.href;```两者也可以组合使用:先用 Puppeteer 或 jsdom 获取 JS 执行后的 HTML,再用 Cheerio 高效提取数据。## 追问方向- **Cheerio 如何处理编码问题?** `cheerio.load(html, { decodeEntities: false })` 可避免中文乱码。- **jsdom 如何模拟用户交互?** 通过 `dom.window.dispatchEvent` 或 `fireEvent` 库触发事件。- **还有其他选择吗?** node-html-parser 更轻量,parse5 更标准,linkedom 性能介于 Cheerio 和 jsdom 之间。