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

面试题手册

什么是容器编排?为什么需要容器编排?主流的容器编排工具有哪些?

答案容器编排(Container Orchestration)是指自动化管理、部署、扩展和联网容器化应用程序的过程。随着微服务架构的普及,单个应用可能包含数十甚至数百个容器,手动管理变得极其困难,容器编排工具应运而生。为什么需要容器编排容器数量庞大:微服务架构下,应用被拆分为多个服务,每个服务可能运行多个容器副本生命周期管理:需要自动化容器的创建、启动、停止、销毁等操作资源调度:根据资源需求和约束,将容器调度到合适的节点上服务发现:容器之间需要相互发现和通信负载均衡:在多个容器副本之间分配流量自动扩展:根据负载自动增加或减少容器数量自我修复:容器失败时自动重启或重新调度滚动更新:零停机地更新应用版本配置管理:统一管理配置和密钥存储管理:自动挂载和管理持久化存储容器编排的核心功能1. 服务发现和负载均衡自动为容器分配 DNS 名称在多个容器副本之间负载均衡支持内部和外部服务发现2. 存储编排自动挂载存储系统支持多种存储后端(本地、NFS、云存储)动态卷供应3. 自动部署和回滚声明式配置自动化部署流程快速回滚到之前的版本4. 自动扩缩容水平扩展:增加容器副本数量垂直扩展:调整容器资源限制基于指标(CPU、内存、QPS)自动扩展5. 自我修复自动重启失败的容器重新调度不健康的容器替换失效的节点6. 配置和密钥管理集中管理配置数据安全存储敏感信息支持配置热更新7. 批处理执行运行批处理任务定时任务调度任务完成自动清理主流容器编排工具1. Kubernetes(K8s)特点:CNCF 托管的开源项目最流行的容器编排平台丰富的生态系统强大的扩展性优势:成熟稳定社区活跃云厂商广泛支持完整的功能集适用场景:大规模生产环境复杂的微服务架构需要高可用性和可扩展性2. Docker Swarm特点:Docker 原生编排工具学习曲线低轻量级设计与 Docker CLI 集成优势:简单易用快速上手适合小规模部署资源占用少适用场景:小型团队简单的应用架构快速原型开发3. Nomad特点:HashiCorp 开发支持多种工作负载(容器、虚拟机、批处理)简单的架构良好的可扩展性优势:多工作负载支持配置简单与 HashiCorp 生态集成资源效率高适用场景:混合工作负载环境需要运行非容器化应用中小规模部署4. Apache Mesos + Marathon特点:通用集群管理器支持多种框架高可扩展性企业级特性优势:资源利用率高支持大规模集群成熟稳定灵活的调度策略适用场景:超大规模集群需要运行多种工作负载企业级环境Kubernetes vs 其他编排工具对比| 特性 | Kubernetes | Docker Swarm | Nomad ||------|-----------|--------------|-------|| 学习曲线 | 陡峭 | 平缓 | 中等 || 复杂度 | 高 | 低 | 中等 || 生态系统 | 丰富 | 有限 | 中等 || 社区支持 | 强 | 中等 | 中等 || 扩展性 | 极高 | 中等 | 高 || 资源占用 | 较高 | 低 | 低 || 适用规模 | 大规模 | 小规模 | 中等规模 || 多工作负载 | 容器为主 | 容器 | 多种类型 |容器编排的最佳实践1. 声明式配置# Kubernetes Deployment 示例apiVersion: apps/v1kind: Deploymentmetadata: name: nginx-deploymentspec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 802. 健康检查livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 30 periodSeconds: 10readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 53. 资源限制resources: requests: memory: "64Mi" cpu: "250m" limits: memory: "128Mi" cpu: "500m"4. 配置管理# ConfigMapapiVersion: v1kind: ConfigMapmetadata: name: app-configdata: database.url: "mysql://localhost:3306" cache.ttl: "3600"# SecretapiVersion: v1kind: Secretmetadata: name: app-secrettype: Opaquedata: password: cGFzc3dvcmQ=5. 滚动更新策略strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0容器编排的挑战复杂性:学习曲线陡峭,配置复杂资源消耗:编排平台本身需要资源网络复杂性:容器网络配置和管理存储管理:持久化存储的复杂性安全性:多租户环境下的安全隔离调试困难:分布式系统的调试挑战升级维护:编排平台的升级和维护容器编排的未来趋势Serverless 容器:AWS Fargate、Google Cloud Run边缘计算:在边缘节点运行容器AI 驱动的调度:智能资源调度和优化服务网格集成:与 Istio、Linkerd 等服务网格深度集成多云管理:统一管理多云容器部署安全性增强:更强的安全隔离和合规性实施建议从小规模开始:先在小规模环境中验证选择合适的工具:根据团队规模和需求选择投资培训:团队需要学习新技能自动化一切:尽可能自动化运维流程监控和日志:建立完善的监控和日志系统文档化:记录架构和配置持续改进:根据实践经验不断优化容器编排是现代云原生应用的基础设施,它通过自动化管理容器,让微服务架构的实施变得可行和高效。选择合适的容器编排工具并正确实施,可以极大地提高应用的可扩展性、可靠性和运维效率。
阅读 0·2月22日 14:31

