Web Crawler — 网络爬虫
V1:老板说去看看竞品网站的价格
Section titled “V1:老板说去看看竞品网站的价格”老板在周会上说:“帮我看看竞品 A 的官网上,产品定价是多少。“你打开竞品网站看了一眼,手动记了几个价格。 第二天老板又问:“竞品 B 和 C 的也看看。“你意识到手动抄数据效率太低了。 你决定写个脚本,自动抓取竞品网站的价格信息,存成 JSON 文件,下次老板问直接打开文件就行。 只需要抓一个页面,提取几个关键字段,能跑就行。
你要解决什么
Section titled “你要解决什么”写一个脚本,抓取指定网页的内容并提取结构化数据,保存为本地 JSON 文件。
给 AI 的 Prompt
Section titled “给 AI 的 Prompt”帮我写一个简单的网页爬虫脚本,要求如下:
1. 使用 Node.js,单个文件 crawler.js,不需要任何框架2. 依赖:仅用 node-fetch(或内置 fetch)+ cheerio 解析 HTML3. 功能: - 从命令行参数接收目标 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,能抓取不同网站的数据
你学到了什么
Section titled “你学到了什么”- HTTP 请求基础:fetch API、请求头、状态码处理 → M13 网络基础
- HTML 解析:DOM 选择器、文本提取、结构化数据抽取 → M9 数据处理
- 命令行工具设计:参数解析、错误处理、优雅退出 → M1 API 基础
V2:要抓整个竞品网站,不止一个页面
Section titled “V2:要抓整个竞品网站,不止一个页面”老板的需求升级了:“不只是定价页面,把竞品的所有产品页面都抓下来,我要看他们的功能介绍和更新日志。” 一个页面一个页面手动输入 URL 太蠢了。你需要从一个起始页出发,自动发现并抓取所有相关链接。 但你也不想把整个互联网都爬下来——需要控制范围,只抓同一个域名下的页面,而且不能重复抓取。 之前的 Node.js 脚本不太好扩展,你决定用 Go 重写一个更结构化的版本。
你要解决什么
Section titled “你要解决什么”构建一个多页面爬虫,从起始 URL 出发,BFS 遍历同域名下的所有页面,去重并保存结果。
给 AI 的 Prompt
Section titled “给 AI 的 Prompt”帮我用 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_at5. 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 的路径被跳过
你学到了什么
Section titled “你学到了什么”- 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 了;一台机器一天也爬不完这么多页面; 有些页面之前抓过,内容没变但又浪费资源重新抓了。你需要一个分布式爬虫架构。
你要解决什么
Section titled “你要解决什么”构建分布式爬虫系统,用数据库管理 URL 队列,Redis 做去重,多个 worker 并行消费任务。
给 AI 的 Prompt
Section titled “给 AI 的 Prompt”帮我构建一个分布式网络爬虫系统,要求如下:
1. 技术栈:Go + PostgreSQL + Redis2. 架构:调度器 (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 才插入 frontier5. 内容去重: - 用 SHA256 对页面内容计算 hash,存入 content_hash 字段 - 重新抓取时比较 hash,内容未变则跳过处理6. Worker Pool: - 支持配置 worker 数量(默认 10 个) - 每个 worker 是独立 goroutine,从 channel 接收任务 - 按域名维度做请求间隔控制(同一域名间隔 2 秒)7. 容错: - 抓取失败自动重试,最多 3 次,每次间隔指数递增 - worker 崩溃后,crawling 状态的 URL 超过 5 分钟自动重置为 pending8. 提供 docker-compose.yml 包含 PostgreSQL 和 Redis9. 调度器和 worker 在同一个 Go 程序中(通过配置切换角色)- docker-compose up 启动基础设施,程序启动后成功连接 DB 和 Redis
- 添加种子 URL 后,worker 开始抓取并自动发现新页面
- Redis Bloom Filter 正确过滤已存在的 URL(日志显示 skip 信息)
- 同一域名的请求间隔不少于 2 秒(通过日志时间戳验证)
- 手动 kill 一个 worker,5 分钟后其任务自动被其他 worker 接管
- 重复抓取内容未变的页面时,content_hash 相同,日志显示跳过处理
- 启动多个 worker 实例,任务被均匀分配(无重复消费)
你学到了什么
Section titled “你学到了什么”- 分布式任务调度:数据库作为任务队列,状态机驱动任务流转 → M5 消息队列
- 布隆过滤器:概率型数据结构,空间换时间的去重方案 → M8 扩展性
- 内容指纹:SHA256 哈希做内容去重,避免重复处理 → M9 数据处理
- 域名级别限流:按域名维度做 rate limiting,每个域名独立计时 → M13 网络基础
- 故障恢复:超时检测 + 状态重置,让系统具备自愈能力 → M8 扩展性
V4:要监控1000个竞品站,每天定时抓
Section titled “V4:要监控1000个竞品站,每天定时抓”爬虫系统稳定运行了一段时间,现在公司要求监控 1000 个竞品网站,每天定时抓取最新内容。 但现在的问题是:没有定时调度机制,每次都要手动触发;所有网站优先级相同,重要的竞品和不重要的混在一起; 很多页面内容根本没变,却每次都重新抓取和解析,浪费了大量计算资源。
你要解决什么
Section titled “你要解决什么”引入定时调度、优先级队列和增量抓取机制,让爬虫系统能够高效地定时监控大量目标站点。
给 AI 的 Prompt
Section titled “给 AI 的 Prompt”帮我为分布式爬虫系统增加定时调度和增量抓取能力,要求如下:
1. 技术栈:Go + PostgreSQL + Redis + Cron2. 定时调度: - 新增 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 禁用某个任务后,该任务不再被触发
你学到了什么
Section titled “你学到了什么”- 定时任务调度:Cron 表达式、任务管理、防止重复执行 → M9 数据处理
- 优先级队列:数据库实现优先级排序,紧急任务插队的设计 → M5 消息队列
- HTTP 条件请求:ETag / Last-Modified / 304 机制,减少无效传输 → M13 网络基础
- 增量处理:只处理变化的数据,大幅节省计算资源 → M9 数据处理
V5:解析结果要实时搜索,爬取失败率高
Section titled “V5:解析结果要实时搜索,爬取失败率高”每天抓取量涨到 10 万页,新问题出现了: 第一,业务方想搜索抓取结果——“帮我找所有提到’AI’的竞品页面”,但现在只能在数据库里模糊查询,速度极慢。 第二,抓取失败率高达 15%——有的网站限流、有的超时、有的返回验证码,失败的页面被丢弃后就没人管了。 第三,数据流太混乱:抓取、解析、存储、索引全在一个 worker 里串行执行,某一步卡住整个流程都停了。
你要解决什么
Section titled “你要解决什么”构建 Pipeline 架构拆分处理流程,引入死信队列处理失败任务,用 Elasticsearch 支持全文搜索。
给 AI 的 Prompt
Section titled “给 AI 的 Prompt”帮我将爬虫系统升级为 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 面板显示各阶段处理速率和错误率
你学到了什么
Section titled “你学到了什么”- Pipeline 架构:将复杂流程拆分为独立阶段,解耦和独立扩展 → M9 数据处理
- 死信队列:失败消息的隔离和重试机制,保证数据不丢失 → M5 消息队列
- 全文搜索引擎:Elasticsearch 索引、分词、高亮的基本使用 → M10 搜索
- 可观测性:多阶段系统的监控指标设计和可视化 → M15 可观测性
V6:单机网络带宽和CPU都是瓶颈
Section titled “V6:单机网络带宽和CPU都是瓶颈”爬虫系统的日抓取量要求达到 100 万页。单机的网络带宽跑满了 1Gbps,CPU 使用率 95%——HTML 解析和内容处理太吃资源。 更大的问题是:1000 个目标站点中有些是同一个域名的不同路径,多个 worker 同时抓同一个域名会被封 IP。 你需要将爬虫分布到多台机器上,并且实现跨机器的域名级别协调。
你要解决什么
Section titled “你要解决什么”实现多机器分布式爬虫,用一致性哈希分配 URL,中心化协调器管理全局状态,跨 worker 域名限流。
给 AI 的 Prompt
Section titled “给 AI 的 Prompt”帮我将爬虫系统升级为多机器分布式架构,要求如下:
1. 技术栈:Go + PostgreSQL + Redis + RabbitMQ + Consul2. 分布式 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 的实时状态和负载情况
你学到了什么
Section titled “你学到了什么”- 一致性哈希:虚拟节点、最小迁移、域名亲和性分配 → M8 扩展性
- 分布式协调:服务注册与发现、心跳检测、主备选举 → M17 基础设施
- 跨节点限流:Redis 滑动窗口实现全局共享的速率限制 → M8 扩展性
- 网络优化:连接池复用、压缩传输、DNS 缓存的实用技巧 → M13 网络基础
- 分布式系统容错:worker 故障转移、协调器高可用的设计 → M16 DevOps