做数据采集的同学一定经历过这种绝望辛辛苦苦爬了一天入库前跑个去重脚本结果内存直接爆掉或者用set()勉强扛住第二天重启任务又从头开始重复采集更糟的是URL明明不同内容却一模一样存了一堆冗余数据浪费存储和算力。很多人把去重简单等同于“判断有没有见过”但在生产环境中去重是一个需要兼顾准确性、内存效率、持久化和业务语义的系统工程。set()在万级数据下够用到了百万、千万级就是灾难。这篇文章不讲基础语法只分享我在电商商品采集、新闻舆情聚合两个项目中实际落地的去重方案包含完整的选型逻辑、代码模板和性能实测数据。合规提醒本文技术方案仅用于合法授权的数据采集与内部研究。所有案例均已脱敏严禁用于未授权抓取、隐私数据获取或违反目标站点服务条款的行为。数据采集前请务必完成合规评估。一、 先搞清楚你到底要去重什么动手写代码前必须先明确去重的对象和粒度。这是很多项目返工的根源。去重类型典型场景核心挑战推荐方案URL去重防止同一链接重复请求参数顺序、UTM追踪码干扰规范化布隆过滤器内容指纹去重不同URL相同正文转载/分页计算开销、近似匹配SimHash/MinHash业务实体去重同一商品/用户多条记录字段组合、模糊匹配数据库唯一约束ETL增量去重定时任务避免重复采集历史数据状态持久化、断点续传Redis Set 时间窗口关键认知没有一种方案能通吃所有场景。URL去重解决“不重复请求”内容去重解决“不重复存储”业务去重解决“不重复使用”。三者往往需要组合使用而不是互相替代。二、 URL去重从字符串比较到规范化处理直接用原始URL做去重是最常见的错误。以下变体本质是同一个资源https://example.com/product?id123sourceadhttps://example.com/product?sourceadid123https://EXAMPLE.COM/Product?ID123utm_campaignspring2.1 URL规范化三要素fromurllib.parseimporturlparse,parse_qs,urlencode,urlunparsedefnormalize_url(url:str)-str:parsedurlparse(url)# 1. 协议域名小写schemeparsed.scheme.lower()netlocparsed.netloc.lower()# 2. 查询参数排序 移除追踪参数paramsparse_qs(parsed.query,keep_blank_valuesTrue)tracking_keys{utm_source,utm_medium,utm_campaign,ref,source}filtered{k:vfork,vinparams.items()ifknotintracking_keys}sorted_queryurlencode(sorted(filtered.items()),doseqTrue)# 3. 移除默认端口、尾部斜杠统一pathparsed.path.rstrip(/)or/returnurlunparse((scheme,netloc,path,,sorted_query,))2.2 大规模URL去重布隆过滤器当URL量级超过百万set()的内存占用会线性增长每个URL字符串哈希表开销。布隆过滤器用极小的空间代价换取O(1)查询误判率可控frompybloom_liveimportBloomFilter# 容量1000万误判率0.1%内存约17MBurl_filterBloomFilter(capacity10_000_000,error_rate0.001)defis_new_url(url:str)-bool:normalizednormalize_url(url)ifnormalizedinurl_filter:returnFalseurl_filter.add(normalized)returnTrue注意事项布隆过滤器不支持删除适合“只增不改”的采集场景误判意味着可能漏采需根据业务容忍度调整error_rate持久化可用url_filter.tofile()保存重启后加载恢复状态。三、 内容去重当URL不同但正文相同这是最容易被忽视、也最浪费资源的环节。新闻转载、商品多店铺铺货、论坛回帖引用都会产生大量内容重复。3.1 SimHash近似去重的性价比之王SimHash将文本压缩为64位指纹汉明距离≤3即判定为相似。相比MD5精确匹配它能识别改写、删减、拼接等轻度变异fromsimhashimportSimhashdeftext_fingerprint(text:str)-Simhash:# 预处理去标点、停用词、转小写cleanedpreprocess(text)returnSimhash(cleaned,f64)defis_similar(sh1:Simhash,sh2:Simhash,threshold:int3)-bool:returnsh1.distance(sh2)threshold工程优化点只对正文计算指纹先用trafilatura/DistilBERT提取正文避免导航栏、广告干扰分桶加速检索将64位指纹按每16位分4桶同桶内才计算汉明距离避免全量比对阈值动态调整短文本200字阈值设为2长文本设为3减少误判。3.2 MinHash LSH海量文档的亚线性检索当文档量超过百万SimHash两两比对仍是O(n²)。MinHash结合局部敏感哈希LSH可将检索复杂度降至O(n)fromdatasketchimportMinHash,MinHashLSH lshMinHashLSH(threshold0.8,num_perm128)defadd_document(doc_id:str,text:str):mhMinHash(num_perm128)forwordintext.split():mh.update(word.encode(utf-8))lsh.insert(doc_id,mh)deffind_similar(text:str)-list[str]:mhMinHash(num_perm128)forwordintext.split():mh.update(word.encode(utf-8))returnlsh.query(mh)适用场景新闻聚合、论文查重、评论去水军。代价是内存高于SimHash适合对召回率要求高的场景。四、 持久化与增量让去重跨会话生效内存中的去重结构重启即丢失。生产环境必须考虑状态持久化和增量策略。4.1 Redis兼顾速度与持久化的首选importredis rredis.Redis(hostlocalhost,port6379,decode_responsesTrue)classPersistentDeduplicator:def__init__(self,key_prefix:str,ttl:int86400*7):self.key_prefixkey_prefix self.ttlttl# 7天过期避免无限膨胀defis_new(self,identifier:str)-bool:keyf{self.key_prefix}:{identifier}# SET NX原子操作并发安全addedr.set(key,1,nxTrue,exself.ttl)returnbool(added)优势支持TTL自动清理历史数据适配周期性采集原子操作避免多线程/分布式环境下的竞态条件可横向扩展支撑亿级去重。4.2 SQLite单机轻量级持久化当数据量500万且无需分布式时SQLite比Redis更省心importsqlite3 connsqlite3.connect(dedup.db)conn.execute(CREATE TABLE IF NOT EXISTS seen (hash TEXT PRIMARY KEY))defis_new_sqlite(h:str)-bool:try:conn.execute(INSERT INTO seen (hash) VALUES (?),(h,))conn.commit()returnTrueexceptsqlite3.IntegrityError:returnFalse注意写入频繁时开启WAL模式PRAGMA journal_modeWAL避免锁竞争拖慢采集主流程。五、 性能实测四种方案横向对比测试环境MacBook Pro M2 / Python 3.11 / 100万条URL 50万篇中文文本方案内存占用插入QPS查询QPS持久化近似匹配适用规模set()2.1 GB85万90万❌❌50万BloomFilter17 MB120万130万✅文件❌5000万SimHash 分桶380 MB8万12万✅✅200万文档Redis SET服务端管理6万8万✅❌无上限MinHash LSH1.8 GB2万5万✅✅500万文档结论URL去重优先选布隆过滤器性价比最高内容去重首选SimHash除非对召回率有极致要求需要跨进程/分布式时Redis是唯一可靠选择set()仅适用于原型验证和小规模脚本。六、 避坑清单这些教训价值百万不要在采集主线程做重型去重SimHash/MinHash计算应异步化或放入独立Worker避免阻塞请求不要忽略编码一致性同一文本UTF-8和GBK的指纹完全不同入库前务必统一编码不要盲目追求零误判布隆过滤器的误判率与内存成反比根据业务容忍度权衡0.1%通常足够不要忘记去重本身的去重多个采集节点共享去重状态时确保标识符生成逻辑一致否则各自为政不要跳过质量校验去重后的数据仍需Schema验证避免因指纹碰撞导致有效数据被误删不要永久保留去重状态设置合理的TTL或归档策略避免存储无限膨胀拖垮系统。七、 总结去重看似是采集链路中最简单的环节实则是数据质量的第一道防线。它考验的不是算法功底而是对业务语义的理解、对工程约束的权衡、对异常边界的预判。从set()到布隆过滤器从MD5到SimHash从内存到Redis每一次升级都不是为了炫技而是为了让系统在真实世界的混沌中保持稳定、高效、可信。当你不再问“怎么去重”而是思考“在这个场景下什么样的去重策略能让数据价值最大化”时才算真正跨过了这道门槛。技术终究是为业务服务的。能让下游分析师少花80%时间清洗数据让存储成本降低60%让采集任务稳定运行30天无需人工干预——这才是去重真正的价值所在。