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

面试题手册

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

如何优化 Cheerio 的性能?有哪些性能优化技巧?

Cheerio 本身是一个轻量级的 HTML 解析器,性能已经非常出色,但在处理大量数据或复杂场景时,我们仍然可以通过多种方式进一步优化性能:1. 选择器性能优化使用具体的选择器// ❌ 慢:使用通配符const items = $('*').filter('.item');// ✅ 快:直接选择const items = $('.item');// ❌ 慢:多重后代选择器const items = $('div div div .item');// ✅ 快:更具体的选择器const items = $('.container .item');// ❌ 慢:使用复杂伪类const items = $('div:has(p):not(.hidden)');// ✅ 快:简化选择器const items = $('div.active');缓存选择器结果// ❌ 慢:重复查询for (let i = 0; i < 100; i++) { const title = $('.item').eq(i).find('.title').text();}// ✅ 快:缓存查询结果const $items = $('.item');for (let i = 0; i < $items.length; i++) { const title = $items.eq(i).find('.title').text();}使用 find() 代替层级选择器// ❌ 较慢const items = $('.container .item .title');// ✅ 更快const $container = $('.container');const items = $container.find('.item').find('.title');2. DOM 操作优化批量操作而非逐个操作// ❌ 慢:逐个添加元素for (let i = 0; i < 1000; i++) { $('.container').append(`<div class="item">${i}</div>`);}// ✅ 快:批量构建 HTMLlet html = '';for (let i = 0; i < 1000; i++) { html += `<div class="item">${i}</div>`;}$('.container').html(html);// ✅ 更快:使用数组 joinconst items = Array.from({ length: 1000 }, (_, i) => `<div class="item">${i}</div>`).join('');$('.container').html(items);减少重排和重绘// ❌ 慢:多次修改 DOM$('.item').addClass('active');$('.item').css('color', 'red');$('.item').attr('data-id', '123');// ✅ 快:一次性修改$('.item').addClass('active').css('color', 'red').attr('data-id', '123');使用文档片段(对于大量插入)// 对于大量 DOM 插入,先构建完整 HTML 再插入function buildLargeList(data) { const html = data.map(item => ` <li class="item" data-id="${item.id}"> <span class="title">${item.title}</span> <span class="price">${item.price}</span> </li> `).join(''); return cheerio.load(`<ul>${html}</ul>`);}3. 数据提取优化使用原生方法获取数据// ❌ 慢:使用 Cheerio 方法const texts = [];$('.item').each((i, el) => { texts.push($(el).text());});// ✅ 快:使用原生方法const texts = $('.item').map((i, el) => el.textContent).get();// ✅ 更快:直接遍历 DOM 元素const $items = $('.item');const texts = [];for (let i = 0; i < $items.length; i++) { texts.push($items[i].textContent);}优化 map() 和 each() 的使用// ❌ 慢:在 each 中创建新对象const data = [];$('.item').each((i, el) => { data.push({ title: $(el).find('.title').text(), price: $(el).find('.price').text() });});// ✅ 快:使用 map()const data = $('.item').map((i, el) => ({ title: $(el).find('.title').text(), price: $(el).find('.price').text()})).get();4. 内存管理优化及时释放大对象// 处理大文件时,分批处理function processLargeHtml(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) => ({ id: $(el).attr('data-id'), title: $(el).find('.title').text() })).get(); results.push(...batchData); // 及时清理 $batch = null; } return results;}避免内存泄漏// ❌ 可能导致内存泄漏let $ = cheerio.load(html);// ... 处理// 忘记清理 $// ✅ 及时清理function processHtml(html) { const $ = cheerio.load(html); const result = extractData($); // Cheerio 对象会自动被垃圾回收 return result;}5. 并发处理优化使用 Worker 线程处理大量数据const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');if (isMainThread) { // 主线程 async function processInParallel(htmlChunks) { const workers = htmlChunks.map(chunk => new Promise((resolve) => { const worker = new Worker(__filename, { workerData: chunk }); worker.on('message', resolve); }) ); return Promise.all(workers); }} else { // Worker 线程 const cheerio = require('cheerio'); const $ = cheerio.load(workerData); const result = extractData($); parentPort.postMessage(result);}批量处理 URLconst axios = require('axios');const cheerio = require('cheerio');async function batchScrape(urls, concurrency = 5) { const results = []; for (let i = 0; i < urls.length; i += concurrency) { const batch = urls.slice(i, i + concurrency); const batchResults = await Promise.all( batch.map(url => scrapeUrl(url)) ); results.push(...batchResults); } return results;}async function scrapeUrl(url) { const response = await axios.get(url); const $ = cheerio.load(response.data); return extractData($);}6. 配置优化使用合适的加载选项// ✅ 禁用不必要的功能以提高性能const $ = cheerio.load(html, { // 不解码 HTML 实体(如果不需要) decodeEntities: false, // 不包含空白节点 withDomLvl1: false, // 不规范化空白 normalizeWhitespace: false});XML 模式优化// 处理 XML 时使用 XML 模式const $ = cheerio.load(xml, { xmlMode: true, decodeEntities: false});7. 性能监控和测试性能测试工具function benchmark(fn, iterations = 1000) { const start = process.hrtime.bigint(); for (let i = 0; i < iterations; i++) { fn(); } const end = process.hrtime.bigint(); const duration = Number(end - start) / 1000000; // 转换为毫秒 return { total: duration, average: duration / iterations, perSecond: iterations / (duration / 1000) };}// 使用示例const result = benchmark(() => { const $ = cheerio.load(html); $('.item').text();}, 1000);console.log(`平均耗时: ${result.average}ms`);console.log(`每秒处理: ${result.perSecond} 次`);内存使用监控function getMemoryUsage() { const usage = process.memoryUsage(); return { rss: `${Math.round(usage.rss / 1024 / 1024)} MB`, heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)} MB`, heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)} MB` };}// 使用示例console.log('处理前:', getMemoryUsage());const result = processLargeHtml(html);console.log('处理后:', getMemoryUsage());8. 实际优化案例优化前async function scrapeSlow(urls) { const results = []; for (const url of urls) { const response = await axios.get(url); const $ = cheerio.load(response.data); $('.item').each((i, el) => { results.push({ title: $(el).find('.title').text(), price: $(el).find('.price').text(), description: $(el).find('.description').text() }); }); } return results;}优化后async function scrapeFast(urls) { // 并发请求 const responses = await Promise.all( urls.map(url => axios.get(url)) ); // 批量处理 const results = responses.flatMap(response => { const $ = cheerio.load(response.data); return $('.item').map((i, el) => ({ title: $(el).find('.title').text(), price: $(el).find('.price').text(), description: $(el).find('.description').text() })).get(); }); return results;}总结Cheerio 性能优化的关键点:选择器优化:使用具体、高效的选择器,缓存查询结果DOM 操作优化:批量操作,减少重排重绘数据提取优化:使用原生方法,优化 map/each内存管理:及时释放大对象,避免内存泄漏并发处理:合理使用并发,提高吞吐量配置优化:根据需求调整加载选项性能监控:定期测试和监控性能指标通过这些优化,Cheerio 可以处理数百万级别的 DOM 元素,保持出色的性能表现。
阅读 0·2月22日 14:30