什么是微服务架构?微服务架构的优势和挑战有哪些?

答案微服务架构是一种将单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,并使用轻量级机制(通常是 HTTP API)进行通信。这些服务围绕业务能力构建,可以通过全自动部署机制独立部署。微服务架构的核心特征单一职责:每个服务专注于单一业务功能独立部署:服务可以独立开发、测试、部署和扩展去中心化:服务可以使用不同的编程语言和数据存储技术松耦合:服务之间通过 API 通信,减少依赖自治性:服务团队拥有服务的完整生命周期可扩展性:可以根据需求独立扩展特定服务微服务 vs 单体架构| 特性 | 单体架构 | 微服务架构 ||------|---------|-----------|| 部署 | 整体部署 | 独立部署 || 扩展 | 整体扩展 | 独立扩展 || 技术栈 | 统一技术栈 | 多样化技术栈 || 复杂度 | 开发简单,运维复杂 | 开发复杂,运维简单 || 故障隔离 | 一个故障影响全局 | 故障隔离在单个服务 || 团队协作 | 大团队协作 | 小团队自治 || 性能 | 调用速度快 | 网络调用有开销 |微服务架构的优势灵活性和敏捷性快速响应业务需求变化独立开发和部署,减少协调成本支持持续交付和持续部署可扩展性根据负载独立扩展需要的服务优化资源使用,降低成本支持水平扩展技术多样性不同服务可以使用最适合的技术栈新技术可以逐步引入避免技术锁定故障隔离单个服务故障不会影响整个系统提高系统整体可用性便于定位和修复问题团队自治小团队负责特定服务减少团队间的依赖和协调提高开发效率微服务架构的挑战分布式系统复杂性服务间通信的复杂性分布式事务处理困难数据一致性难以保证运维复杂性需要管理大量服务监控和日志收集复杂故障排查困难网络延迟服务间通信通过网络增加响应时间需要优化网络性能数据管理分布式数据一致性跨服务查询复杂数据迁移困难测试复杂性需要测试多个服务集成测试复杂环境搭建困难微服务架构的关键组件1. API 网关(API Gateway)统一入口点请求路由负载均衡认证和授权限流和熔断2. 服务发现(Service Discovery)服务注册服务查找健康检查负载均衡3. 配置中心(Configuration Center)集中配置管理动态配置更新配置版本控制环境隔离4. 消息队列(Message Queue)异步通信解耦服务流量削峰事件驱动架构5. 分布式追踪(Distributed Tracing)请求链路追踪性能分析故障定位依赖分析6. 监控和日志(Monitoring and Logging)服务监控日志收集告警通知性能分析微服务通信模式1. 同步通信REST APIGraphQLgRPC优点:简单直观实时响应易于调试缺点:耦合度高性能受网络影响容易产生级联故障2. 异步通信消息队列(Kafka、RabbitMQ)事件总线发布/订阅模式优点:松耦合高性能容错性好缺点:复杂度高调试困难最终一致性微服务数据管理策略1. 每个服务独立数据库服务拥有自己的数据库避免跨服务数据库访问提高服务独立性2. 数据一致性最终一致性Saga 模式事件溯源CQRS(命令查询责任分离)3. 数据同步事件驱动同步定时任务同步CDC(Change Data Capture)微服务部署策略1. 蓝绿部署维护两套相同环境新版本部署到绿环境切换流量到绿环境出问题快速回滚2. 金丝雀发布逐步向部分用户发布新版本监控指标和错误率逐步扩大发布范围出问题快速回滚3. 滚动更新逐步替换旧版本实例保持服务可用性自动回滚机制微服务最佳实践1. 领域驱动设计(DDD)按业务领域划分服务边界定义清晰的上下文边界避免服务过大或过小2. 容器化使用 Docker 打包服务环境一致性快速部署和扩展3. 自动化CI/CD 流水线自动化测试自动化部署4. 监控和可观测性全面的监控指标分布式追踪集中式日志管理5. 故障处理熔断器模式限流机制降级策略重试机制6. 安全性服务间认证(JWT、mTLS)API 网关安全数据加密安全审计微服务架构适用场景适合微服务的场景:大型复杂应用需要频繁迭代和快速交付团队规模较大需要独立扩展不同模块业务边界清晰不适合微服务的场景:小型简单应用团队规模小对性能要求极高初创公司快速验证想法微服务技术栈语言和框架:Java: Spring Boot, Spring CloudGo: Go Micro, gRPCPython: Flask, FastAPINode.js: Express, NestJS基础设施:容器:Docker, KubernetesAPI 网关:Kong, Nginx, API Gateway服务发现:Consul, Eureka, etcd配置中心:Spring Cloud Config, Consul消息队列:Kafka, RabbitMQ, RocketMQ监控:Prometheus, Grafana, ELK追踪:Jaeger, Zipkin微服务架构是现代云原生应用的主流架构模式,它通过将应用拆分为小型、独立的服务,提高了系统的灵活性、可扩展性和可维护性。但同时也带来了分布式系统的复杂性,需要团队具备相应的技术能力和运维经验。
阅读 0·2月22日 14:31

