不能直接用 Redis Set 存原始 URL,因语义等价但字符串不同的 URL 变体会导致去重失效;必须先归一化生成固定长度 SHA256 指纹,再经布隆过滤器预筛,Redis Set 仅作最终确认层。

直接用 redis.Redis().sadd() 做亿级去重会爆内存、拖慢速度、甚至阻塞 Redis —— 亿级 URL 必须做指纹归一化 + 布隆过滤器预筛,Redis Set 只作最终确认层。
为什么不能直接用 Redis Set 存原始 URL?
原始 URL 含大量语义等价但字符串不同的变体:参数顺序不同、大小写混用、末尾斜杠有无、utm_* 追踪参数、URL 编码差异(%20 vs 空格)等。直接存会导致同一页面被当成多个 URL,去重失效。
-
sadd("crawler:urls", "https://example.com/a?b=1&c=2")和sadd("crawler:urls", "https://example.com/a?c=2&b=1")被视为两个元素,但它们指向同一资源 - SHA256 指纹长度固定(64 字符),1 亿 URL 占约 6–7 GB 内存;若存原始 URL,平均长度超 80 字符,内存翻倍且哈希表冲突率上升
- Redis Set 查询虽为 O(1),但亿级 key 下内存碎片和 rehash 开销显著,
SADD延迟可能从 0.2ms 升至 1ms+,影响吞吐
必须做的 URL 指纹归一化步骤
归一化不是可选项,是亿级去重的前置生死线。跳过这步,后面所有优化都白搭。
- 用
urllib.parse.urlparse()拆解 URL,强制转小写:scheme、netloc - 用
posixpath.normpath()标准化path(如/a/../b→/b),并确保不以//开头 - 用
urllib.parse.parse_qsl()解析 query,过滤掉已知无意义参数(如utm_source、ref、timestamp),再按 key 字典序排序后urlencode() - 丢弃
fragment(#...部分),它不影响服务器请求 - 对归一化后的字符串用
hashlib.sha256().hexdigest()生成指纹 —— 别用md5或base64,抗碰撞能力不够
示例归一化后结果:https://example.com/blog/ + ?category=tech&page=1 → 9f86d081...e2eea9
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
立即学习“Python免费学习笔记(深入)”;
Redis Bloom Filter 是预筛核心,不是可选插件
亿级场景下,布隆过滤器(Bloom Filter)必须部署在 Redis 层前端,承担 99% 的“快速否定”判断,把真正需要查 Set 的请求压到最低。
- 必须用
RedisBloom模块(bf.add/bf.exists),不是纯 Python 的pybloom_live—— 后者无法共享状态,分布式无效 - 初始化时设好预期容量(
capacity)和误判率(error),例如BF.RESERVE crawler:bloom 100000000 0.001(支持 1 亿 URL,误判率 0.1%) - 流程必须是:
bf.exists("crawler:bloom", fingerprint) == False → 直接抓取;True → 再查 <code>redis.sadd("crawler:urls", fingerprint)确认 - 误判率 0.1% 意味着每 1000 个新 URL 中最多 1 个被误判为“已存在”,但不会漏判 —— 若业务不能容忍极低漏抓,就接受这点代价;想 100% 精确,就别碰亿级规模
连接与降级必须写进主逻辑,不能靠运气
生产环境里,ConnectionError 和 TimeoutError 出现频率远高于本地测试。没做容错的爬虫,在云网络抖动时会批量崩溃。
- 初始化 Redis 客户端必须带
socket_timeout=2、socket_connect_timeout=2、retry_on_timeout=True - 所有
bf.exists()和sadd()调用必须包在try/except redis.exceptions.RedisError:里 - 降级策略只有一条:失败时用本地
set()缓存本进程最近 1000 个指纹,仅用于临时避错,**绝不持久化、不跨进程、不作为兜底方案** - 连接池必须复用:
connection_pool=redis.BlockingConnectionPool(max_connections=32),别在每个子进程里新建Redis()实例
真正难的不是代码怎么写,而是归一化规则要覆盖目标站全部 URL 变体,以及 Bloom Filter 容量和误差率得根据实际爬取节奏动态调优 —— 这部分没法自动化,得靠日志抽样和误判率监控来反复校准。

















