乐闻世界logo
搜索文章和话题

服务端面试题手册

什么是自动化测试?自动化测试的类型和最佳实践有哪些?

答案自动化测试是 DevOps 实践中不可或缺的一环,它通过编写和执行自动化测试脚本,验证软件的功能、性能和可靠性,确保代码变更不会引入新的缺陷。自动化测试的类型1. 单元测试(Unit Testing)测试单个函数、方法或类由开发人员编写执行速度快依赖隔离(使用 Mock 或 Stub)示例:def calculate_discount(price, discount_rate): return price * (1 - discount_rate)def test_calculate_discount(): assert calculate_discount(100, 0.1) == 90 assert calculate_discount(200, 0.2) == 1602. 集成测试(Integration Testing)测试多个组件或服务的集成验证组件间的接口和数据流可以使用真实的依赖或模拟依赖示例:def test_user_service_integration(): user = user_service.create_user("test@example.com", "password123") retrieved_user = user_service.get_user(user.id) assert retrieved_user.email == "test@example.com"3. 端到端测试(End-to-End Testing)模拟真实用户场景测试完整的应用流程使用浏览器自动化工具(Selenium、Cypress)示例:describe('User Login Flow', () => { it('should successfully login with valid credentials', () => { cy.visit('/login') cy.get('#email').type('user@example.com') cy.get('#password').type('password123') cy.get('#login-button').click() cy.url().should('include', '/dashboard') })})4. 性能测试(Performance Testing)负载测试:测试系统在预期负载下的表现压力测试:测试系统在极限负载下的表现峰值测试:测试系统在突发流量下的表现工具: JMeter、Gatling、Locust5. 安全测试(Security Testing)漏洞扫描依赖检查安全配置验证工具: OWASP ZAP、Snyk、SonarQube自动化测试金字塔自动化测试金字塔描述了不同类型测试的理想比例: /\ / \ E2E Tests (少量) /____\ / \ Integration Tests (中等) /________\ / \ Unit Tests (大量) /____________\原则:底部(单元测试):最多,执行最快,成本最低中部(集成测试):适中,执行速度中等顶部(端到端测试):最少,执行最慢,成本最高常用自动化测试工具单元测试框架Java: JUnit, TestNGPython: pytest, unittestJavaScript: Jest, Mocha, JasmineGo: testing 包, testifyC#: NUnit, xUnit集成测试工具Postman: API 测试RestAssured: REST API 测试(Java)Supertest: HTTP 断言库(Node.js)端到端测试工具Selenium: 跨浏览器自动化Cypress: 现代 E2E 测试框架Playwright: 微软开发的浏览器自动化工具Puppeteer: Google Chrome 无头浏览器控制性能测试工具JMeter: 功能强大的性能测试工具Gatling: 高性能负载测试工具Locust: Python 编写的负载测试工具k6: 现代化的性能测试工具测试覆盖率工具JaCoCo: Java 代码覆盖率Coverage.py: Python 代码覆盖率Istanbul: JavaScript 代码覆盖率自动化测试在 CI/CD 中的集成1. 持续集成(CI)阶段# GitLab CI 示例test: stage: test script: - pip install -r requirements.txt - pytest tests/unit/ - pytest tests/integration/ coverage: '/TOTAL.*\s+(\d+%)$/'2. 测试策略快速反馈:单元测试在每次提交时运行全面验证:集成测试在合并请求时运行最终确认:端到端测试在部署到预生产环境时运行3. 测试报告生成测试报告(HTML、JUnit XML)覆盖率报告失败测试的截图和日志集成到 CI/CD 平台(GitLab、GitHub Actions)自动化测试最佳实践1. 测试编写原则独立性:每个测试应该独立运行可重复性:测试结果应该可重复快速执行:测试应该快速完成清晰命名:测试名称应该描述测试内容单一职责:每个测试只验证一个方面2. 测试数据管理使用测试数据工厂每个测试使用独立的数据测试后清理数据使用事务回滚3. Mock 和 Stub隔离外部依赖模拟各种场景(成功、失败、超时)验证方法调用示例:from unittest.mock import Mockdef test_send_email(): email_service = Mock() user_service = UserService(email_service) user_service.send_welcome_email("user@example.com") email_service.send.assert_called_once_with( "user@example.com", "Welcome!" )4. 测试覆盖率设定覆盖率目标(如 80%)关注关键路径的覆盖率不要为了覆盖率而写无意义的测试定期审查未覆盖的代码5. 测试维护定期更新测试删除过时的测试重构重复的测试代码保持测试代码质量测试驱动开发(TDD)TDD 是一种开发方法,要求在编写功能代码之前先编写测试。TDD 循环Red:编写一个失败的测试Green:编写最简单的代码使测试通过Refactor:重构代码,保持测试通过TDD 的优势提高代码质量减少缺陷改善设计提供活的文档行为驱动开发(BDD)BDD 是 TDD 的扩展,使用自然语言描述测试场景。示例(Gherkin 语法)Feature: User Login Scenario: Successful login with valid credentials Given a user exists with email "user@example.com" and password "password123" When the user logs in with email "user@example.com" and password "password123" Then the user should be redirected to the dashboard And the user should see a welcome messageBDD 工具Cucumber: 支持 Gherkin 语法SpecFlow: .NET BDD 框架Behave: Python BDD 框架自动化测试的挑战维护成本:测试代码需要持续维护测试稳定性:flaky tests(不稳定的测试)执行时间:测试套件可能变得很慢环境一致性:不同环境下的测试结果可能不同测试数据:管理测试数据的复杂性技能要求:团队需要掌握测试技能自动化测试的未来趋势AI 辅助测试:使用 AI 生成测试用例可视化测试:低代码/无代码测试工具测试左移:更早地介入测试混沌工程:主动测试系统的弹性测试右移:在生产环境中进行测试自动化测试是 DevOps 实践的基础,它通过快速反馈和持续验证,确保软件质量,支持频繁的代码变更和部署。建立完善的自动化测试体系,是实现持续交付和持续部署的关键。
阅读 0·2月22日 14:31

Cheerio 和 jsdom 有什么区别?如何选择使用?

