服务端面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 00:52

Cypress 和 Selenium 有什么区别?何时选择 Cypress?

核心区别在架构:Cypress 运行在与应用同源的浏览器内,通过 Chrome DevTools Protocol 直接操作 DOM,内置自动等待和重试机制;Selenium 通过外部 WebDriver 进程与浏览器通信,需显式编写等待逻辑。这意味着 Cypress 调试体验远优于 Selenium(可视化 Test Runner、时间旅行),且代码更简洁,但仅支持 Chromium 内核和 JavaScript;Selenium 跨浏览器覆盖全面(Chrome/Firefox/Safari),支持多语言(Java/Python/C#),适合需要兼容性测试的团队。选择 Cypress 的场景:前端 SPA 项目为主、团队用 JavaScript、追求快速反馈和低维护成本。选 Selenium 的场景:必须覆盖多浏览器、团队非 JS 技术栈、需测试非 Web 应用。追问Cypress 的同源架构为什么无法测试跨域场景?有什么变通方案?Selenium 的显式等待(WebDriverWait)和隐式等待(implicit wait)有什么区别?各自的风险是什么?Cypress 的 cy.intercept() 如何模拟后端响应?与 Selenium 的 Mock Server 方案相比优劣如何?大型项目中 Cypress 测试执行变慢,如何优化?Playwright 与 Cypress 相比有哪些改进?是否正在取代 Cypress?写段代码// Cypress: 自动等待,无需 sleepcy.visit('/login');cy.get('#user').type('admin');cy.get('#pass').type('1234');cy.get('#submit').click();cy.url().should('include', '/dashboard');// Selenium (Python): 必须显式等待from selenium.webdriver.support.ui import WebDriverWaitelem = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, 'submit')))elem.click()
服务端阅读 05月29日 00:51

Elasticsearch 的 suggest 功能如何实现自动补全?

ES 提供四种 suggester:Completion Suggester 基于内存 FST(有限状态转换器)做前缀匹配,延迟极低,适合自动补全;Term Suggester 基于编辑距离做拼写纠错;Phrase Suggester 在 Term 基础上加 n-gram 语言模型优化整句建议;Context Suggester 为 Completion 增加分类/地理上下文过滤。自动补全场景用 Completion Suggester:索引时将建议词存入 completion 类型字段构建 FST,查询时用 prefix 参数匹配,毫秒级返回。追问Completion Suggester 为什么快?它将建议词构建为 FST 结构常驻内存,前缀匹配是 O(k) 复杂度(k 为前缀长度),不涉及倒排索引扫描和评分计算,所以延迟在毫秒级。Completion 和 searchasyou_type 有什么区别?Completion 是独立建议通道,返回建议词而非文档;searchasyoutype 是特殊 text 子类型,本质还是全文检索返回文档。Completion 更适合输入框补全,searchasyoutype 更适合边输边搜文档内容。拼写纠错用哪个?Term Suggester。它基于倒排索引中的词项做编辑距离计算,返回相似词建议。需要指定 suggest_mode:missing(仅缺词建议)、popular(热门词优先)、always(始终建议)。如何给建议词加权重?在 completion 字段中设置 weight 属性,权重高的建议优先返回。常用于热门搜索词提权:"suggest": {"input": "laptop", "weight": 100}。FST 内存开销大吗?FST 是高度压缩的有向无环图,百万级建议词通常只占几十 MB。但建议词总量达亿级时需评估内存,可用 context 过滤减少单次匹配范围。写段代码PUT /search-suggest{ "mappings": { "properties": { "title": { "type": "keyword" }, "suggest": { "type": "completion", "analyzer": "standard" } } }}GET /search-suggest/_search{ "suggest": { "auto": { "prefix": "elas", "completion": { "field": "suggest", "size": 5 } } }}
服务端阅读 05月29日 00:51

DevSecOps 的关键实践有哪些?如何将安全左移?

DevSecOps 是将安全内嵌到 DevOps 全流程的实践,核心理念是"安全左移"——在编码阶段而非上线后才做安全检查。关键实践包括:在 CI 流水线中集成 SAST(静态代码扫描)和 SCA(依赖漏洞扫描),构建阶段做容器镜像扫描(Trivy),部署前用 DAST 对运行时做动态测试,运行时通过 Falco 做入侵检测。此外还包括用 Vault 管理密钥与凭证轮换、RBAC 最小权限控制、基础设施即代码的安全扫描(tfsec),以及用 SLSA 框架保障软件供应链完整性。本质是把安全从"门卫"变成"内嵌检查点"。追问SAST 和 DAST 分别能发现什么类型的漏洞?为什么两者必须互补?容器镜像扫描应该扫基础镜像还是应用层?Trivy 的工作原理是什么?HashiCorp Vault 如何实现密钥自动轮换而不中断服务?SLSA 框架的四个级别分别保证什么?达到 Level 3 需要哪些前提?如何处理安全扫描的大量误报而不拖慢 CI 流水线?写段代码# CI 流水线内嵌安全扫描stages: - security - testsast: stage: security script: semgrep ci --config autodep-scan: stage: security script: snyk test --severity-threshold=highcontainer-scan: stage: test script: - docker build -t app:$CI_SHA . - trivy image --exit-code 1 app:$CI_SHA
服务端阅读 05月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 版本。写段代码// 表格转对象数组插件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(); };};
服务端阅读 05月29日 00:51

Elasticsearch 如何实现地理空间搜索?

ES 通过 geopoint 和 geoshape 两种类型支持地理搜索。geopoint 存储经纬度坐标点,支持 geodistance(圆形范围)、geoboundingbox(矩形范围)、geopolygon(多边形范围)查询;geoshape 存储复杂几何形状(线、多边形),支持相交、包含等空间关系查询。底层使用 geohash 编码将二维坐标映射为一维字符串,利用 BKD tree 索引加速范围检索。查询时先通过 geohash 前缀粗筛,再计算精确距离过滤。追问geopoint 和 geoshape 怎么选?存储门店位置等点数据用 geopoint,存储配送区域等面数据用 geoshape。geopoint 查询更快,geoshape 支持更复杂的空间关系但索引开销更大。geo_distance 查询性能如何优化?先用 geoboundingbox 缩小候选集,再在结果上做精确距离计算。也可设置 geopoint 的 geohashprecision 控制索引精度。经纬度顺序容易搞混怎么办?ES 的 geo_point 支持多种格式:字符串 "lat,lon"、数组 [lon,lat](GeoJSON 标准)、对象 {lat,lon}。数组格式是 lon 在前,容易出错,推荐用对象格式避免歧义。geohash 精度怎么选?精度越高定位越准但索引越大。常见选择:1km 精度用 geohash_precision=5(约 4.9km 边长),100m 用 6,10m 用 7。业务精度需求决定精度设置。geo 查询能和普通查询组合吗?可以,放在 bool query 的 filter 子句中。geo 查询不计算评分,适合做过滤条件配合全文检索使用。写段代码PUT /stores{ "mappings": { "properties": { "name": { "type": "keyword" }, "location": { "type": "geo_point" } } }}GET /stores/_search{ "query": { "bool": { "filter": { "geo_distance": { "distance": "5km", "location": { "lat": 39.9, "lon": 116.4 } } } } }}
服务端阅读 05月29日 00:51

持续集成、持续交付和持续部署有什么区别?

三者是递进关系:持续集成(CI)解决"代码能否合入"的问题——每次提交自动触发构建和测试,确保主分支始终可构建;持续交付(Continuous Delivery)解决"代码能否随时上线"的问题——在 CI 基础上自动化部署到预发布环境,但推送到生产环境需要人工审批门控;持续部署(Continuous Deployment)解决"代码能否自动上线"的问题——通过所有测试的变更直接部署到生产,无需人工干预。核心区别就在一个门:生产环境部署前是否有手动批准环节。选哪个取决于业务风险容忍度和测试覆盖成熟度。追问CI 中"每次提交都触发构建"在高频提交时如何避免流水线排队?持续交付中手动审批门控应该设在哪个环节?审批人需要关注什么?从持续交付升级到持续部署,测试覆盖率要达到什么水平才安全?蓝绿部署和金丝雀发布在持续部署中各起什么作用?CI/CD 流水线中安全扫描(SAST/DAST)应该放在哪个阶段?写段代码# GitHub Actions:CI + 持续交付流水线on: [push]jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci && npm test deploy-staging: needs: test runs-on: ubuntu-latest environment: staging # 自动部署到预发布 steps: - run: npx deploy-to staging
服务端阅读 05月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 拿不到但浏览器能看到,就是动态渲染。写段代码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();}
服务端阅读 05月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 和事件绑定。写段代码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();
服务端阅读 05月29日 00:51

