跳转到内容

Web Crawler — 网络爬虫

V1:老板说去看看竞品网站的价格

Section titled “V1:老板说去看看竞品网站的价格”

老板在周会上说:“帮我看看竞品 A 的官网上,产品定价是多少。“你打开竞品网站看了一眼,手动记了几个价格。 第二天老板又问:“竞品 B 和 C 的也看看。“你意识到手动抄数据效率太低了。 你决定写个脚本,自动抓取竞品网站的价格信息,存成 JSON 文件,下次老板问直接打开文件就行。 只需要抓一个页面,提取几个关键字段,能跑就行。

写一个脚本,抓取指定网页的内容并提取结构化数据,保存为本地 JSON 文件。

帮我写一个简单的网页爬虫脚本,要求如下:
1. 使用 Node.js,单个文件 crawler.js,不需要任何框架
2. 依赖:仅用 node-fetch(或内置 fetch)+ cheerio 解析 HTML
3. 功能:
- 从命令行参数接收目标 URL:node crawler.js https://example.com/pricing
- 抓取页面 HTML 内容
- 用 cheerio 解析 DOM,提取所有包含价格信息的元素(匹配 $ 或 ¥ 符号的文本)
- 同时提取页面标题、所有 h2 标题、所有带价格的段落
4. 输出:
- 在终端打印提取结果的摘要
- 保存完整结果到 output.json 文件,格式:
{"url": "...", "title": "...", "crawled_at": "...", "prices": [...], "headings": [...]}
5. 错误处理:网络超时 10 秒,HTTP 错误码打印警告
6. 添加 User-Agent 头模拟浏览器请求
  • 运行 node crawler.js https://example.com 成功抓取页面
  • output.json 文件生成,包含 url、title、crawled_at 等字段
  • 对包含价格信息的网页,prices 数组正确提取了价格文本
  • 输入无效 URL 时,脚本打印错误信息而不是崩溃
  • 修改目标 URL,能抓取不同网站的数据
  • HTTP 请求基础:fetch API、请求头、状态码处理 → M13 网络基础
  • HTML 解析:DOM 选择器、文本提取、结构化数据抽取 → M9 数据处理
  • 命令行工具设计:参数解析、错误处理、优雅退出 → M1 API 基础

V2:要抓整个竞品网站,不止一个页面

Section titled “V2:要抓整个竞品网站,不止一个页面”

老板的需求升级了:“不只是定价页面,把竞品的所有产品页面都抓下来,我要看他们的功能介绍和更新日志。” 一个页面一个页面手动输入 URL 太蠢了。你需要从一个起始页出发,自动发现并抓取所有相关链接。 但你也不想把整个互联网都爬下来——需要控制范围,只抓同一个域名下的页面,而且不能重复抓取。 之前的 Node.js 脚本不太好扩展,你决定用 Go 重写一个更结构化的版本。

构建一个多页面爬虫,从起始 URL 出发,BFS 遍历同域名下的所有页面,去重并保存结果。

帮我用 Go 写一个多页面网络爬虫,要求如下:
1. 技术栈:纯 Go 标准库 + golang.org/x/net/html 解析库
2. 爬取策略:
- 从命令行参数接收起始 URL
- BFS(广度优先)遍历:从起始页提取所有 <a href="..."> 链接
- 只跟进同域名下的链接(忽略外部链接)
- 用 map[string]bool 作为 visited 集合,避免重复抓取
- 最大抓取页面数限制(默认 100 页,可通过 -max 参数调整)
3. 并发控制:
- 用 goroutine + channel 实现并发抓取,最多 5 个并发 worker
- 每个请求之间间隔 1 秒,尊重目标网站(rate limiting)
4. 数据存储:
- 每个页面保存为一个 JSON 文件,放在 output/ 目录下
- 文件名用 URL 的 MD5 哈希值命名
- 内容包含:url, title, links (页面中发现的链接列表), text_content (正文文本), crawled_at
5. robots.txt:
- 启动时先请求 /robots.txt,解析 Disallow 规则
- 被禁止的路径不抓取,打印跳过日志
6. 日志:打印进度,如 [15/100] Crawling: https://...
  • 运行 go run main.go https://example.com 开始从起始页 BFS 抓取
  • output/ 目录下生成多个 JSON 文件,每个文件对应一个页面
  • 同一个页面不会被重复抓取(检查 visited 集合日志)
  • 外部链接被正确忽略,只抓取同域名下的页面
  • 达到 -max 限制后自动停止
  • robots.txt 中 Disallow 的路径被跳过
  • BFS 遍历算法:队列 + 已访问集合的经典模式 → M9 数据处理
  • Go 并发模型:goroutine + channel 实现 worker pool → M8 扩展性
  • 爬虫礼仪:robots.txt 协议、请求频率控制、User-Agent 规范 → M13 网络基础
  • URL 解析与规范化:相对路径转绝对路径、去锚点、去参数去重 → M9 数据处理

