用Redis存URL的MD5而非原始URL,因MD5长度固定、可标准化去重;但需先归一化URL(去斜杠、排序并解码query),再用SET的sismember+sadd原子操作判断插入,配合连接池防并发问题。

为什么用 Redis 存 URL 的 MD5 而不是原始 URL?
因为原始 URL 长度不固定,有的带动态参数、乱序 query、session_id,直接比对极易误判重复;MD5 固定 32 字符,标准化后去重更稳。但注意:hashlib.md5(url.encode()).hexdigest() 前必须统一处理 URL:去掉末尾斜杠、排序 query 参数、解码 URL 编码(urllib.parse.unquote),否则相同语义的 URL 会算出不同哈希。
常见错误现象:redis.exceptions.ConnectionError 或去重失效——多半是没做 URL 归一化,或 Redis 连接未复用(每次新建连接导致性能崩)。
- 归一化示例:
https://a.com/?b=1&a=2和https://a.com/?a=2&b=1必须转成同一串 MD5 - 别用
url.strip('/') + '?' + '&'.join(sorted(query_pairs))这种手写拼接,改用urllib.parse.urlunparse+urllib.parse.parse_qs安全重组 - Redis 连接建议用单例或连接池(
redis.ConnectionPool),避免爬虫并发高时连接耗尽
如何用 Redis 的 SET 实现高效去重判断?
用 SET 类型存 MD5,靠 redis_client.sismember('urls_seen', md5) 判断是否已存在,再用 redis_client.sadd('urls_seen', md5) 尝试插入——原子操作,天然防竞态。别用 GET/SET 组合,那是自己造 race condition。
性能影响明显:单次判断 + 插入平均 redis_client.expire('urls_seen', 604800),否则集合无限增长。
立即学习“Python免费学习笔记(深入)”;
- 别把整个 URL 当 key(如
redis_client.get(url)),key 长度超限且无法批量查 - 避免用
HSET存 URL→状态映射,纯属浪费内存和 CPU - 如果要支持清理旧数据,改用
zset+ 时间戳分值,但增量爬取通常不需要这么复杂
增量爬取时怎么保证“新 URL 不漏、旧 URL 不重”?
核心逻辑就一句:先查 MD5 是否存在,不存在才请求 + 解析 + 入库 + 写入 Redis。关键在“解析”环节——新页面里提取的子链接,必须同样走一遍归一化 + MD5 + sismember 流程,不能跳过。
容易踩的坑是“只对种子 URL 做去重,子链接放行”。结果就是首页抓 10 次,每次都把全站链接又扫一遍。还有人把去重逻辑写在入库后,导致重复请求已失效的 URL(比如 404 页面反复抓)。
- 所有待爬 URL(无论来源)必须在
requests.get()前完成去重校验 - 如果用 Scrapy,把去重逻辑塞进
start_requests和parse里的response.follow()之前 - Redis 写失败(如网络抖动)不能静默吞掉,得 fallback 到内存 set() 或抛异常中断,否则漏去重
Redis 挂了或网络超时怎么办?
别让整个爬虫停摆。加一层简单容错:try/except redis.ConnectionError,捕获后降级到本地 set() 缓存本次运行的 MD5,同时打日志告警。但注意:本地 set 只能保本次进程内去重,重启即丢,所以必须标清楚“降级模式”,避免误以为长期可靠。
另一个现实问题是 Redis 内存爆了——如果爬的站点 URL 量极大(千万级),SET 内存占用线性增长,这时得考虑布隆过滤器(pyreBloom 或 RedisBloom 模块),但会引入误判率(false positive)。普通中小规模爬取,坚持用 SET 最省心。
- 别在 except 里 sleep(1) 后重试,可能拖慢整个爬虫节奏
- 本地降级的
seen_urls = set()必须是模块级变量,不能每次函数调用都新建 - 如果用 Docker 部署,检查 Redis 容器内存 limit,
docker stats看实际 RSS 占用
sismember,而是 URL 归一化的边界 case——比如带 fragment 的 URL 是否该去掉 #xxx,比如大小写敏感的域名要不要强制小写。这些细节不抠准,Redis 再快也白搭。