Elasticsearch 的 fielddata 和 doc_values 有什么区别?

fielddata 是基于 JVM 堆内存的倒排索引补充结构,仅在搜索时按需加载到内存,专用于 text 字段的聚合和排序,内存消耗不可控,易引发 OOM;docvalues 是基于磁盘的列式存储,索引时随文档写入持久化,默认对 keyword 和数值类型启用,不占堆内存,ES 5.x 后成为默认方案。核心区别:存储位置(堆 vs 磁盘)、加载时机(搜索时 vs 索引时)、内存风险(高 vs 低)、适用类型(text vs keyword/numeric)。生产环境中应优先用 docvalues,若字段不需聚合/排序可关闭 doc_values 节省磁盘。追问为什么 fielddata 容易 OOM?fielddata 将字段的全部唯一词项加载到 JVM 堆,高基数字段(如 UUID)可能占用数 GB。且被 LRU 缓存持有,不会主动释放,直到触发 circuit breaker。text 字段需要聚合怎么办?推荐用 multi-fields:主字段 text 做全文检索,子字段 keyword 启用 doc_values 做聚合。避免在 text 上开启 fielddata: true。关闭 doc_values 有什么影响?该字段将无法用于聚合、排序和脚本访问。如果字段只做全文检索不需要这些操作,关闭可节省约 10-15% 磁盘空间。doc_values 的列式存储如何加速聚合?列式存储按字段值连续排列,对同一字段的遍历只需顺序读磁盘,CPU 缓存命中率高。行式存储需跳行读取,I/O 散乱。ES 7.x 后还有必要了解 fielddata 吗?需要。排查旧索引的内存问题时仍需检查 fielddata 占用,GET _nodes/stats/indices?fielddata 可定位高消耗字段。写段代码PUT /products{ "mappings": { "properties": { "title": { "type": "text", "fields": { "keyword": { "type": "keyword", "doc_values": true } } }, "status": { "type": "keyword", "doc_values": true } } }}
服务端阅读 05月29日 00:50