Dubbo 框架的核心架构和特性是什么?Dubbo 如何实现服务治理?

Dubbo 是阿里巴巴开源的高性能 Java RPC 框架,广泛应用于微服务架构中:核心架构:1. 服务提供者(Provider)暴露服务的应用启动时向注册中心注册服务可以部署多个实例实现负载均衡2. 服务消费者(Consumer)调用远程服务的应用启动时从注册中心订阅服务通过代理调用远程服务3. 注册中心(Registry)服务注册与发现的核心组件常用实现:Zookeeper、Nacos、Redis负责维护服务列表和健康状态4. 监控中心(Monitor)统计服务调用次数和调用时间提供服务治理数据支持常用实现:Dubbo Admin、Prometheus5. 容器(Container)服务运行容器常用:Spring Container、Spring Boot核心特性:1. 远程调用支持多种协议:Dubbo、RMI、Hessian、HTTP、Webservice、Thrift、REST默认使用 Dubbo 协议(基于 Netty)支持同步和异步调用2. 集群容错Failover:失败自动切换,默认策略Failfast:快速失败,只发起一次调用Failsafe:失败安全,出现异常时忽略Failback:失败自动恢复,后台记录失败请求Forking:并行调用,只要一个成功即返回Broadcast:广播调用,所有调用都成功才算成功3. 负载均衡Random:随机,按权重设置随机概率RoundRobin:轮询,按公约后的权重设置轮询比率LeastActive:最少活跃调用数ConsistentHash:一致性 Hash,相同参数的请求总是发到同一提供者4. 服务降级Mock 数据返回 null抛出指定异常5. 服务限流并发数限制QPS 限制6. 服务路由条件路由标签路由脚本路由7. 配置中心动态配置配置版本管理配置推送使用示例:服务提供者:@Servicepublic class UserServiceImpl implements UserService { @Override public User getUserById(Long id) { return new User(id, "张三"); }}// 配置<dubbo:service interface="com.example.UserService" ref="userService"/>服务消费者:// 配置<dubbo:reference interface="com.example.UserService" id="userService"/>// 使用@Autowiredprivate UserService userService;public void test() { User user = userService.getUserById(1L);}优势:高性能:基于 Netty,支持长连接易用性:与 Spring 深度集成可扩展:支持多种协议和负载均衡策略服务治理:完善的服务治理功能社区活跃:阿里巴巴和社区持续维护适用场景:Java 微服务架构内部服务调用高并发场景需要完善服务治理的系统
阅读 0·2月22日 14:08