Cheerio 和 jsdom 都是 Node.js 中处理 HTML/XML 的工具,但它们的设计理念和实现方式有显著差异。以下是详细的对比分析:1. 核心架构对比Cheerio类型:HTML 解析器底层实现:基于 htmlparser2DOM 实现:自定义的轻量级 DOM 实现JavaScript 执行:不支持浏览器环境模拟:不模拟jsdom类型:完整的 DOM 和浏览器环境模拟器底层实现:基于 WHATWG DOM 标准DOM 实现:完整的 W3C DOM 规范实现JavaScript 执行:完全支持浏览器环境模拟:完整模拟2. 功能对比表| 特性 | Cheerio | jsdom ||------|---------|-------|| HTML 解析 | ✅ 快速 | ✅ 标准 || CSS 选择器 | ✅ jQuery 风格 | ✅ 标准 || DOM 操作 | ✅ 基础操作 | ✅ 完整 API || JavaScript 执行 | ❌ 不支持 | ✅ 完全支持 || 事件处理 | ❌ 不支持 | ✅ 完全支持 || 性能 | ⚡ 极快 | 🐢 较慢 || 内存占用 | 📉 低 | 📈 高 || 浏览器 API | ❌ 无 | ✅ 完整 || 网络请求 | ❌ 无 | ✅ 支持 || Canvas | ❌ 无 | ✅ 支持 || LocalStorage | ❌ 无 | ✅ 支持 |3. 使用示例对比Cheerio 使用示例const cheerio = require('cheerio');const html = ` <div id="container"> <p class="text">Hello World</p> <button onclick="alert('Clicked')">Click</button> </div>`;const $ = cheerio.load(html);// 基本操作console.log($('#container').text()); // "Hello World"console.log($('.text').text()); // "Hello World"// DOM 操作$('.text').addClass('highlight');console.log($('.text').attr('class')); // "text highlight"// 无法执行 JavaScript$('button').click(); // 无效,Cheerio 不支持事件jsdom 使用示例const { JSDOM } = require('jsdom');const html = ` <div id="container"> <p class="text">Hello World</p> <button onclick="alert('Clicked')">Click</button> </div>`;const dom = new JSDOM(html);const document = dom.window.document;// 基本操作console.log(document.getElementById('container').textContent); // "Hello World"console.log(document.querySelector('.text').textContent); // "Hello World"// DOM 操作document.querySelector('.text').classList.add('highlight');console.log(document.querySelector('.text').className); // "text highlight"// 可以执行 JavaScriptconst button = document.querySelector('button');button.click(); // 有效,会触发 onclick 事件// 可以使用浏览器 APIconsole.log(dom.window.innerWidth); // 窗口宽度console.log(dom.window.location.href); // 当前 URL4. 性能对比解析速度测试const cheerio = require('cheerio');const { JSDOM } = require('jsdom');const largeHtml = '<div>' + '<p>Test</p>'.repeat(10000) + '</div>';// Cheerio 性能测试const start1 = Date.now();const $ = cheerio.load(largeHtml);const cheerioTime = Date.now() - start1;console.log(`Cheerio: ${cheerioTime}ms`);// jsdom 性能测试const start2 = Date.now();const dom = new JSDOM(largeHtml);const jsdomTime = Date.now() - start2;console.log(`jsdom: ${jsdomTime}ms`);// 典型结果:// Cheerio: 5-10ms// jsdom: 100-500ms内存占用对比// Cheerio - 内存占用低function cheerioMemoryTest() { const $ = cheerio.load(largeHtml); const elements = $('p'); return elements.length;}// jsdom - 内存占用高function jsdomMemoryTest() { const dom = new JSDOM(largeHtml); const elements = dom.window.document.querySelectorAll('p'); return elements.length;}5. 适用场景对比使用 Cheerio 的场景// 1. 网页爬虫和数据提取async function scrapeWebsite() { const axios = require('axios'); const response = await axios.get('https://example.com'); const $ = cheerio.load(response.data); return { title: $('title').text(), links: $('a').map((i, el) => $(el).attr('href')).get() };}// 2. HTML 内容处理function processHtml(html) { const $ = cheerio.load(html); $('script').remove(); // 移除脚本 $('style').remove(); // 移除样式 return $.html();}// 3. 批量处理大量文档function batchProcess(htmlList) { return htmlList.map(html => { const $ = cheerio.load(html); return $('title').text(); });}使用 jsdom 的场景// 1. 测试前端代码const { JSDOM } = require('jsdom');function testFrontendCode() { const dom = new JSDOM(` <div id="app"></div> <script> document.getElementById('app').textContent = 'Hello'; </script> `, { runScripts: 'dangerously' }); console.log(dom.window.document.getElementById('app').textContent);}// 2. 服务端渲染 (SSR)function renderComponent(component) { const dom = new JSDOM('<div id="root"></div>'); const root = dom.window.document.getElementById('root'); // 执行组件代码 component(root); return dom.serialize();}// 3. 处理需要 JavaScript 的内容function processDynamicContent(html) { const dom = new JSDOM(html, { runScripts: 'dangerously', resources: 'usable' }); // 等待 JavaScript 执行完成 return new Promise(resolve => { dom.window.onload = () => { resolve(dom.serialize()); }; });}6. API 对比Cheerio API 特点const $ = cheerio.load(html);// jQuery 风格的 API$('.class').text();$('.class').html();$('.class').attr('href');$('.class').addClass('active');$('.class').find('a');// 链式调用$('.container') .find('.item') .addClass('highlight') .text();// 不支持的浏览器 API$.window; // undefined$.document; // undefined$.localStorage; // undefinedjsdom API 特点const dom = new JSDOM(html);const document = dom.window.document;// 标准 DOM APIdocument.querySelector('.class').textContent;document.querySelector('.class').innerHTML;document.querySelector('.class').getAttribute('href');document.querySelector('.class').classList.add('active');document.querySelector('.class').querySelector('a');// 支持浏览器 APIdom.window.innerWidth;dom.window.location.href;dom.window.localStorage;dom.window.fetch;dom.window.console;7. 选择建议选择 Cheerio 的情况只需要解析和提取数据网页爬虫数据抓取HTML 内容处理性能要求高处理大量文档批量操作实时处理资源受限内存有限CPU 有限无服务器环境不需要浏览器功能不需要执行 JavaScript不需要事件处理不需要浏览器 API选择 jsdom 的情况需要完整的浏览器环境前端代码测试服务端渲染组件测试需要执行 JavaScript动态内容处理客户端代码执行框架渲染需要浏览器 APILocalStorageFetch APICanvasWeb Workers需要标准 DOM 行为事件冒泡DOM 事件浏览器兼容性测试8. 混合使用场景// 先用 jsdom 执行 JavaScript,再用 Cheerio 解析const { JSDOM } = require('jsdom');const cheerio = require('cheerio');async function hybridProcess(html) { // 1. 使用 jsdom 执行 JavaScript const dom = new JSDOM(html, { runScripts: 'dangerously' }); // 等待 JavaScript 执行 await new Promise(resolve => { dom.window.onload = resolve; }); // 2. 获取执行后的 HTML const processedHtml = dom.serialize(); // 3. 使用 Cheerio 快速解析 const $ = cheerio.load(processedHtml); return { title: $('title').text(), content: $('.content').text() };}总结Cheerio:轻量、快速、专注数据提取,适合爬虫和静态 HTML 处理jsdom:完整、标准、模拟浏览器,适合测试和动态内容处理选择原则:根据需求选择,需要性能用 Cheerio,需要完整功能用 jsdom混合使用:可以结合两者优势,先用 jsdom 执行 JS,再用 Cheerio 解析
阅读 0·2月22日 14:31

Cheerio 和 Puppeteer 有什么区别?如何选择使用?