DevOps 的核心概念和关键原则是什么?

DevOps 不是工具或职位,而是一种将开发与运维深度融合的文化实践。其核心可用 CAMLS 概括:Culture(打破部门壁垒、共担责任)、Automation(构建/测试/部署/监控全链路自动化)、Measurement(用 MTTR、部署频率等指标驱动改进)、Sharing(跨团队知识共享与无指责复盘)。关键原则包括基础设施即代码(IaC)保证环境一致性、CI/CD 流水线实现快速可靠交付、监控与可观测性保障生产稳定性,以及通过 blameless postmortem 从故障中学习而非追责。追问DevOps 中的"无指责复盘"(blameless postmortem)怎么做?如何避免流于形式?基础设施即代码(IaC)和传统运维脚本有什么本质区别?Terraform 和 Ansible 各自适用什么场景?如何度量 DevOps 转型的成效?DORA 四项关键指标分别是什么?小团队没有专职运维,如何落地 DevOps 实践?监控(Monitoring)和可观测性(Observability)有什么区别?写段代码# GitLab CI/CD 最小流水线示例stages: - test - deploytest: stage: test script: - npm install && npm testdeploy: stage: deploy script: - npx netlify deploy --prod only: - main
服务端阅读 05月29日 00:50

Cheerio 中如何高效使用选择器提取数据?

