标签

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爬虫、数据挖掘、数据抓取等应用程序的开发。

Cheerio
服务端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); }); ```
服务端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 使用中有哪些常见坑?怎么解决?最常见的五大坑:①中文乱码——需要用 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 实体(如 &amp; → &),withDomLvl1 决定是否遵循 DOM Level1 规范。对大文件可用流式读取拼接后再 load。 ## 追问 **xmlMode 什么时候必须开启?** 解析 XML/RSS/Atom feed 时必须开启,否则标签大小写会被统一转小写,XML 的自闭合标签也会被错误处理。 **decodeEntities 设为 false 有什么用?** 保留原始 HTML 实体不解码,适合需要原样输出 HTML 的场景,如内容编辑器。默认 true 会把 &amp; 转成 &。 **能直接加载 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 和 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月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 之间。