Cheerio 和 Puppeteer 都是 Node.js 中用于处理网页的工具,但它们的设计目标和使用场景有显著差异:1. 核心区别| 特性 | Cheerio | Puppeteer ||------|---------|-----------|| 类型 | HTML 解析器 | 浏览器自动化工具 || JavaScript 执行 | 不支持 | 完全支持 || 动态内容 | 无法处理 | 完全支持 || 性能 | 极快 | 较慢 || 资源消耗 | 低 | 高 || API | jQuery 风格 | 浏览器 DevTools 协议 || 使用场景 | 静态 HTML 解析 | 动态网页、截图、PDF |2. Cheerio 的特点优势轻量快速:核心代码只有几百行,解析速度极快简单易用:jQuery 风格的 API,学习成本低低资源消耗:不需要启动浏览器,内存占用少适合批量处理:可以快速处理大量静态页面局限性无法执行 JavaScript:只能解析静态 HTML无法处理动态内容:无法获取通过 JS 动态加载的数据无法处理复杂交互:不支持点击、滚动等用户操作无法截图或生成 PDF:没有可视化能力适用场景// 适合:静态网页数据提取const cheerio = require('cheerio');const axios = require('axios');async function scrapeStaticSite() { const response = await axios.get('https://example.com'); const $ = cheerio.load(response.data); return { title: $('title').text(), links: $('a').map((i, el) => $(el).attr('href')).get() };}3. Puppeteer 的特点优势完整浏览器环境:使用真实的 Chrome/ChromiumJavaScript 执行:可以执行页面中的所有 JavaScript动态内容支持:可以获取 AJAX 加载的数据交互能力:支持点击、输入、滚动等操作可视化功能:支持截图、生成 PDF网络拦截:可以监控和修改网络请求局限性资源消耗大:需要启动完整的浏览器实例速度较慢:相比 Cheerio 慢很多复杂度高:API 相对复杂,学习成本高部署困难:在某些服务器环境部署较复杂适用场景// 适合:动态网页、需要交互的场景const puppeteer = require('puppeteer');async function scrapeDynamicSite() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com', { waitUntil: 'networkidle2' }); // 等待动态内容加载 await page.waitForSelector('.dynamic-content'); const data = await page.evaluate(() => { return { title: document.title, content: document.querySelector('.dynamic-content').textContent }; }); await browser.close(); return data;}4. 性能对比// Cheerio - 快速解析const cheerio = require('cheerio');async function cheerioBenchmark() { const start = Date.now(); const $ = cheerio.load(htmlString); const items = $('.item').map((i, el) => $(el).text()).get(); const time = Date.now() - start; console.log(`Cheerio: ${time}ms, ${items.length} items`); // 结果:通常 < 10ms}// Puppeteer - 完整浏览器const puppeteer = require('puppeteer');async function puppeteerBenchmark() { const start = Date.now(); const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setContent(htmlString); const items = await page.$$eval('.item', elements => elements.map(el => el.textContent) ); await browser.close(); const time = Date.now() - start; console.log(`Puppeteer: ${time}ms, ${items.length} items`); // 结果:通常 500-2000ms}5. 选择建议使用 Cheerio 的场景网站内容是静态 HTML需要处理大量页面对性能要求高只需要提取数据,不需要交互服务器资源有限使用 Puppeteer 的场景网站使用 JavaScript 动态加载内容需要模拟用户操作(点击、滚动等)需要截图或生成 PDF需要处理复杂的 SPA 应用需要监控网络请求混合使用场景// 先用 Puppeteer 获取动态内容,再用 Cheerio 解析const puppeteer = require('puppeteer');const cheerio = require('cheerio');async function hybridScrape() { const browser = await puppeteer.launch(); const page = await browser.newPage(); // 使用 Puppeteer 加载动态页面 await page.goto('https://example.com/dynamic'); await page.waitForSelector('.content'); // 获取 HTML const html = await page.content(); await browser.close(); // 使用 Cheerio 快速解析 const $ = cheerio.load(html); const data = $('.item').map((i, el) => ({ title: $(el).find('.title').text(), content: $(el).find('.content').text() })).get(); return data;}6. 实际应用示例Cheerio - 抓取静态博客async function scrapeBlog() { const response = await axios.get('https://blog.example.com'); const $ = cheerio.load(response.data); return $('.post').map((i, el) => ({ title: $(el).find('h2').text(), date: $(el).find('.date').text(), excerpt: $(el).find('.excerpt').text() })).get();}Puppeteer - 抓取动态电商网站async function scrapeShop() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://shop.example.com'); // 滚动加载更多商品 for (let i = 0; i < 5; i++) { await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); await page.waitForTimeout(1000); } const products = await page.$$eval('.product', items => items.map(item => ({ name: item.querySelector('.name').textContent, price: item.querySelector('.price').textContent })) ); await browser.close(); return products;}总结Cheerio:适合静态页面、高性能需求、批量处理Puppeteer:适合动态页面、需要交互、可视化需求混合使用:先用 Puppeteer 加载动态内容,再用 Cheerio 解析,可以获得最佳的性能和功能平衡
阅读 0·2月22日 14:30

如何使用 Cheerio 进行网页爬虫和数据抓取?