Cheerio 支持 CSS 选择器 + jQuery 扩展伪类,核心选择器分四类:基本(元素/ID/类)、层级(后代/子元素/兄弟)、属性(= ^= $= *=)、伪类(:first/:eq(n)/:contains()/:has())。高效用法的关键是缩小搜索范围:先选父容器再 .find(),缓存 $ 变量避免重复查找,用具体选择器替代通配符。追问.find() 和层级选择器 $('div p') 哪个更快?性能差异极小,但 .find() 可读性更好且支持链式调用,推荐优先使用。已缓存的父元素上 .find() 还能跳过全局搜索。如何获取元素的文本、属性和 HTML?.text() 取文本(含子元素文本拼接),.attr('href') 取属性值,.html() 取内部 HTML,.val() 取表单值。注意 .text() 会递归合并所有子节点文本。.each() 和 .map() 有什么区别?.each() 遍历不返回值(适合副作用操作),.map() 返回数组(适合数据提取)。.map() 需链式调用 .get() 才能得到原生数组。:contains() 和 .filter() 如何选择?:contains('text') 在选择器阶段过滤,写法简洁但不支持正则;.filter(fn) 在回调中可写任意判断逻辑,更灵活。写段代码const $ = cheerio.load(html);const $list = $('#article-list');const titles = $list.find('.title').map((i, el) => $(el).text().trim()).get();
服务端阅读 05月29日 00:50

Elasticsearch 如何优化写入性能?

核心思路是减少刷新频率、批量提交、降低副本开销。具体操作:将 refreshinterval 设为 -1 禁用自动刷新,写入完成后手动 refresh;使用 Bulk API 批量提交文档(建议 5-15MB 一批);写入期间将 numberofreplicas 设为 0,写完后恢复;调大 translog.flushthreshold_size 减少 flush 次数;合理路由使热点数据集中写入少数分片,避免跨节点协调开销。追问bulk 请求多大合适?建议单次 bulk 请求体 5-15MB,文档数不超 10000。过大易触发 GC 甚至 OOM,过小则网络开销占比高。用 BulkProcessor 可自动攒批提交。refresh_interval=-1 写完忘改回来怎么办?新写入文档对搜索不可见,但不丢数据。生产中可在索引模板中设置正常值,仅写入任务启动时临时覆盖。副本数为 0 风险多大?单节点故障会丢分片数据。建议仅在大批量初始导入时使用,写完立即恢复副本并等待 allocation 完成。translog 配置怎么调?将 translog.durability 改为 async,translog.syncinterval 设为 30s,flushthreshold_size 从默认 512MB 调到 1GB,减少磁盘 fsync 次数。冷热数据如何隔离写入?用 ILM 策略将新数据写入热节点(SSD),rollover 后迁移到冷节点(HDD)。hot 阶段设短 refresh_interval,cold 阶段恢复默认。写段代码PUT /logs-write{ "settings": { "refresh_interval": "-1", "number_of_replicas": 0, "translog": { "durability": "async", "sync_interval": "30s" } }}
服务端阅读 05月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 },但后者会把换行也合并成空格,按需选择。写段代码// 正确处理 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);
服务端阅读 05月29日 00:50

Cheerio 加载 HTML 有哪几种方式?

核心方法是 cheerio.load(html, options),支持三种输入源:HTML 字符串、fs.readFileSync 读取的文件内容、Buffer。第二个参数控制解析行为:xmlMode 用于 XML 文档,decodeEntities 控制是否解码 HTML 实体(如 & → &),withDomLvl1 决定是否遵循 DOM Level1 规范。对大文件可用流式读取拼接后再 load。追问xmlMode 什么时候必须开启?解析 XML/RSS/Atom feed 时必须开启,否则标签大小写会被统一转小写,XML 的自闭合标签也会被错误处理。decodeEntities 设为 false 有什么用?保留原始 HTML 实体不解码,适合需要原样输出 HTML 的场景,如内容编辑器。默认 true 会把 & 转成 &。能直接加载 URL 吗?不能。Cheerio 是纯解析库,没有网络能力。需先用 axios/fetch 获取 HTML 字符串,再传入 load()。cheerio.load() 返回的 $ 是什么?返回一个函数,既可当选择器 $(selector) 使用,也挂载了 .html()、.xml() 等静态方法,本质是包装后的 DOM 操作入口。Buffer 输入和字符串输入有区别吗?功能一致,Buffer 会被内部自动转字符串。用 Buffer 可避免编码问题,尤其处理非 UTF-8 内容时更安全。写段代码const cheerio = require('cheerio');const $ = cheerio.load('<root><item>值</item></root>', { xmlMode: true, decodeEntities: false});console.log($('item').text()); // 值
服务端阅读 05月29日 00:50