V3:要定期抓 100 个竞品网站,几百万页

Section titled “V3:要定期抓 100 个竞品网站,几百万页”

你的爬虫被公司多个团队发现了,大家都想用。市场部要监控 50 个竞品,产品部要追踪 30 个行业网站,HR 还要抓招聘信息。 页面总量从几百飙升到几百万。单机内存放不下所有 visited URL 了;一台机器一天也爬不完这么多页面; 有些页面之前抓过,内容没变但又浪费资源重新抓了。你需要一个分布式爬虫架构。

构建分布式爬虫系统,用数据库管理 URL 队列,Redis 做去重,多个 worker 并行消费任务。

帮我构建一个分布式网络爬虫系统,要求如下:
1. 技术栈:Go + PostgreSQL + Redis
2. 架构:调度器 (Scheduler) + 多个 Worker,通过数据库协调
3. URL 管理(PostgreSQL):
- 表 url_frontier:id, url (唯一索引), domain, status (pending/crawling/done/failed),
priority, retry_count, last_crawled_at, content_hash, created_at
- 调度器每次从 frontier 取一批 pending URL,标记为 crawling,分配给 worker
- worker 完成后更新 status 为 done,存入 content_hash
- 同一域名的 URL 按 priority 排序,高优先级先抓
4. 去重(Redis Bloom Filter):
- 新发现的 URL 先查 Redis Bloom Filter
- 如果可能已存在则查 PostgreSQL 确认(布隆过滤器允许假阳性)
- 确认是新 URL 才插入 frontier
5. 内容去重:
- 用 SHA256 对页面内容计算 hash,存入 content_hash 字段
- 重新抓取时比较 hash,内容未变则跳过处理
6. Worker Pool:
- 支持配置 worker 数量(默认 10 个)
- 每个 worker 是独立 goroutine,从 channel 接收任务
- 按域名维度做请求间隔控制(同一域名间隔 2 秒)
7. 容错:
- 抓取失败自动重试,最多 3 次,每次间隔指数递增
- worker 崩溃后,crawling 状态的 URL 超过 5 分钟自动重置为 pending
8. 提供 docker-compose.yml 包含 PostgreSQL 和 Redis
9. 调度器和 worker 在同一个 Go 程序中(通过配置切换角色)
  • docker-compose up 启动基础设施,程序启动后成功连接 DB 和 Redis
  • 添加种子 URL 后,worker 开始抓取并自动发现新页面
  • Redis Bloom Filter 正确过滤已存在的 URL(日志显示 skip 信息)
  • 同一域名的请求间隔不少于 2 秒(通过日志时间戳验证)
  • 手动 kill 一个 worker,5 分钟后其任务自动被其他 worker 接管
  • 重复抓取内容未变的页面时,content_hash 相同,日志显示跳过处理
  • 启动多个 worker 实例,任务被均匀分配(无重复消费)
  • 分布式任务调度:数据库作为任务队列,状态机驱动任务流转 → M5 消息队列
  • 布隆过滤器:概率型数据结构,空间换时间的去重方案 → M8 扩展性
  • 内容指纹:SHA256 哈希做内容去重,避免重复处理 → M9 数据处理
  • 域名级别限流:按域名维度做 rate limiting,每个域名独立计时 → M13 网络基础
  • 故障恢复:超时检测 + 状态重置,让系统具备自愈能力 → M8 扩展性

V4:要监控1000个竞品站,每天定时抓

Section titled “V4:要监控1000个竞品站,每天定时抓”

爬虫系统稳定运行了一段时间,现在公司要求监控 1000 个竞品网站,每天定时抓取最新内容。 但现在的问题是:没有定时调度机制,每次都要手动触发;所有网站优先级相同,重要的竞品和不重要的混在一起; 很多页面内容根本没变,却每次都重新抓取和解析,浪费了大量计算资源。

引入定时调度、优先级队列和增量抓取机制,让爬虫系统能够高效地定时监控大量目标站点。