Gradle 有哪些常用命令?如何优化构建性能?

Gradle 提供了丰富的命令行工具和选项,熟练掌握这些命令可以大大提高开发效率。以下是 Gradle 常用命令的详细说明:基本命令查看帮助# 查看帮助信息./gradlew help# 查看任务帮助./gradlew help --task build# 查看所有可用任务./gradlew tasks# 查看特定组的任务./gradlew tasks --group=build# 查看所有任务(包括隐藏任务)./gradlew tasks --all查看项目信息# 查看项目信息./gradlew projects# 查看项目属性./gradlew properties# 查看依赖./gradlew dependencies# 查看特定配置的依赖./gradlew dependencies --configuration implementation# 查看特定项目的依赖./gradlew :app:dependencies构建命令基本构建# 构建项目./gradlew build# 清理并构建./gradlew clean build# 跳过测试构建./gradlew build -x test# 只运行测试./gradlew test# 运行特定测试类./gradlew test --tests MyTest# 运行特定测试方法./gradlew test --tests MyTest.testMethod构建变体# 构建特定变体./gradlew assembleDebug./gradlew assembleRelease# 构建所有变体./gradlew assemble# 构建特定模块./gradlew :module1:build./gradlew :module2:build任务执行执行单个任务# 执行特定任务./gradlew clean# 执行多个任务./gradlew clean build test# 执行任务并传递参数./gradlew build -Pprofile=production任务依赖# 查看任务依赖图./gradlew build --dry-run# 查看任务执行顺序./gradlew build --console=plain# 强制重新执行任务./gradlew build --rerun-tasks性能优化命令并行构建# 启用并行构建./gradlew build --parallel# 指定并行线程数./gradlew build --parallel --max-workers=4# 配置按需构建./gradlew build --configure-on-demand构建缓存# 启用构建缓存./gradlew build --build-cache# 清理构建缓存./gradlew cleanBuildCache# 使用离线模式./gradlew build --offline配置缓存# 启用配置缓存./gradlew build --configuration-cache# 清理配置缓存./gradlew cleanConfigurationCache调试和诊断详细输出# 显示详细日志./gradlew build --info# 显示调试日志./gradlew build --debug# 显示堆栈跟踪./gradlew build --stacktrace# 显示完整堆栈跟踪./gradlew build --full-stacktrace性能分析# 生成构建报告./gradlew build --scan# 生成性能报告./gradlew build --profile# 查看构建时间./gradlew build --console=plain依赖分析# 查看依赖树./gradlew :app:dependencies# 查看特定依赖的详细信息./gradlew dependencyInsight --dependency spring-boot-starter-web# 查找依赖冲突./gradlew dependencies | grep -i conflict持续构建文件监控# 启用持续构建./gradlew build --continuous# 指定监控间隔(秒)./gradlew build --continuous --interval=5# 持续测试./gradlew test --continuous自定义任务执行传递参数# 传递项目属性./gradlew build -Pversion=1.0.0# 传递系统属性./gradlew build -Dspring.profiles.active=production# 传递 JVM 参数./gradlew build -Dorg.gradle.jvmargs="-Xmx2048m"# 传递多个参数./gradlew build -Penv=prod -Dlog.level=debug条件执行# 只在特定条件下执行任务./gradlew build -PenableFeature=true# 使用环境变量ENV=production ./gradlew build插件管理查看插件# 查看已应用的插件./gradlew plugins# 查看插件详情./gradlew plugins --detail更新插件# 更新依赖./gradlew dependencyUpdates# 更新 Wrapper./gradlew wrapper --gradle-version=8.0多项目构建项目选择# 构建特定项目./gradlew :app:build# 构建多个项目./gradlew :app:build :library:build# 构建所有项目./gradlew build# 排除特定项目./gradlew build -x :module1:build项目依赖# 查看项目依赖关系./gradlew projects# 查看特定项目的依赖./gradlew :app:dependenciesAndroid 特定命令Android 构建# 构建 Debug 版本./gradlew assembleDebug# 构建 Release 版本./gradlew assembleRelease# 安装到设备./gradlew installDebug./gradlew installRelease# 卸载应用./gradlew uninstallDebug./gradlew uninstallReleaseAndroid 测试# 运行单元测试./gradlew test# 运行仪器测试./gradlew connectedAndroidTest# 运行特定测试./gradlew test --tests com.example.MyTestAndroid 其他命令# 生成 Lint 报告./gradlew lint# 生成签名 APK./gradlew assembleRelease# 生成 Bundle./gradlew bundleRelease发布和部署发布到仓库# 发布到本地仓库./gradlew publishToMavenLocal# 发布到远程仓库./gradlew publish# 发布特定模块./gradlew :app:publish版本管理# 查看版本./gradlew --version# 使用特定版本./gradlew build --gradle-version=8.0常用选项通用选项# 指定设置文件./gradlew build --settings-file=custom-settings.gradle# 指定构建文件./gradlew build --build-file=custom-build.gradle# 指定 Gradle 用户主目录./gradlew build --gradle-user-home=/custom/path# 指定项目目录./gradlew build --project-dir=/custom/project输出控制# 控制台输出模式./gradlew build --console=plain./gradlew build --console=auto./gradlew build --console=rich# 颜色输出./gradlew build --color=always./gradlew build --color=never./gradlew build --color=auto# 安静模式./gradlew build --quiet故障排除清理和重试# 清理构建./gradlew clean# 清理所有缓存./gradlew clean cleanBuildCache cleanConfigurationCache# 强制重新下载依赖./gradlew build --refresh-dependencies# 重新执行所有任务./gradlew build --rerun-tasks网络问题# 使用离线模式./gradlew build --offline# 配置代理./gradlew build -Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080最佳实践1. 使用别名# 在 shell 中创建别名alias gb='./gradlew build'alias gt='./gradlew test'alias gc='./gradlew clean'2. 使用脚本# 创建构建脚本#!/bin/bash./gradlew clean build --parallel --build-cache3. 使用配置文件# gradle.propertiesorg.gradle.parallel=trueorg.gradle.caching=trueorg.gradle.configureondemand=trueorg.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m4. 使用 Gradle Daemon# 启用 Gradle Daemon./gradlew build --daemon# 停止所有 Daemon./gradlew --stop# 查看运行中的 Daemon./gradlew --status常见问题解决1. 内存不足# 增加 JVM 内存./gradlew build -Dorg.gradle.jvmargs="-Xmx4096m -XX:MaxMetaspaceSize=1024m"2. 构建缓慢# 启用并行构建和缓存./gradlew build --parallel --build-cache --configuration-cache3. 依赖冲突# 查看依赖树./gradlew dependencies# 使用依赖分析工具./gradlew dependencyInsight --dependency <dependency-name>4. 任务不执行# 强制重新执行任务./gradlew build --rerun-tasks# 查看任务状态./gradlew build --info
阅读 0·2月22日 14:08