如何用 Cheerio 实现网页爬虫和数据抓取?

核心流程三步走:用 axios/fetch 获取 HTML → cheerio.load() 加载 → 选择器提取数据。Cheerio 只负责解析,HTTP 请求需搭配其他库。关键要注意设置 User-Agent、处理相对 URL 转绝对路径、以及加入请求延迟避免被封禁。追问如何处理分页爬取?循环拼接页码参数逐页请求,每页之间加 1-2 秒延迟,收集数据后统一存储。注意检测是否有下一页(如"下一页"按钮或总页数标识)来决定终止条件。爬取的数据如何清洗?用 .trim() 去空白,正则移除 HTML 标签和特殊字符,parseFloat/parseInt 转数字类型,过滤空值和无效条目。如何实现请求重试机制?封装 fetchWithRetry 函数,失败后指数退避等待重试(1s→2s→4s),超过最大次数抛出异常。并发爬取如何控制?用 p-limit 等并发控制库限制同时请求数,或手动实现队列,避免瞬间大量请求导致 IP 被封。写段代码const axios = require('axios');const cheerio = require('cheerio');async function scrape(url) { const { data } = await axios.get(url); const $ = cheerio.load(data); const items = []; $('.item').each((i, el) => { items.push({ title: $(el).find('h2').text().trim() }); }); return items;}
服务端阅读 05月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 可能要几分钟。写段代码// 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());});
服务端阅读 05月29日 00:50

Cheerio 和 jQuery 有什么区别?

Cheerio 是 Node.js 端的 HTML 解析库,实现了 jQuery 核心 API,但剥离了浏览器依赖——不渲染 DOM、不处理 CSS 布局、不执行 JavaScript,因此比 jQuery 轻量且解析速度快 8 倍以上。jQuery 运行在浏览器中操作真实 DOM,Cheerio 则在服务端解析 HTML 字符串构建虚拟 DOM 树,只做数据提取与结构操作。追问Cheerio 为什么比 jsdom 快?jsdom 会构建完整的浏览器环境(CSSOM、布局计算等),Cheerio 仅构建 DOM 树,跳过了渲染管线,所以解析速度快一个数量级。Cheerio 能替代 jQuery 做前端开发吗?不能。Cheerio 没有 render 树,修改 DOM 后不会触发页面重绘,也无法绑定事件,它只适合服务端数据提取场景。Cheerio 的底层解析器是什么?默认使用 htmlparser2,也可切换到 parse5。htmlparser2 速度更快,parse5 则更严格遵循 HTML 规范。什么场景下必须用 jQuery 而非 Cheerio?需要事件监听、CSS 动画、视口计算、用户交互等浏览器原生能力时,必须用 jQuery。写段代码const cheerio = require('cheerio');const $ = cheerio.load('<ul><li>A</li><li>B</li></ul>');$('li').each((i, el) => console.log($(el).text()));
服务端阅读 05月29日 00:25

Cypress 中如何实现数据驱动测试?

数据驱动测试将测试数据与逻辑分离,Cypress 通过 cy.fixture() 加载 cypress/fixtures/ 下的 JSON 文件驱动测试。核心流程:在 fixtures 目录建数据文件,测试中用 cy.fixture() 加载后遍历执行,实现一组逻辑跑多组数据。更简洁的方式是结合 cy.each() 或原生 forEach 迭代数据,避免为每组数据写重复测试。外部数据文件适合管理多环境配置和边界值数据集,fixtures 适合静态模拟数据。追问cy.fixture() 和直接 import JSON 有什么区别?cy.fixture() 走 Cypress 管道,支持超时重试和命令日志;import 是编译时加载,不经过 Cypress 命令链,无法在报告中追踪。如何用 fixtures 实现参数化测试?用 cy.fixture() 加载数组数据,配合 cy.each() 或 forEach 遍历,每组数据生成独立 it 用例,失败时能精确定位是哪组数据有问题。fixtures 数据在不同测试间会互相影响吗?Cypress 默认每个测试前重置 fixtures 状态;但如果在 before 中修改 fixture 返回的对象,会影响后续测试,建议每次加载用深拷贝。大量测试数据该怎么管理?按模块分目录(fixtures/auth/、fixtures/products/),公共数据放 fixtures/common/;环境相关数据用 cypress.env.json + CYPRESS_ 环境变量区分。写段代码// fixtures/users.json: [{"name":"Alice","role":"admin"},{"name":"Bob","role":"user"}]describe('数据驱动权限测试', () => { let users; before(() => { cy.fixture('users').then(data => users = data); }); users.forEach((user, i) => { it(`用户 ${user.name} 角色为 ${user.role}`, () => { cy.login(user.name, 'pass'); cy.get('[data-testid=role]').should('contain', user.role); }); });});
服务端阅读 05月29日 00:24