帮我为分布式爬虫系统增加定时调度和增量抓取能力,要求如下:
1. 技术栈:Go + PostgreSQL + Redis + Cron
2. 定时调度:
- 新增 crawl_tasks 表:id, name, seed_urls (JSON数组), cron_expression, priority, enabled, last_run_at
- 内置 cron 调度器(用 robfig/cron 库),按 cron_expression 定时触发抓取任务
- 支持不同频率:核心竞品每 4 小时一次,普通站点每天一次,低优先级每周一次
- API 接口管理任务:POST /api/tasks 创建、PUT /api/tasks/:id 更新、GET /api/tasks 列表
3. 优先级队列:
- url_frontier 表的 priority 字段改为 0-10 等级(10 最高)
- 调度器取任务时按 priority DESC, created_at ASC 排序
- 核心竞品的种子 URL 优先级设为 8-10,普通站点 4-6,低优先级 1-3
- 紧急任务支持:API 接口可手动提交高优先级 URL,插队到队列最前面
4. 增量抓取:
- 请求时携带 If-None-Match (ETag) 和 If-Modified-Since 头
- 服务器返回 304 Not Modified 时跳过该页面,日志记录"内容未变"
- 保存每个 URL 的 ETag 和 Last-Modified 到 url_frontier 表(新增两个字段)
- 对于不支持条件请求的网站,用 content_hash 比较(已有机制)
5. 抓取报告:
- 每次定时任务完成后生成报告:总页面数、新增页面、变更页面、未变页面、失败页面
- 报告存入 crawl_reports 表:id, task_id, started_at, finished_at, stats (JSON)
- GET /api/reports?task_id=1 查看历史报告
6. 提供 docker-compose.yml 和示例配置
  • 创建定时任务后,到达 cron 触发时间时自动开始抓取
  • 高优先级 URL 总是在低优先级之前被处理(通过日志顺序验证)
  • 手动提交紧急 URL 后立即被 worker 抓取(不用等待排队)
  • 第二次抓取时,未变化的页面返回 304,日志显示跳过(增量抓取生效)
  • url_frontier 表中有正确的 etag 和 last_modified 值
  • 任务完成后 crawl_reports 表中有对应报告,统计数据准确
  • 通过 API 禁用某个任务后,该任务不再被触发
  • 定时任务调度:Cron 表达式、任务管理、防止重复执行 → M9 数据处理
  • 优先级队列:数据库实现优先级排序,紧急任务插队的设计 → M5 消息队列
  • HTTP 条件请求:ETag / Last-Modified / 304 机制,减少无效传输 → M13 网络基础
  • 增量处理:只处理变化的数据,大幅节省计算资源 → M9 数据处理

V5:解析结果要实时搜索,爬取失败率高

Section titled “V5:解析结果要实时搜索,爬取失败率高”

每天抓取量涨到 10 万页,新问题出现了: 第一,业务方想搜索抓取结果——“帮我找所有提到’AI’的竞品页面”,但现在只能在数据库里模糊查询,速度极慢。 第二,抓取失败率高达 15%——有的网站限流、有的超时、有的返回验证码,失败的页面被丢弃后就没人管了。 第三,数据流太混乱:抓取、解析、存储、索引全在一个 worker 里串行执行,某一步卡住整个流程都停了。

构建 Pipeline 架构拆分处理流程,引入死信队列处理失败任务,用 Elasticsearch 支持全文搜索。

帮我将爬虫系统升级为 Pipeline 架构,增加搜索和失败处理能力,要求如下:
1. 技术栈:Go + PostgreSQL + Redis + Elasticsearch + RabbitMQ(或用 Redis Stream 模拟)
2. Pipeline 架构(4 个阶段):
- Stage 1 - Crawl:抓取页面 HTML,写入 crawl_queue
- Stage 2 - Parse:从 crawl_queue 消费,解析 HTML 提取结构化数据(标题、正文、链接、元数据),写入 parse_queue
- Stage 3 - Store:从 parse_queue 消费,将结构化数据存入 PostgreSQL
- Stage 4 - Index:从 store_queue 消费,将数据索引到 Elasticsearch
- 每个阶段独立的 worker pool,可单独扩缩容
3. 死信队列(Dead Letter Queue):
- 每个阶段处理失败 3 次后,消息转入对应的死信队列(dlq_crawl, dlq_parse 等)
- 管理接口 GET /api/dlq:查看各队列积压数量
- 管理接口 POST /api/dlq/retry:将死信队列中的消息重新投入正常队列
- 失败原因记录:超时、HTTP错误、解析异常、索引失败等分类统计
4. Elasticsearch 全文搜索:
- 索引名 crawled_pages,mapping 包含:url, domain, title, content, crawled_at
- content 字段使用 ik_max_word 分词器(中文支持)
- 搜索 API:GET /api/search?q=关键词&domain=xxx&from=0&size=20
- 支持高亮返回匹配片段
5. 监控仪表盘:
- 各阶段的消费速率(条/秒)和队列积压长度
- 各阶段的成功率和错误分布
- Elasticsearch 索引大小和文档数量
- 提供 Grafana dashboard 配置
6. docker-compose.yml 包含 PostgreSQL、Redis、Elasticsearch、RabbitMQ、Grafana
  • 添加种子 URL 后,数据依次流过 Crawl → Parse → Store → Index 四个阶段
  • 每个阶段可独立运行多个 worker(启动时指定 —stage=crawl —workers=5)
  • 模拟抓取失败(目标网站返回 503),失败 3 次后消息进入死信队列
  • 调用 /api/dlq 查看积压,调用 /api/dlq/retry 重新处理
  • 通过 /api/search?q=关键词 搜索抓取内容,返回高亮匹配结果
  • 搜索支持按域名过滤和分页
  • Grafana 面板显示各阶段处理速率和错误率
  • Pipeline 架构:将复杂流程拆分为独立阶段,解耦和独立扩展 → M9 数据处理
  • 死信队列:失败消息的隔离和重试机制,保证数据不丢失 → M5 消息队列
  • 全文搜索引擎:Elasticsearch 索引、分词、高亮的基本使用 → M10 搜索
  • 可观测性:多阶段系统的监控指标设计和可视化 → M15 可观测性