Cheerio 在网页爬虫和数据抓取方面表现出色,因为它轻量、快速且易于使用。以下是使用 Cheerio 进行网页爬虫的完整指南:1. 基本爬虫架构const cheerio = require('cheerio');const axios = require('axios');async function scrapePage(url) { try { // 1. 发送 HTTP 请求获取 HTML const response = await axios.get(url, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); // 2. 使用 Cheerio 加载 HTML const $ = cheerio.load(response.data); // 3. 提取数据 const data = { title: $('title').text(), description: $('meta[name="description"]').attr('content'), links: [] }; // 4. 提取所有链接 $('a[href]').each((index, element) => { data.links.push({ text: $(element).text().trim(), href: $(element).attr('href') }); }); return data; } catch (error) { console.error('爬取失败:', error.message); throw error; }}2. 抓取新闻网站示例async function scrapeNews() { const url = 'https://example-news.com'; const response = await axios.get(url); const $ = cheerio.load(response.data); const articles = []; $('.news-item').each((index, element) => { const $item = $(element); articles.push({ title: $item.find('.title').text().trim(), link: $item.find('a').attr('href'), summary: $item.find('.summary').text().trim(), date: $item.find('.date').text().trim(), author: $item.find('.author').text().trim() }); }); return articles;}3. 抓取电商产品信息async function scrapeProducts() { const url = 'https://example-shop.com/products'; const response = await axios.get(url); const $ = cheerio.load(response.data); const products = []; $('.product-card').each((index, element) => { const $product = $(element); const priceText = $product.find('.price').text(); const price = parseFloat(priceText.replace(/[^0-9.]/g, '')); products.push({ name: $product.find('.product-name').text().trim(), price: price, originalPrice: parseFloat($product.find('.original-price').text().replace(/[^0-9.]/g, '')) || null, discount: $product.find('.discount').text().trim(), rating: parseFloat($product.find('.rating').attr('data-rating')), reviews: parseInt($product.find('.review-count').text().replace(/[^0-9]/g, '')), image: $product.find('img').attr('src'), link: $product.find('a.product-link').attr('href') }); }); return products;}4. 分页爬取async function scrapeMultiplePages(baseUrl, maxPages) { const allData = []; for (let page = 1; page <= maxPages; page++) { const url = `${baseUrl}?page=${page}`; console.log(`正在爬取第 ${page} 页...`); try { const response = await axios.get(url); const $ = cheerio.load(response.data); $('.item').each((index, element) => { allData.push({ id: $(element).attr('data-id'), title: $(element).find('.title').text().trim(), content: $(element).find('.content').text().trim() }); }); // 延迟避免被封 await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { console.error(`第 ${page} 页爬取失败:`, error.message); } } return allData;}5. 处理相对 URLconst URL = require('url');function resolveUrl(base, relative) { return URL.resolve(base, relative);}// 使用示例const baseUrl = 'https://example.com';const relativeLink = '/article/123';const absoluteUrl = resolveUrl(baseUrl, relativeLink);// 结果: https://example.com/article/1236. 数据清洗和验证function cleanData(rawData) { return rawData.map(item => ({ title: item.title.replace(/\s+/g, ' ').trim(), price: parseFloat(item.price) || 0, description: item.description .replace(/<[^>]*>/g, '') // 移除 HTML 标签 .replace(/\s+/g, ' ') // 合并空格 .trim(), date: new Date(item.date), isValid: item.title.length > 0 && item.price > 0 })).filter(item => item.isValid);}7. 错误处理和重试机制async function fetchWithRetry(url, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await axios.get(url, { timeout: 10000, headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); return response.data; } catch (error) { console.log(`尝试 ${i + 1}/${maxRetries} 失败:`, error.message); if (i === maxRetries - 1) { throw error; } // 指数退避 await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000)); } }}8. 保存数据到文件const fs = require('fs');function saveToFile(data, filename) { const jsonData = JSON.stringify(data, null, 2); fs.writeFileSync(filename, jsonData, 'utf8'); console.log(`数据已保存到 ${filename}`);}// 保存为 CSVfunction saveToCSV(data, filename) { if (data.length === 0) return; const headers = Object.keys(data[0]).join(','); const rows = data.map(item => Object.values(item).map(value => `"${String(value).replace(/"/g, '""')}"` ).join(',') ); const csv = [headers, ...rows].join('\n'); fs.writeFileSync(filename, csv, 'utf8'); console.log(`CSV 已保存到 ${filename}`);}9. 完整爬虫示例const cheerio = require('cheerio');const axios = require('axios');const fs = require('fs');class WebScraper { constructor(baseUrl) { this.baseUrl = baseUrl; this.data = []; } async scrape(maxPages = 5) { for (let page = 1; page <= maxPages; page++) { await this.scrapePage(page); await this.delay(1000); } this.saveData(); return this.data; } async scrapePage(page) { const url = `${this.baseUrl}?page=${page}`; console.log(`正在爬取: ${url}`); try { const html = await fetchWithRetry(url); const $ = cheerio.load(html); $('.article').each((index, element) => { this.data.push(this.extractData($, element)); }); console.log(`第 ${page} 页完成,共 ${this.data.length} 条数据`); } catch (error) { console.error(`第 ${page} 页爬取失败:`, error.message); } } extractData($, element) { const $el = $(element); return { title: $el.find('.title').text().trim(), author: $el.find('.author').text().trim(), date: $el.find('.date').text().trim(), content: $el.find('.content').text().trim(), link: $el.find('a').attr('href'), tags: $el.find('.tag').map((i, tag) => $(tag).text()).get() }; } saveData() { const filename = `scraped_data_${Date.now()}.json`; fs.writeFileSync(filename, JSON.stringify(this.data, null, 2)); console.log(`数据已保存到 ${filename}`); } delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }}// 使用示例async function main() { const scraper = new WebScraper('https://example-blog.com/articles'); const data = await scraper.scrape(10); console.log(`爬取完成,共 ${data.length} 条数据`);}main().catch(console.error);最佳实践设置合理的延迟:避免频繁请求导致被封使用 User-Agent:模拟真实浏览器请求处理异常:完善的错误处理和重试机制数据验证:清洗和验证提取的数据遵守 robots.txt:尊重网站的爬虫规则增量更新:只抓取新增或变化的数据并发控制:使用队列控制并发请求数量
阅读 0·2月22日 14:30

Cheerio 使用中的常见问题有哪些?如何解决这些问题?

Cheerio 提供了丰富的 API,但在实际使用中,开发者经常会遇到一些常见问题。以下是 Cheerio 使用中的常见问题及其解决方案:1. 中文乱码问题问题描述当抓取包含中文的网页时,出现乱码。解决方案const axios = require('axios');const cheerio = require('cheerio');const iconv = require('iconv-lite');async function scrapeWithEncoding(url) { // 方案1:设置响应类型为 arraybuffer const response = await axios.get(url, { responseType: 'arraybuffer', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } }); // 方案2:检测编码并转换 let html = response.data; // 检测 Content-Type 中的编码 const contentType = response.headers['content-type'] || ''; const charsetMatch = contentType.match(/charset=([^;]+)/i); if (charsetMatch) { const charset = charsetMatch[1].toLowerCase(); if (charset !== 'utf-8') { html = iconv.decode(Buffer.from(html), charset); } } // 方案3:从 HTML meta 标签获取编码 const $temp = cheerio.load(html); const metaCharset = $temp('meta[charset]').attr('charset'); if (metaCharset && metaCharset.toLowerCase() !== 'utf-8') { html = iconv.decode(Buffer.from(html), metaCharset); } const $ = cheerio.load(html); return $('title').text();}2. 选择器找不到元素问题描述使用选择器查询时返回空结果,但元素确实存在。解决方案const cheerio = require('cheerio');const html = ` <div class="container"> <p class="text">Hello</p> </div>`;const $ = cheerio.load(html);// 问题:选择器错误console.log($('.container p.text').length); // 1// 解决方案1:检查选择器语法console.log($('.container > p.text').length); // 1// 解决方案2:使用更宽松的选择器console.log($('.container .text').length); // 1// 解决方案3:逐步调试console.log($('.container').length); // 1console.log($('.container p').length); // 1console.log($('.container p').hasClass('text')); // true// 解决方案4:使用 contains() 查找包含文本的元素console.log($('p:contains("Hello")').length); // 1// 解决方案5:检查 HTML 是否正确加载console.log($.html()); // 查看完整的 HTML3. 动态内容无法获取问题描述页面中通过 JavaScript 动态加载的内容无法获取。解决方案const cheerio = require('cheerio');const axios = require('axios');// 问题:直接使用 Cheerio 无法获取动态内容async function scrapeStatic() { const response = await axios.get('https://example.com/dynamic'); const $ = cheerio.load(response.data); console.log($('.dynamic-content').text()); // 空}// 解决方案:结合 Puppeteerconst puppeteer = require('puppeteer');async function scrapeDynamic() { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com/dynamic'); // 等待动态内容加载 await page.waitForSelector('.dynamic-content'); const html = await page.content(); await browser.close(); const $ = cheerio.load(html); console.log($('.dynamic-content').text()); // 有内容}// 解决方案:直接调用 APIasync function scrapeAPI() { const response = await axios.get('https://example.com/api/data'); const data = response.data; console.log(data); // 直接获取 JSON 数据}4. 内存占用过高问题描述处理大量 HTML 时内存占用过高,导致程序崩溃。解决方案const cheerio = require('cheerio');// 问题:一次性处理大文件function processLargeFileBad(html) { const $ = cheerio.load(html); const results = []; // 处理数百万个元素 $('.item').each((i, el) => { results.push({ title: $(el).find('.title').text(), content: $(el).find('.content').text() }); }); return results;}// 解决方案1:分批处理function processLargeFileGood(html) { const $ = cheerio.load(html); const batchSize = 1000; const total = $('.item').length; const results = []; for (let i = 0; i < total; i += batchSize) { const $batch = $('.item').slice(i, i + batchSize); const batchData = $batch.map((j, el) => ({ title: $(el).find('.title').text(), content: $(el).find('.content').text() })).get(); results.push(...batchData); // 及时清理 $batch = null; // 强制垃圾回收(开发环境) if (global.gc) { global.gc(); } } return results;}// 解决方案2:使用流式处理const fs = require('fs');const { Transform } = require('stream');function processWithStream(filePath) { return new Promise((resolve, reject) => { const results = []; let buffer = ''; const transformStream = new Transform({ transform(chunk, encoding, callback) { buffer += chunk.toString(); // 按标签分割处理 const items = buffer.match(/<item[^>]*>[\s\S]*?<\/item>/g) || []; items.forEach(item => { const $ = cheerio.load(item); results.push({ title: $('.title').text(), content: $('.content').text() }); }); // 清理已处理的内容 const lastIndex = buffer.lastIndexOf('</item>'); if (lastIndex !== -1) { buffer = buffer.slice(lastIndex + 7); } callback(); }, flush(callback) { resolve(results); callback(); } }); fs.createReadStream(filePath) .pipe(transformStream) .on('error', reject); });}5. 相对路径处理问题问题描述提取的链接是相对路径,无法直接访问。解决方案const cheerio = require('cheerio');const { URL } = require('url');function resolveLinks(html, baseUrl) { const $ = cheerio.load(html); const links = []; $('a[href]').each((i, el) => { const href = $(el).attr('href'); const absoluteUrl = new URL(href, baseUrl).href; links.push({ text: $(el).text().trim(), href: href, absoluteUrl: absoluteUrl }); }); return links;}// 使用示例const html = ` <a href="/page1">Page 1</a> <a href="../page2">Page 2</a> <a href="https://example.com/page3">Page 3</a>`;const links = resolveLinks(html, 'https://example.com/dir/index.html');console.log(links);6. 表单数据提取问题问题描述提取表单数据时遇到复选框、多选框等复杂情况。解决方案const cheerio = require('cheerio');function extractFormData(html) { const $ = cheerio.load(html); const formData = {}; // 文本输入 $('input[type="text"]').each((i, el) => { const name = $(el).attr('name'); const value = $(el).val() || ''; formData[name] = value; }); // 复选框(多选) $('input[type="checkbox"]:checked').each((i, el) => { const name = $(el).attr('name'); const value = $(el).val(); if (!formData[name]) { formData[name] = []; } formData[name].push(value); }); // 单选框 $('input[type="radio"]:checked').each((i, el) => { const name = $(el).attr('name'); const value = $(el).val(); formData[name] = value; }); // 下拉选择 $('select').each((i, el) => { const name = $(el).attr('name'); const selectedOption = $(el).find('option:selected'); formData[name] = selectedOption.val(); }); // 多选下拉 $('select[multiple]').each((i, el) => { const name = $(el).attr('name'); const selectedOptions = $(el).find('option:selected'); formData[name] = selectedOptions.map((j, opt) => $(opt).val()).get(); }); // 文本域 $('textarea').each((i, el) => { const name = $(el).attr('name'); const value = $(el).val() || ''; formData[name] = value; }); return formData;}7. HTML 实体编码问题问题描述HTML 中的特殊字符被编码,如  、& 等。解决方案const cheerio = require('cheerio');const html = '<div>Hello & World   Test</div>';// 问题:默认会解码实体const $ = cheerio.load(html);console.log($('.div').text()); // "Hello & World Test"// 解决方案1:禁用实体解码const $2 = cheerio.load(html, { decodeEntities: false });console.log($2('.div').text()); // "Hello & World   Test"// 解决方案2:手动处理实体const he = require('he');const text = he.decode($('.div').text());console.log(text); // "Hello & World Test"// 解决方案3:使用 html() 方法获取原始 HTMLconst rawHtml = $('.div').html();console.log(rawHtml); // "Hello & World   Test"8. 性能问题问题描述处理大量数据时性能不佳。解决方案const cheerio = require('cheerio');// 问题:使用复杂选择器function slowQuery($) { return $('div div div p span a').text();}// 解决方案1:使用更具体的选择器function fastQuery1($) { return $('.container .link').text();}// 解决方案2:使用 find() 方法function fastQuery2($) { return $('.container').find('.link').text();}// 解决方案3:缓存选择器结果function fastQuery3($) { const $container = $('.container'); return $container.find('.link').text();}// 解决方案4:使用原生方法function fastestQuery($) { const container = $('.container')[0]; return container.querySelector('.link').textContent;}9. 空白字符处理问题描述提取的文本包含大量空白字符。解决方案const cheerio = require('cheerio');const html = ` <div> <p> Hello World </p> </div>`;const $ = cheerio.load(html);// 问题:包含大量空白console.log($('p').text()); // "\n Hello\n World\n "// 解决方案1:使用 trim()console.log($('p').text().trim()); // "Hello\n World"// 解决方案2:使用正则替换console.log($('p').text().replace(/\s+/g, ' ').trim()); // "Hello World"// 解决方案3:使用 normalizeWhitespace 选项const $2 = cheerio.load(html, { normalizeWhitespace: true });console.log($2('p').text()); // "Hello World"// 解决方案4:自定义清理函数function cleanText(text) { return text .replace(/[\r\n\t]+/g, ' ') // 替换换行和制表符 .replace(/\s+/g, ' ') // 合并多个空格 .trim(); // 去除首尾空格}console.log(cleanText($('p').text())); // "Hello World"10. XML 解析问题问题描述解析 XML 文档时出现问题。解决方案const cheerio = require('cheerio');const xml = ` <root> <item id="1"> <name>Item 1</name> </item> <item id="2"> <name>Item 2</name> </item> </root>`;// 解决方案:使用 XML 模式const $ = cheerio.load(xml, { xmlMode: true, decodeEntities: false});// 提取数据const items = [];$('item').each((i, el) => { items.push({ id: $(el).attr('id'), name: $(el).find('name').text() });});console.log(items);通过掌握这些常见问题的解决方案,可以更有效地使用 Cheerio 进行 HTML/XML 解析和数据提取。
阅读 0·2月22日 14:30

Cheerio 的 DOM 操作方法有哪些?如何使用这些方法?

Cheerio 提供了丰富的 DOM 操作方法,与 jQuery 的 API 高度兼容。以下是常用的 DOM 操作方法:1. 获取和设置内容// 获取文本内容$('p').text(); // 获取第一个匹配元素的文本$('p').text('New text'); // 设置所有匹配元素的文本// 获取 HTML 内容$('.container').html(); // 获取第一个匹配元素的 HTML$('.container').html('<p>New</p>'); // 设置所有匹配元素的 HTML// 获取表单值$('input').val(); // 获取输入值$('input').val('new value'); // 设置输入值// 获取属性值$('a').attr('href'); // 获取 href 属性$('a').attr('href', 'new-url'); // 设置 href 属性$('a').attr({ // 设置多个属性 href: 'new-url', target: '_blank'});$('a').removeAttr('target'); // 移除属性// 获取数据属性$('div').data('id'); // 获取 data-id$('div').data('id', '123'); // 设置 data-id2. CSS 类操作// 添加类$('div').addClass('active');$('div').addClass('active highlight');// 移除类$('div').removeClass('active');$('div').removeClass('active highlight');// 切换类$('div').toggleClass('active');// 检查类$('div').hasClass('active'); // 返回 true/false// 获取类名$('div').attr('class'); // 获取所有类名3. CSS 样式操作// 获取样式$('div').css('color'); // 获取 color 样式// 设置样式$('div').css('color', 'red');$('div').css({ // 设置多个样式 color: 'red', fontSize: '14px', backgroundColor: '#f0f0f0'});4. DOM 遍历// 查找子元素$('div').find('p'); // 查找所有后代 p$('div').children(); // 获取直接子元素$('div').children('p'); // 获取直接子元素 p// 父元素$('p').parent(); // 获取直接父元素$('p').parents(); // 获取所有祖先元素$('p').parents('.container'); // 获取匹配的祖先元素$('p').closest('.container'); // 获取最近的匹配祖先// 兄弟元素$('p').next(); // 下一个兄弟元素$('p').prev(); // 上一个兄弟元素$('p').nextAll(); // 之后的所有兄弟元素$('p').prevAll(); // 之前的所有兄弟元素$('p').siblings(); // 所有兄弟元素$('p').siblings('.active'); // 匹配的兄弟元素5. DOM 插入// 内部插入$('div').append('<p>New paragraph</p>'); // 在元素末尾插入$('<p>New</p>').appendTo('div'); // 插入到元素末尾$('div').prepend('<p>New paragraph</p>'); // 在元素开头插入$('<p>New</p>').prependTo('div'); // 插入到元素开头// 外部插入$('div').after('<p>New paragraph</p>'); // 在元素之后插入$('<p>New</p>').insertAfter('div'); // 插入到元素之后$('div').before('<p>New paragraph</p>'); // 在元素之前插入$('<p>New</p>').insertBefore('div'); // 插入到元素之前// 替换$('p').replaceWith('<div>New</div>'); // 替换元素$('<div>New</div>').replaceAll('p'); // 替换所有匹配元素6. DOM 删除// 删除元素$('p').remove(); // 删除匹配的元素及其子元素$('p').empty(); // 清空元素内容,保留元素本身// 分离元素const $detached = $('p').detach(); // 删除元素但保留数据和事件7. DOM 复制// 克隆元素const $clone = $('div').clone(); // 克隆元素const $cloneWithEvents = $('div').clone(true); // 克隆元素并复制事件8. 元素过滤// 过滤元素$('li').filter('.active'); // 保留匹配的元素$('li').not('.active'); // 移除匹配的元素$('li').first(); // 获取第一个元素$('li').last(); // 获取最后一个元素$('li').eq(2); // 获取索引为 2 的元素$('li').slice(1, 4); // 获取索引 1-3 的元素// 查找元素$('div').has('p'); // 包含 p 的 div$('div').is('.active'); // 检查是否匹配9. 集合操作// 获取元素数量$('li').length; // 或 $('li').size()// 获取索引$('li').index(); // 当前元素在集合中的索引$('li').index($('.active')); // 指定元素的索引// 转换为数组const texts = $('li').map(function() { return $(this).text();}).get(); // 转换为普通数组// 遍历元素$('li').each(function(i, elem) { console.log(i, $(this).text());});// 获取 DOM 元素const elem = $('div')[0]; // 获取第一个原生 DOM 元素const elem = $('div').get(0); // 同上const elems = $('div').get(); // 获取所有原生 DOM 元素10. 实用示例// 提取文章标题和链接const articles = [];$('.article').each(function() { articles.push({ title: $(this).find('h2').text(), link: $(this).find('a').attr('href'), summary: $(this).find('p').text().trim() });});// 修改表格数据$('table tr').each(function() { const $row = $(this); const price = parseFloat($row.find('.price').text()); if (price > 100) { $row.addClass('highlight'); }});// 生成新的 HTMLconst $container = cheerio.load('<div class="container"></div>');data.items.forEach(item => { $container('.container').append(` <div class="item"> <h3>${item.title}</h3> <p>${item.description}</p> </div> `);});
阅读 0·2月22日 14:30

Cheerio 如何加载 HTML 内容?有哪些加载方式?

Cheerio 提供了多种加载 HTML 内容的方法,适用于不同的使用场景:1. 从 HTML 字符串加载最常用的方法,直接传入 HTML 字符串:const cheerio = require('cheerio');const $ = cheerio.load('<div class="content"><p>Hello</p></div>');2. 从文件加载读取 HTML 文件后加载:const fs = require('fs');const cheerio = require('cheerio');const html = fs.readFileSync('index.html', 'utf8');const $ = cheerio.load(html);3. 使用配置选项加载cheerio.load() 方法接受第二个参数作为配置选项:const $ = cheerio.load(html, { // 是否识别 XML 模式 xmlMode: false, // 是否解码 HTML 实体 decodeEntities: true, // 是否包含空白节点 withDomLvl1: false, // 自�认函数处理 XML 标签 normalizeWhitespace: false, // 使用 htmlparser2 的选项 xml: { xmlMode: false, decodeEntities: true }});4. XML 模式加载处理 XML 文档时启用 XML 模式:const xml = '<root><item>Value</item></root>';const $ = cheerio.load(xml, { xmlMode: true });5. 流式处理(结合其他库)对于大文件,可以使用流式读取:const fs = require('fs');const cheerio = require('cheerio');const stream = fs.createReadStream('large.html');let html = '';stream.on('data', chunk => { html += chunk;});stream.on('end', () => { const $ = cheerio.load(html); // 处理 DOM});最佳实践对于小到中等大小的 HTML,直接使用 cheerio.load()对于大文件,考虑分块处理或使用专门的流式解析器XML 文档务必设置 xmlMode: true根据需求调整 decodeEntities 选项
阅读 0·2月22日 14:30

Cheerio 中常用的选择器有哪些?如何高效使用选择器?

Cheerio 支持几乎所有 jQuery 的选择器语法,这使得熟悉 jQuery 的开发者可以快速上手。以下是 Cheerio 中常用的选择器类型:1. 基本选择器// 元素选择器$('div')$('p')$('a')// ID 选择器$('#header')$('#main-content')// 类选择器$('.container')$('.active')// 多重选择器$('div, p, a')$('.class1, .class2')2. 层级选择器// 后代选择器$('div p') // div 内的所有 p 元素$('.container a') // .container 内的所有 a 元素// 子元素选择器$('ul > li') // ul 的直接子元素 li// 相邻兄弟选择器$('h2 + p') // 紧跟在 h2 后的 p 元素// 通用兄弟选择器$('h2 ~ p') // h2 之后的所有兄弟 p 元素3. 属性选择器// 存在属性$('[href]')$('[data-id]')// 属性值匹配$('[class="active"]')$('[href="https://example.com"]')// 属性值包含$('[class*="btn"]') // class 包含 "btn"$('[href*="/api/"]')// 属性值开头$('[class^="col-"]') // class 以 "col-" 开头$('[href^="https://"]')// 属性值结尾$('[src$=".jpg"]') // src 以 ".jpg" 结尾$('[href$=".html"]')// 多属性选择器$('a[href^="http"][target="_blank"]')4. 伪类选择器// 位置伪类$('li:first') // 第一个 li$('li:last') // 最后一个 li$('li:even') // 偶数位置的 li$('li:odd') // 奇数位置的 li$('li:eq(2)') // 第三个 li(从 0 开始)$('li:gt(2)') // 索引大于 2 的 li$('li:lt(5)') // 索引小于 5 的 li// 内容伪类$('p:contains("Hello")') // 包含 "Hello" 文本的 p$('p:empty') // 空的 p 元素$('div:has(p)') // 包含 p 的 div// 表单伪类$('input:checked') // 选中的 checkbox/radio$('input:disabled') // 禁用的 input$('input:enabled') // 启用的 input$('input:focus') // 获得焦点的 input$('option:selected') // 选中的 option5. 表单选择器$(':text') // 文本输入框$(':password') // 密码输入框$(':radio') // 单选按钮$(':checkbox') // 复选框$(':submit') // 提交按钮$(':reset') // 重置按钮$(':button') // 按钮$(':file') // 文件上传$(':image') // 图片按钮6. 组合选择器示例// 复杂选择器组合$('.container > .row:first-child .col-md-4 a[href^="http"]')// 多条件筛选$('div.active[data-type="article"]:not(.hidden)')// 嵌套选择$('.content').find('p').filter('.highlight')性能优化建议使用具体的选择器:$('#content p') 比 $('p') 更快避免过度使用通配符:$('*') 性能较差缓存选择结果:重复使用时保存到变量使用 find() 代替层级选择器:在某些情况下性能更好限制搜索范围:先选择父元素再查找子元素// 好的做法const $content = $('#content');const $paragraphs = $content.find('p');// 避免$('p').each(function() { if ($(this).parents('#content').length) { // 处理 }});
阅读 0·2月22日 14:30

如何开发和使用 Cheerio 插件?有哪些实用的插件示例?

Cheerio 支持插件系统,可以通过插件扩展其功能。以下是 Cheerio 插件开发的完整指南:1. Cheerio 插件基础插件结构Cheerio 插件本质上是一个函数,它接收 Cheerio 实例作为参数,并扩展其原型:// 基本插件结构module.exports = function(cheerio) { // 扩展 Cheerio 原型 cheerio.prototype.myMethod = function(selector) { // 插件逻辑 return this; };};简单插件示例// my-plugin.jsmodule.exports = function(cheerio) { // 添加一个获取所有文本内容的方法 cheerio.prototype.getAllText = function() { return this.map((i, el) => cheerio(el).text()).get(); }; // 添加一个过滤空元素的方法 cheerio.prototype.filterEmpty = function() { return this.filter((i, el) => { return cheerio(el).text().trim().length > 0; }); };};2. 使用插件安装和加载插件const cheerio = require('cheerio');const myPlugin = require('./my-plugin');// 加载插件cheerio.use(myPlugin);// 使用插件提供的方法const $ = cheerio.load('<div><p>Hello</p><p></p></div>');console.log($('p').getAllText()); // ['Hello']console.log($('p').filterEmpty().length); // 1链式调用// 插件方法支持链式调用const result = $('p') .filterEmpty() .getAllText() .map(text => text.toUpperCase());3. 实用插件示例1. 文本清理插件// text-cleaner.jsmodule.exports = function(cheerio) { // 清理文本中的多余空白 cheerio.prototype.cleanText = function() { return this.each((i, el) => { const $el = cheerio(el); const text = $el.text() .replace(/\s+/g, ' ') .trim(); $el.text(text); }); }; // 移除指定标签 cheerio.prototype.removeTags = function(tags) { const tagArray = Array.isArray(tags) ? tags : [tags]; return this.each((i, el) => { const $el = cheerio(el); tagArray.forEach(tag => { $el.find(tag).remove(); }); }); };};2. 数据提取插件// data-extractor.jsmodule.exports = function(cheerio) { // 提取表格数据为二维数组 cheerio.prototype.tableToArray = function() { const result = []; this.find('tr').each((i, row) => { const rowData = []; cheerio(row).find('td, th').each((j, cell) => { rowData.push(cheerio(cell).text().trim()); }); result.push(rowData); }); return result; }; // 提取表格数据为对象数组 cheerio.prototype.tableToObjects = function() { const $ = cheerio(this); const headers = []; const result = []; // 提取表头 $.find('thead th, tr:first-child td').each((i, th) => { headers.push(cheerio(th).text().trim()); }); // 提取数据行 $find('tbody tr, tr:not(:first-child)').each((i, row) => { const obj = {}; cheerio(row).find('td').each((j, td) => { const key = headers[j] || `col_${j}`; obj[key] = cheerio(td).text().trim(); }); result.push(obj); }); return result; };};3. URL 处理插件// url-handler.jsconst { URL } = require('url');module.exports = function(cheerio) { // 将相对 URL 转换为绝对 URL cheerio.prototype.resolveUrls = function(baseUrl) { return this.each((i, el) => { const $el = cheerio(el); const href = $el.attr('href'); const src = $el.attr('src'); if (href) { $el.attr('href', new URL(href, baseUrl).href); } if (src) { $el.attr('src', new URL(src, baseUrl).href); } }); }; // 提取所有链接 cheerio.prototype.extractLinks = function() { const links = []; this.find('a[href]').each((i, el) => { const $el = cheerio(el); links.push({ text: $el.text().trim(), href: $el.attr('href'), title: $el.attr('title') }); }); return links; };};4. 图片处理插件// image-handler.jsmodule.exports = function(cheerio) { // 提取所有图片信息 cheerio.prototype.extractImages = function() { const images = []; this.find('img').each((i, el) => { const $el = cheerio(el); images.push({ src: $el.attr('src'), alt: $el.attr('alt'), title: $el.attr('title'), width: $el.attr('width'), height: $el.attr('height') }); }); return images; }; // 懒加载图片处理 cheerio.prototype.handleLazyImages = function() { return this.each((i, el) => { const $el = cheerio(el); const dataSrc = $el.attr('data-src'); if (dataSrc) { $el.attr('src', dataSrc); } }); };};4. 高级插件开发带配置的插件// configurable-plugin.jsmodule.exports = function(cheerio, options = {}) { const defaultOptions = { trim: true, removeEmpty: true, maxLength: 1000 }; const opts = { ...defaultOptions, ...options }; cheerio.prototype.smartExtract = function() { return this.map((i, el) => { let text = cheerio(el).text(); if (opts.trim) { text = text.trim(); } if (opts.removeEmpty && text.length === 0) { return null; } if (opts.maxLength && text.length > opts.maxLength) { text = text.substring(0, opts.maxLength) + '...'; } return text; }).filter(text => text !== null); };};// 使用cheerio.use(configurablePlugin, { trim: true, removeEmpty: true, maxLength: 500});异步插件// async-plugin.jsmodule.exports = function(cheerio) { cheerio.prototype.fetchContent = async function(url) { const axios = require('axios'); const response = await axios.get(url); return cheerio.load(response.data); }; cheerio.prototype.batchProcess = async function(processor) { const results = []; for (let i = 0; i < this.length; i++) { const result = await processor(cheerio(this[i])); results.push(result); } return results; };};5. 插件发布package.json 配置{ "name": "cheerio-my-plugin", "version": "1.0.0", "description": "My Cheerio plugin", "main": "index.js", "keywords": [ "cheerio", "plugin", "html", "parser" ], "peerDependencies": { "cheerio": ">=1.0.0" }, "repository": { "type": "git", "url": "https://github.com/username/cheerio-my-plugin" }, "license": "MIT"}插件测试// test.jsconst cheerio = require('cheerio');const myPlugin = require('./my-plugin');describe('My Plugin', () => { beforeEach(() => { cheerio.use(myPlugin); }); test('should filter empty elements', () => { const $ = cheerio.load('<div><p>Hello</p><p></p></div>'); const result = $('p').filterEmpty(); expect(result.length).toBe(1); expect(result.text()).toBe('Hello'); }); test('should get all text', () => { const $ = cheerio.load('<div><p>Hello</p><p>World</p></div>'); const result = $('p').getAllText(); expect(result).toEqual(['Hello', 'World']); });});6. 现有流行插件cheerio-tableparserconst cheerio = require('cheerio');const tableParser = require('cheerio-tableparser');cheerio.use(tableParser);const $ = cheerio.load(html);const tableData = $('table').parsetable();cheerio-selectconst cheerio = require('cheerio');const select = require('cheerio-select');cheerio.use(select);const $ = cheerio.load(html);const elements = $.select('div > p:first-child');7. 最佳实践1. 命名规范// ✅ 好的命名cheerio.prototype.extractLinks = function() {}cheerio.prototype.cleanText = function() {}// ❌ 不好的命名cheerio.prototype.doSomething = function() {}cheerio.prototype.method1 = function() {}2. 返回值处理// ✅ 支持链式调用cheerio.prototype.myMethod = function() { // 处理逻辑 return this;};// ✅ 返回新集合cheerio.prototype.myFilter = function() { const filtered = this.filter(/* 条件 */); return filtered;};3. 错误处理cheerio.prototype.safeExtract = function() { try { // 提取逻辑 return this.map((i, el) => { return cheerio(el).text(); }).get(); } catch (error) { console.error('Extraction failed:', error); return []; }};4. 文档和注释/** * 提取所有链接信息 * @param {Object} options - 配置选项 * @param {boolean} options.resolveAbsolute - 是否转换为绝对 URL * @param {string} options.baseUrl - 基础 URL * @returns {Array} 链接信息数组 */cheerio.prototype.extractLinks = function(options = {}) { // 实现};通过开发和使用 Cheerio 插件,可以大大扩展 Cheerio 的功能,提高开发效率。
阅读 0·2月22日 14:30

Cheerio 如何处理动态加载的内容?有哪些解决方案?

Cheerio 本身不支持处理动态加载的内容,因为它只是一个 HTML 解析器,不会执行 JavaScript。但是,我们可以通过多种方式结合其他工具来处理动态内容:1. 使用 Puppeteer + Cheerio 组合这是最常用的方案,先用 Puppeteer 加载动态页面,然后用 Cheerio 解析:const puppeteer = require('puppeteer');const cheerio = require('cheerio');async function scrapeDynamicContent(url) { const browser = await puppeteer.launch({ headless: true }); const page = await browser.newPage(); // 访问页面并等待动态内容加载 await page.goto(url, { waitUntil: 'networkidle2' }); // 等待特定元素出现 await page.waitForSelector('.dynamic-content', { timeout: 10000 }); // 获取完整的 HTML const html = await page.content(); // 关闭浏览器 await browser.close(); // 使用 Cheerio 解析 const $ = cheerio.load(html); // 提取数据 const data = []; $('.dynamic-item').each((index, element) => { data.push({ title: $(element).find('.title').text().trim(), content: $(element).find('.content').text().trim(), link: $(element).find('a').attr('href') }); }); return data;}2. 处理无限滚动页面async function scrapeInfiniteScroll(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); let previousHeight = 0; let currentHeight = await page.evaluate('document.body.scrollHeight'); // 滚动直到没有新内容加载 while (currentHeight > previousHeight) { previousHeight = currentHeight; // 滚动到底部 await page.evaluate('window.scrollTo(0, document.body.scrollHeight)'); // 等待新内容加载 await page.waitForTimeout(2000); currentHeight = await page.evaluate('document.body.scrollHeight'); } // 获取 HTML 并用 Cheerio 解析 const html = await page.content(); const $ = cheerio.load(html); const items = $('.item').map((i, el) => ({ title: $(el).find('.title').text(), link: $(el).find('a').attr('href') })).get(); await browser.close(); return items;}3. 处理懒加载图片async function scrapeLazyImages(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); // 滚动页面触发懒加载 const scrollHeight = await page.evaluate('document.body.scrollHeight'); for (let i = 0; i < scrollHeight; i += 500) { await page.evaluate(`window.scrollTo(0, ${i})`); await page.waitForTimeout(500); } // 等待所有图片加载完成 await page.evaluate(async () => { const images = Array.from(document.querySelectorAll('img[data-src]')); await Promise.all(images.map(img => { if (img.dataset.src) { img.src = img.dataset.src; } })); }); await page.waitForTimeout(2000); const html = await page.content(); const $ = cheerio.load(html); const images = $('img').map((i, el) => ({ src: $(el).attr('src'), alt: $(el).attr('alt') })).get(); await browser.close(); return images;}4. 处理点击加载更多async function scrapeClickToLoad(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url); let hasMore = true; const allItems = []; while (hasMore) { // 等待内容加载 await page.waitForSelector('.item'); // 获取当前页面的 HTML const html = await page.content(); const $ = cheerio.load(html); // 提取当前页面的数据 $('.item').each((index, element) => { const title = $(element).find('.title').text(); // 避免重复添加 if (!allItems.some(item => item.title === title)) { allItems.push({ title }); } }); // 检查是否有"加载更多"按钮 const loadMoreButton = await page.$('.load-more'); if (loadMoreButton) { // 点击加载更多 await loadMoreButton.click(); // 等待新内容加载 await page.waitForTimeout(2000); } else { hasMore = false; } } await browser.close(); return allItems;}5. 处理 AJAX 请求async function scrapeAJAXContent(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); // 监听网络请求 const apiData = []; page.on('response', async response => { const url = response.url(); if (url.includes('/api/data')) { const data = await response.json(); apiData.push(...data); } }); await page.goto(url); // 等待 AJAX 请求完成 await page.waitForSelector('.data-loaded', { timeout: 15000 }); await browser.close(); return apiData;}6. 使用 Playwright 替代方案Playwright 是另一个强大的浏览器自动化工具:const { chromium } = require('playwright');const cheerio = require('cheerio');async function scrapeWithPlaywright(url) { const browser = await chromium.launch(); const page = await browser.newPage(); await page.goto(url, { waitUntil: 'networkidle' }); // 等待动态内容 await page.waitForSelector('.dynamic-content'); const html = await page.content(); await browser.close(); const $ = cheerio.load(html); return $('.item').map((i, el) => $(el).text()).get();}7. 优化性能的技巧// 1. 禁用不必要的资源加载async function optimizedScrape(url) { const browser = await puppeteer.launch(); const page = await browser.newPage(); // 阻止图片、字体等资源加载 await page.setRequestInterception(true); page.on('request', request => { const resourceType = request.resourceType(); if (['image', 'font', 'stylesheet'].includes(resourceType)) { request.abort(); } else { request.continue(); } }); await page.goto(url); const html = await page.content(); await browser.close(); return cheerio.load(html);}// 2. 复用浏览器实例class Scraper { constructor() { this.browser = null; } async init() { this.browser = await puppeteer.launch(); } async scrape(url) { const page = await this.browser.newPage(); await page.goto(url); const html = await page.content(); await page.close(); return cheerio.load(html); } async close() { await this.browser.close(); }}// 使用示例async function main() { const scraper = new Scraper(); await scraper.init(); const urls = ['url1', 'url2', 'url3']; const results = []; for (const url of urls) { const $ = await scraper.scrape(url); results.push($('.title').text()); } await scraper.close(); return results;}8. 错误处理和重试async function scrapeWithRetry(url, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto(url, { timeout: 30000 }); await page.waitForSelector('.content', { timeout: 10000 }); const html = await page.content(); await browser.close(); return cheerio.load(html); } catch (error) { console.log(`尝试 ${i + 1} 失败:`, error.message); if (i === maxRetries - 1) { throw error; } await new Promise(resolve => setTimeout(resolve, 2000)); } }}总结虽然 Cheerio 本身无法处理动态内容,但通过与 Puppeteer、Playwright 等浏览器自动化工具结合,可以有效地处理各种动态加载场景。关键在于:等待策略:使用 waitForSelector、waitForTimeout 等确保内容加载完成性能优化:禁用不必要的资源加载,复用浏览器实例错误处理:实现重试机制,处理网络异常混合使用:先用浏览器工具加载动态内容,再用 Cheerio 快速解析
阅读 0·2月22日 14:30