Cypress 中 Page Object 模式有必要用吗?

Page Object 模式将页面元素选择器和操作封装为独立类,测试代码只调用方法不直接写选择器,页面变更时只需改 Page Object 不改测试。但在 Cypress 中,Custom Command 常能替代 POM 的大部分功能——cy.login() 比 loginPage.login() 更符合 Cypress 风格。POM 真正有价值的场景是:多页面复杂流程(如电商下单流程跨 4 个页面)、团队已熟悉 POM 模式、选择器需要跨多个测试文件共享复用。追问Cypress 官方对 POM 的态度是什么?官方认为 POM 不是必须的,Cypress 的 Custom Command 和组合式 API 已能很好复用逻辑;过度封装反而增加维护成本,简单场景用 Custom Command 更合适。Custom Command 和 POM 怎么选?单页面或少交互场景用 Custom Command(如 cy.login());多页面流程且团队习惯 OOP 风格时用 POM,两者可混合使用。POM 中选择器应该怎么管理?统一使用 data-testid 属性作为选择器锚点,不依赖 CSS class 或 DOM 结构,UI 样式变更不影响测试稳定性。POM 类变得臃肿怎么办?拆分为基础 PageObject(通用方法)+ 具体页面子类;组件级别的对象(如导航栏)独立为 Component Object,避免单类膨胀。写段代码// POM 类 + 测试使用class LoginPage { get username() { return cy.get('[data-testid=username]'); } get password() { return cy.get('[data-testid=password]'); } login(user, pass) { this.username.type(user); this.password.type(pass); cy.get('[data-testid=submit]').click(); }}// 测试中const login = new LoginPage();login.login('admin', '123456');cy.url().should('include', '/dashboard');
服务端阅读 05月29日 00:24

如何优化 Cypress 测试的执行速度?

核心优化手段:用 cy.session() 缓存登录状态避免重复登录;通过 --parallel 并行执行拆分 spec 文件;用 cy.intercept() 拦截 mock 网络请求减少真实 API 调用;避免 cy.wait() 硬编码等待,让 Cypress 自动重试机制生效;配置 baseUrl 避免重复导航。综合使用可将 1000+ 用例执行时间从 20 分钟压到 5 分钟以内。追问cy.session() 和 before() 中登录有什么区别?before() 每个测试文件都会执行登录;cy.session() 在同文件内跨测试复用登录状态,且 session 失效时自动重建,减少冗余请求。并行执行为什么需要 Cypress Cloud?Cypress 的并行调度依赖 Dashboard 服务分配测试到不同机器,免费版可用 cypress-parallel 插件做本地并行,但缺少负载均衡。如何识别最慢的测试用例?运行 cypress run --reporter=json 生成报告,按 duration 排序定位瓶颈;或在 Cypress Cloud 查看耗时分布图。spec 文件应该怎么拆分?按功能模块拆分,每个 spec 控制在 10-20 个测试;避免单文件过大影响并行均衡,也避免过碎导致启动开销占比过高。cy.intercept() mock 数据会不会导致测试失真?会,应在关键流程用真实 API,仅在辅助请求(如第三方服务)使用 mock,并在 CI 中定期跑无 mock 的全量回归验证。写段代码// cy.session 缓存登录 + intercept mockbeforeEach(() => { cy.session('user', () => { cy.intercept('POST', '/api/login', { statusCode: 200 }); cy.visit('/login'); cy.get('[name=email]').type('user@test.com'); cy.get('[name=password]').type('pass123'); cy.get('button').click(); });});