DevOps 中监控和日志管理的重要性是什么?常用的监控和日志工具有哪些?

答案监控和日志管理是 DevOps 实践中至关重要的组成部分,它们帮助团队了解系统运行状态、快速定位问题、优化性能,并确保系统的稳定性和可靠性。监控(Monitoring)监控是指对系统、应用程序和基础设施进行持续观察和测量的过程,以确保它们按预期运行。监控的核心指标基础设施指标CPU 使用率内存使用率磁盘 I/O网络流量磁盘空间应用程序指标请求响应时间吞吐量(QPS)错误率并发连接数业务指标(订单量、用户数等)自定义指标队列长度缓存命中率数据库连接数特定业务逻辑指标监控类型黑盒监控(Black-box Monitoring)从外部视角监控系统模拟用户行为检查系统可用性示例:Ping 检查、HTTP 健康检查白盒监控(White-box Monitoring)从内部视角监控系统收集应用程序内部指标深入了解系统状态示例:应用性能监控(APM)、日志分析合成监控(Synthetic Monitoring)主动探测系统模拟用户操作预警潜在问题示例:网站可用性监控常用监控工具Prometheus开源时间序列数据库强大的查询语言(PromQL)服务发现机制告警规则配置Grafana可视化仪表板支持多种数据源丰富的图表类型告警通知Zabbix企业级监控解决方案分布式监控架构自动发现功能灵活的告警机制Nagios老牌监控工具插件系统丰富主机和服务监控告警通知DatadogSaaS 监控平台全栈监控APM 集成机器学习告警日志管理(Log Management)日志管理是指收集、存储、分析和可视化系统日志的过程,帮助团队了解系统行为、排查问题和审计操作。日志类型应用日志应用程序输出日志业务逻辑日志错误和异常日志系统日志操作系统日志内核日志系统服务日志访问日志Web 服务器访问日志API 调用日志用户行为日志安全日志登录日志权限变更日志安全事件日志日志最佳实践结构化日志使用 JSON 格式包含时间戳、级别、消息添加上下文信息示例: { "timestamp": "2024-01-01T10:00:00Z", "level": "INFO", "service": "user-service", "message": "User login successful", "user_id": "12345", "ip": "192.168.1.1" }日志级别DEBUG:调试信息INFO:一般信息WARN:警告信息ERROR:错误信息FATAL:致命错误日志轮转按大小或时间轮转保留策略配置压缩旧日志避免磁盘占满敏感信息保护不记录密码、密钥脱敏处理敏感数据符合合规要求常用日志工具ELK Stack(Elasticsearch, Logstash, Kibana)Elasticsearch:日志存储和搜索Logstash:日志收集和处理Kibana:日志可视化Filebeat:轻量级日志收集器Fluentd开源日志收集器插件系统丰富高性能处理统一日志层Splunk企业级日志分析平台强大的搜索能力机器学习分析商业软件Graylog开源日志管理平台集中式日志收集实时分析告警功能LokiGrafana 生态日志系统轻量级设计类似 Prometheus 的标签模型成本低监控和日志的集成1. 统一的可观测性平台将监控指标、日志和追踪数据整合提供统一的查询和分析界面关联不同类型的数据示例:Grafana + Loki + Tempo2. 告警集成基于监控指标的告警基于日志的告警多渠道通知(邮件、短信、Slack)告警聚合和去重3. 自动化响应告警触发自动化脚本自动扩缩容自动故障转移自动修复可观测性的三大支柱指标(Metrics)数值化的数据时间序列数据适合趋势分析示例:CPU 使用率、响应时间日志(Logs)离散的事件记录详细的上下文信息适合问题排查示例:错误日志、访问日志追踪(Tracing)分布式请求追踪跨服务调用链性能分析示例:Jaeger、Zipkin监控和日志的实施策略分层监控基础设施层平台层应用层业务层SLA/SLO/SLISLI(Service Level Indicator):服务级别指标SLO(Service Level Objective):服务级别目标SLA(Service Level Agreement):服务级别协议告警策略设置合理的阈值避免告警疲劳分级告警告警升级机制持续优化定期审查监控覆盖优化告警规则改进日志质量提升查询效率最佳实践尽早实施在项目初期就建立监控日志从第一天就开始记录持续改进监控策略全面覆盖覆盖所有关键组件监控业务指标记录重要事件自动化自动部署监控代理自动配置告警规则自动生成报表文档化记录监控架构文档化告警处理流程维护运行手册团队协作开发、运维共同参与定期复盘重大事故持续改进监控和日志管理是 DevOps 实践的基础设施,它们提供了系统的"眼睛"和"耳朵",帮助团队及时发现和解决问题,确保系统的稳定运行和持续改进。
阅读 0·2月22日 14:31

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