Gradle 如何实现构建变体和多环境配置?

Gradle 支持多种构建变体和产品风味,这对于 Android 开发和多环境部署尤为重要。以下是 Gradle 构建变体的详细说明:构建变体概念构建变体(Build Variants)是 Gradle 中用于生成不同版本应用程序的机制,它允许开发者基于不同的配置生成多个构建输出。Android 构建变体基本配置android { // 构建类型 buildTypes { debug { applicationIdSuffix ".debug" versionNameSuffix "-debug" debuggable true minifyEnabled false } release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } } // 产品风味 flavorDimensions "version", "environment" productFlavors { free { dimension "version" applicationId "com.example.app.free" versionName "1.0-free" } paid { dimension "version" applicationId "com.example.app.paid" versionName "1.0-paid" } dev { dimension "environment" buildConfigField "String", "API_URL", "\"https://dev.api.example.com\"" resValue "string", "app_name", "My App (Dev)" } staging { dimension "environment" buildConfigField "String", "API_URL", "\"https://staging.api.example.com\"" resValue "string", "app_name", "My App (Staging)" } prod { dimension "environment" buildConfigField "String", "API_URL", "\"https://api.example.com\"" resValue "string", "app_name", "My App" } }}构建变体组合上面的配置会生成以下构建变体:freeDevDebugfreeDevReleasefreeStagingDebugfreeStagingReleasefreeProdDebugfreeProdReleasepaidDevDebugpaidDevReleasepaidStagingDebugpaidStagingReleasepaidProdDebugpaidProdReleaseJava/Kotlin 构建变体使用源集// build.gradlesourceSets { main { java { srcDirs 'src/main/java' } resources { srcDirs 'src/main/resources' } } // 自定义源集 custom { java { srcDirs 'src/custom/java' } resources { srcDirs 'src/custom/resources' } }}// 为特定构建类型配置源集android.sourceSets { debug { java.srcDirs 'src/debug/java' res.srcDirs 'src/debug/res' } release { java.srcDirs 'src/release/java' res.srcDirs 'src/release/res' }}使用任务变体// 为不同环境创建任务tasks.register('buildDev') { group = 'build' description = 'Build for development environment' doLast { // 开发环境构建逻辑 }}tasks.register('buildStaging') { group = 'build' description = 'Build for staging environment' doLast { // 预发布环境构建逻辑 }}tasks.register('buildProd') { group = 'build' description = 'Build for production environment' doLast { // 生产环境构建逻辑 }}多环境配置使用配置文件// build.gradleext { environments = [ dev: [ apiUrl: 'https://dev.api.example.com', dbUrl: 'jdbc:mysql://dev-db.example.com:3306/mydb', enableDebug: true ], staging: [ apiUrl: 'https://staging.api.example.com', dbUrl: 'jdbc:mysql://staging-db.example.com:3306/mydb', enableDebug: false ], prod: [ apiUrl: 'https://api.example.com', dbUrl: 'jdbc:mysql://prod-db.example.com:3306/mydb', enableDebug: false ] ]}// 根据环境变量选择配置def environment = project.hasProperty('env') ? project.env : 'dev'def config = environments[environment]tasks.register('buildWithConfig') { doLast { println "Building for environment: ${environment}" println "API URL: ${config.apiUrl}" println "DB URL: ${config.dbUrl}" println "Debug enabled: ${config.enableDebug}" }}使用属性文件// 创建配置任务tasks.register('generateConfig') { def env = project.hasProperty('env') ? project.env : 'dev' def configFile = file("config/${env}.properties") inputs.file configFile outputs.dir('build/config') doLast { copy { from configFile into 'build/config' rename { 'application.properties' } } }}// 依赖配置任务tasks.named('processResources') { dependsOn 'generateConfig' from 'build/config'}动态变体生成基于输入生成变体// build.gradledef variants = ['variant1', 'variant2', 'variant3']variants.each { variant -> tasks.register("build${variant.capitalize()}") { group = 'build' description = "Build ${variant}" doLast { println "Building ${variant}" // 变体特定的构建逻辑 } }}// 创建聚合任务tasks.register('buildAllVariants') { group = 'build' description = 'Build all variants' dependsOn variants.collect { "build${it.capitalize()}" }}基于配置文件生成变体// variants.json[ { "name": "variant1", "version": "1.0.0", "features": ["feature1", "feature2"] }, { "name": "variant2", "version": "2.0.0", "features": ["feature1", "feature3"] }]// build.gradleimport groovy.json.JsonSlurperdef variantsFile = file('variants.json')def variants = new JsonSlurper().parse(variantsFile)variants.each { variant -> tasks.register("build${variant.name.capitalize()}") { group = 'build' description = "Build ${variant.name} v${variant.version}" doLast { println "Building ${variant.name} version ${variant.version}" println "Features: ${variant.features.join(', ')}" } }}变体特定依赖为不同变体配置依赖// Android 项目dependencies { implementation 'androidx.core:core-ktx:1.9.0' // Debug 特定依赖 debugImplementation 'com.facebook.stetho:stetho:1.6.0' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' // Release 特定依赖 releaseImplementation 'com.squareup.okhttp3:okhttp:4.10.0' // 产品风味特定依赖 freeImplementation 'com.google.android.gms:play-services-ads:21.3.0' paidImplementation 'com.example:premium-features:1.0.0' // 构建变体特定依赖 freeDevImplementation 'com.example:dev-tools:1.0.0' paidProdImplementation 'com.example:prod-analytics:1.0.0'}Java 项目变体依赖configurations { devImplementation stagingImplementation prodImplementation}dependencies { implementation 'org.springframework.boot:spring-boot-starter-web:3.0.0' devImplementation 'org.springframework.boot:spring-boot-devtools:3.0.0' stagingImplementation 'org.springframework.boot:spring-boot-starter-actuator:3.0.0' prodImplementation 'org.springframework.boot:spring-boot-starter-security:3.0.0'}// 为不同环境创建任务tasks.register('runDev', JavaExec) { group = 'application' classpath = sourceSets.main.runtimeClasspath + configurations.devImplementation mainClass = 'com.example.Application' args = ['--spring.profiles.active=dev']}tasks.register('runStaging', JavaExec) { group = 'application' classpath = sourceSets.main.runtimeClasspath + configurations.stagingImplementation mainClass = 'com.example.Application' args = ['--spring.profiles.active=staging']}tasks.register('runProd', JavaExec) { group = 'application' classpath = sourceSets.main.runtimeClasspath + configurations.prodImplementation mainClass = 'com.example.Application' args = ['--spring.profiles.active=prod']}变体特定资源Android 资源变体app/├── src/│ ├── main/│ │ ├── res/│ │ │ ├── values/│ │ │ │ └── strings.xml│ │ │ └── drawable/│ │ │ └── icon.png│ ├── debug/│ │ └── res/│ │ └── values/│ │ └── strings.xml│ ├── free/│ │ └── res/│ │ └── values/│ │ └── strings.xml│ └── paid/│ └── res/│ └── values/│ └── strings.xmlJava 资源变体sourceSets { main { resources { srcDirs 'src/main/resources' } } dev { resources { srcDirs 'src/dev/resources' } } staging { resources { srcDirs 'src/staging/resources' } } prod { resources { srcDirs 'src/prod/resources' } }}// 为不同环境创建 JARtasks.register('devJar', Jar) { group = 'build' from sourceSets.main.output from sourceSets.dev.output archiveFileName = 'app-dev.jar'}tasks.register('stagingJar', Jar) { group = 'build' from sourceSets.main.output from sourceSets.staging.output archiveFileName = 'app-staging.jar'}tasks.register('prodJar', Jar) { group = 'build' from sourceSets.main.output from sourceSets.prod.output archiveFileName = 'app-prod.jar'}最佳实践1. 合理规划变体维度// 避免过多的变体组合flavorDimensions "version" // 只使用一个维度productFlavors { free { dimension "version" } paid { dimension "version" }}2. 使用共享配置// 定义共享配置def commonConfig = { versionCode 1 versionName "1.0.0" minSdkVersion 21 targetSdkVersion 33}android { defaultConfig commonConfig}3. 使用变体过滤器android { variantFilter { variant -> def names = variant.flavors*.name if (names.contains("paid") && names.contains("dev")) { variant.setIgnore(true) // 忽略 paidDev 变体 } }}4. 使用构建变体特定的任务android.applicationVariants.all { variant -> def variantName = variant.name.capitalize() tasks.register("process${variantName}Resources") { doLast { println "Processing resources for ${variant.name}" } }}5. 使用版本目录管理变体// gradle/libs.versions.toml[versions]spring-boot = "3.0.0"[libraries]spring-boot-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }// 在 build.gradle 中使用dependencies { implementation libs.spring.boot.web}
阅读 0·2月22日 14:08

gRPC 的核心特性和优势是什么?为什么选择 gRPC 而不是其他 RPC 框架?

gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 和 Protobuf 构建,具有以下核心特性和优势:核心特性:1. 基于 HTTP/2多路复用:单个 TCP 连接可以同时发送多个请求,减少连接开销二进制分帧:比 HTTP/1.x 的文本格式更高效头部压缩:使用 HPACK 算法压缩头部,减少传输数据量服务端推送:支持服务端主动推送数据流式传输:支持单向流和双向流2. 基于 Protobuf高效序列化:二进制格式,序列化/反序列化速度快强类型:通过 .proto 文件定义接口,编译时类型检查跨语言:支持 10+ 种编程语言向后兼容:字段编号机制保证版本兼容性3. 四种服务模式一元 RPC(Unary):客户端发送一个请求,服务端返回一个响应服务端流式 RPC(Server Streaming):客户端发送一个请求,服务端返回流式响应客户端流式 RPC(Client Streaming):客户端发送流式请求,服务端返回一个响应双向流式 RPC(Bidirectional Streaming):客户端和服务端都可以发送流式数据优势:1. 高性能HTTP/2 多路复用减少连接开销Protobuf 二进制序列化效率高支持流式传输,适合大数据场景2. 低延迟二进制协议减少解析时间多路复用避免队头阻塞连接复用减少握手开销3. 跨语言支持自动生成多种语言的客户端和服务端代码统一的接口定义语言(IDL)无缝集成不同语言的服务4. 强类型和代码生成编译时类型检查,减少运行时错误自动生成代码,提高开发效率IDE 支持良好,开发体验佳5. 流式通信支持实时数据传输适合聊天、推送、实时监控等场景减少请求-响应的往返次数6. 双向流支持客户端和服务端可以同时发送数据适合实时协作、游戏等场景减少连接建立的开销7. 生态系统完善拦截器机制(Interceptor)负载均衡服务发现链路追踪集成适用场景:微服务内部通信实时数据流处理跨语言服务调用高性能要求的场景需要流式通信的应用代码示例:// 定义服务service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} rpc SayHelloStream (HelloRequest) returns (stream HelloReply) {}}// 定义消息message HelloRequest { string name = 1;}message HelloReply { string message = 1;}
阅读 0·2月22日 14:08