爬虫系统的日抓取量要求达到 100 万页。单机的网络带宽跑满了 1Gbps,CPU 使用率 95%——HTML 解析和内容处理太吃资源。 更大的问题是:1000 个目标站点中有些是同一个域名的不同路径,多个 worker 同时抓同一个域名会被封 IP。 你需要将爬虫分布到多台机器上,并且实现跨机器的域名级别协调。

实现多机器分布式爬虫,用一致性哈希分配 URL,中心化协调器管理全局状态,跨 worker 域名限流。

帮我将爬虫系统升级为多机器分布式架构,要求如下:
1. 技术栈:Go + PostgreSQL + Redis + RabbitMQ + Consul
2. 分布式 Worker 集群:
- 中心协调器(Coordinator):负责任务分配、worker 注册、健康检查
- Worker 节点:启动时向 Coordinator 注册,定期心跳上报状态(CPU、内存、带宽使用率)
- Coordinator 根据 worker 负载情况分配任务,避免过载节点
- Worker 异常时(心跳超时 30 秒),其任务自动重新分配到其他节点
3. 一致性哈希 URL 分配:
- 对 URL 的域名做一致性哈希,同一域名的所有 URL 固定分配到同一个 worker
- 好处:同一域名的请求天然串行,不需要跨 worker 的域名限流协调
- Worker 增减时自动 rehash,最小化 URL 迁移量(虚拟节点 150 个)
- 哈希环状态存储在 Redis 中,所有节点共享同一视图
4. 跨 Worker 域名限流:
- 即使一致性哈希保证了域名亲和性,仍需全局限流作为兜底
- Redis 滑动窗口限流:每个域名每分钟最多 30 个请求
- 限流 key:rate_limit:{domain},value 为 Redis Sorted Set(时间戳作为 score)
- Worker 每次请求前检查限流,超限则延迟执行
5. 中心协调器:
- 服务注册与发现(用 Consul 或简化版 Redis 实现)
- 全局任务看板:各 worker 的当前任务数、完成数、失败数
- 动态调整:API 接口添加/移除 worker,手动触发 rehash
- 协调器自身高可用:主备模式,通过 Redis 分布式锁选主
6. 网络优化:
- 连接池复用:每个域名维护独立的 HTTP 连接池(最大 5 个连接)
- 压缩:请求头 Accept-Encoding: gzip, deflate,减少带宽占用
- DNS 缓存:本地缓存 DNS 解析结果(TTL 5 分钟),避免重复 DNS 查询
7. docker-compose.yml 支持启动 1 个 Coordinator + N 个 Worker 节点
  • 启动 Coordinator 和 3 个 Worker 节点,Consul 中能看到所有注册的服务
  • 同一域名的 URL 始终分配到同一个 Worker(通过日志中的 worker_id 验证)
  • 新增 1 个 Worker 节点后,一致性哈希重新平衡,只有部分域名迁移
  • 停掉 1 个 Worker,30 秒后其域名的 URL 被重新分配到其他 Worker
  • 同一域名的请求频率不超过每分钟 30 个(通过 Redis 限流 key 验证)
  • Coordinator 主节点挂掉后,备节点 10 秒内接管(分布式锁切换)
  • 全局任务看板显示各 Worker 的实时状态和负载情况
  • 一致性哈希:虚拟节点、最小迁移、域名亲和性分配 → M8 扩展性
  • 分布式协调:服务注册与发现、心跳检测、主备选举 → M17 基础设施
  • 跨节点限流:Redis 滑动窗口实现全局共享的速率限制 → M8 扩展性
  • 网络优化:连接池复用、压缩传输、DNS 缓存的实用技巧 → M13 网络基础
  • 分布式系统容错:worker 故障转移、协调器高可用的设计 → M16 DevOps