答案自动化测试是 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

async/await 是如何工作的?与 Promise 有什么关系?

async/await 是 ES2017 引入的语法糖,用于处理异步操作,它基于 Promise 构建,让异步代码看起来更像同步代码,大大提高了代码的可读性和可维护性。async 函数基本概念async 函数是使用 async 关键字声明的函数,它总是返回一个 Promise。即使函数内部没有显式返回 Promise,也会被包装成一个 Promise。基本用法async function fetchData() { return 'Hello World';}// 等同于function fetchData() { return Promise.resolve('Hello World');}fetchData().then(result => console.log(result)); // 输出: Hello World返回 Promiseasync function fetchData() { // 返回普通值 return 42;}async function fetchDataWithError() { // 抛出错误 throw new Error('出错了');}fetchData().then(result => console.log(result)); // 输出: 42fetchDataWithError().catch(error => console.error(error.message)); // 输出: 出错了await 表达式基本概念await 关键字只能在 async 函数内部使用,它会暂停 async 函数的执行,等待 Promise 完成,然后返回 Promise 的结果。基本用法async function fetchData() { const promise = Promise.resolve('Hello'); const result = await promise; console.log(result); // 输出: Hello return result;}fetchData();等待多个 Promiseasync function fetchMultipleData() { const promise1 = fetch('/api/user'); const promise2 = fetch('/api/posts'); const [userResponse, postsResponse] = await Promise.all([promise1, promise2]); const user = await userResponse.json(); const posts = await postsResponse.json(); return { user, posts };}错误处理使用 try/catchasync function fetchData() { try { const response = await fetch('/api/data'); const data = await response.json(); return data; } catch (error) { console.error('请求失败:', error.message); throw error; // 可以选择重新抛出错误 }}捕获特定错误async function fetchData() { try { const response = await fetch('/api/data'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { if (error.name === 'TypeError') { console.error('网络连接问题'); } else if (error.message.includes('HTTP error')) { console.error('服务器错误'); } else { console.error('未知错误:', error); } throw error; }}async/await vs Promise.then()Promise.then() 链式调用function fetchData() { return fetch('/api/user') .then(response => response.json()) .then(user => fetch(`/api/posts/${user.id}`)) .then(response => response.json()) .then(posts => ({ user, posts })) .catch(error => { console.error(error); throw error; });}async/await(更易读)async function fetchData() { try { const userResponse = await fetch('/api/user'); const user = await userResponse.json(); const postsResponse = await fetch(`/api/posts/${user.id}`); const posts = await postsResponse.json(); return { user, posts }; } catch (error) { console.error(error); throw error; }}并行执行使用 Promise.allasync function fetchAllData() { const [user, posts, comments] = await Promise.all([ fetch('/api/user').then(r => r.json()), fetch('/api/posts').then(r => r.json()), fetch('/api/comments').then(r => r.json()) ]); return { user, posts, comments };}使用 Promise.allSettledasync function fetchAllDataWithErrors() { const results = await Promise.allSettled([ fetch('/api/user').then(r => r.json()), fetch('/api/posts').then(r => r.json()), fetch('/api/comments').then(r => r.json()) ]); results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`请求 ${index} 成功:`, result.value); } else { console.error(`请求 ${index} 失败:`, result.reason); } }); return results;}常见使用场景1. 顺序执行异步操作async function processItems(items) { const results = []; for (const item of items) { const result = await processItem(item); results.push(result); } return results;}2. 并行执行异步操作async function processItemsParallel(items) { const promises = items.map(item => processItem(item)); const results = await Promise.all(promises); return results;}3. 带超时的请求async function fetchWithTimeout(url, timeout = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); return await response.json(); } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error('请求超时'); } throw error; }}4. 重试机制async function fetchWithRetry(url, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } catch (error) { console.error(`尝试 ${i + 1} 失败:`, error.message); if (i === maxRetries - 1) { throw error; } await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); } }}最佳实践1. 总是使用 try/catch// 不推荐:没有错误处理async function fetchData() { const response = await fetch('/api/data'); return await response.json();}// 推荐:添加错误处理async function fetchData() { try { const response = await fetch('/api/data'); return await response.json(); } catch (error) { console.error('请求失败:', error); throw error; }}2. 避免在循环中顺序 await// 不推荐:顺序执行,速度慢async function processItems(items) { const results = []; for (const item of items) { const result = await processItem(item); results.push(result); } return results;}// 推荐:并行执行,速度快async function processItems(items) { const promises = items.map(item => processItem(item)); return await Promise.all(promises);}3. 合理使用 Promise.all// 推荐:并行执行独立的异步操作async function fetchData() { const [user, posts, comments] = await Promise.all([ fetchUser(), fetchPosts(), fetchComments() ]); return { user, posts, comments };}4. 使用 finally 进行清理async function fetchData() { let connection; try { connection = await createConnection(); const data = await connection.query('SELECT * FROM users'); return data; } catch (error) { console.error('查询失败:', error); throw error; } finally { if (connection) { await connection.close(); } }}常见陷阱1. 忘记使用 await// 错误:没有 awaitasync function fetchData() { const promise = fetch('/api/data'); // promise 是一个 Promise 对象,不是数据 console.log(promise); // 输出: Promise {<pending>}}// 正确:使用 awaitasync function fetchData() { const response = await fetch('/api/data'); const data = await response.json(); console.log(data);}2. 在非 async 函数中使用 await// 错误:在非 async 函数中使用 awaitfunction fetchData() { const data = await fetch('/api/data'); // SyntaxError}// 正确:在 async 函数中使用 awaitasync function fetchData() { const data = await fetch('/api/data'); return data;}3. 过度使用 try/catch// 不推荐:过度使用 try/catchasync function fetchData() { try { try { const response = await fetch('/api/data'); try { const data = await response.json(); return data; } catch (error) { console.error('解析失败:', error); } } catch (error) { console.error('请求失败:', error); } } catch (error) { console.error('未知错误:', error); }}// 推荐:合理使用 try/catchasync function fetchData() { try { const response = await fetch('/api/data'); return await response.json(); } catch (error) { console.error('请求或解析失败:', error); throw error; }}与 Promise 的关系async/await 本质上是 Promise 的语法糖,它们之间可以互相转换:// async/awaitasync function fetchData() { const response = await fetch('/api/data'); return await response.json();}// 等同于 Promisefunction fetchData() { return fetch('/api/data') .then(response => response.json());}总结async 函数总是返回 Promise:即使返回普通值也会被包装成 Promiseawait 暂停执行:等待 Promise 完成后继续执行使用 try/catch 处理错误:确保错误被正确捕获和处理并行执行提高性能:使用 Promise.all 并行执行独立的异步操作避免过度嵌套:保持代码扁平和清晰理解与 Promise 的关系:async/await 是 Promise 的语法糖,可以互相转换
阅读 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