news 2026/8/10 1:49:59

爬虫爬了3亿URL去重标记炸了?Python布尔数组从set到位数组的踩坑实录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
爬虫爬了3亿URL去重标记炸了?Python布尔数组从set到位数组的踩坑实录

「部分情节为虚构演绎,仅供参考」

做爬虫的同行都知道一个经典问题:URL去重。你爬一个大站,链接之间互相引用,爬虫很容易在同一个URL上反复抓取。所以你需要一个「已访问集合」——每抓到一个URL,先查一下有没有访问过,没访问过就加入队列并标记为已访问。

小规模的时候,一个set()就搞定了:

visited=set()defshould_fetch(url):ifurlinvisited:returnFalsevisited.add(url)returnTrue

跑几十万URL,丝滑。几百万URL,还行。然后我接了个活,要爬一个电商站的全量商品。估算了一下,全站URL大约3亿个set()里存3亿个URL字符串,内存直接干到几十GB。不是我夸张,Python的set每个元素要存哈希值、指针、字符串对象本身,一个URL平均几十字节,3亿个就是十几GB起步。

「去重」变成了「去重负」——内存负、时间负、心态负。

各种去重方案,数据一上量全翻车

方案一:set存URL字符串

visited=set()# 3亿个URL,每个平均50字节,加上set开销...

3亿个URL字符串,光字符串本身就要约15GB,加上set的哈希表开销(每个entry约72字节),总内存轻松超过30GB。我的32GB服务器直接OOM。而且set的in操作虽然平均O(1),但3亿个元素时哈希冲突增多,缓存命中率极低,实际速度也不快。

方案二:URL哈希后存set

既然URL太长,那就哈希一下嘛。用MD5或SHA1把URL映射成固定长度的哈希值:

importhashlib visited=set()defmark(url):h=hashlib.md5(url.encode()).digest()# 16字节visited.add(h)

MD5是16字节,比URL字符串短多了。但3亿个16字节的bytes对象,加上set开销,每个entry约88字节,总内存约26GB。还是爆。而且MD5有碰撞风险——两个不同URL可能哈希到同一个值,导致漏爬。虽然概率极低,但生产环境谁敢赌?

方案三:布隆过滤器

布隆过滤器是URL去重的经典方案:

frompybloom_liveimportBloomFilter visited=BloomFilter(capacity=300_000_000,error_rate=0.001)

3亿个URL,错误率0.1%,布隆过滤器只需要约540MB。内存从30GB降到540MB,香爆了。但布隆过滤器有两个致命问题:

  1. 有误判率:它可能把没访问过的URL判为已访问(假阳性),导致漏爬。0.1%听起来低,但3亿URL就是300万个误判;
  2. 不支持删除:标准布隆过滤器只能加不能删,如果你的爬虫需要重新访问某些URL(比如内容更新了),做不到。

更要命的是,布隆过滤器不告诉你哪些URL访问过,它只能回答「可能访问过」或「肯定没访问过」。如果你需要遍历已访问URL列表(比如做断点续爬),布隆过滤器帮不了你。

方案四:位图(bitmap)标记

换个思路:如果URL有自增ID(比如商品ID从1到3亿),那直接用一个布尔数组标记就行:

visited=[False]*300_000_000# 3亿个布尔值

list[bool]3亿个元素,每个8字节指针,就是2.4GB。换成bytearray

visited=bytearray(300_000_000)# 3亿字节,约280MB

280MB,比布隆过滤器还省,而且零误判、支持删除、支持遍历。但问题来了:如果ID空间是稀疏的呢?比如商品ID不是连续的,最大ID是100亿但实际只有3亿个商品,那bytearray(10_000_000_000)就要9.3GB,又爆了。

方案五:numpy布尔数组

importnumpyasnp visited=np.zeros(300_000_000,dtype=np.bool_)

3亿个bool_,约280MB,和bytearray一样。numpy的向量化操作快,但定长不支持动态扩展,稀疏ID空间照样浪费。

小结

方案3亿URL内存误判删除遍历稀疏ID
set存URL~30GB支持支持支持
set存MD5~26GB碰撞支持支持支持
布隆过滤器~540MB有假阳性不支持不支持支持
bytearray~280MB支持支持不支持
numpy~280MB不支持支持不支持

各有各的死穴。set内存炸,布隆有误判,位图不支持稀疏ID。

破局思路:位图为什么不能「自动伸缩」

内存墙:280MB和2.4GB的差距不只是内存

你可能觉得280MB和2.4GB就是差了点内存,没什么大不了。但在实际运行中,这个差距是数量级的。CPU缓存的层级是这样的:L1缓存32KB、L2缓存256KB、L3缓存几十MB。280MB的数据虽然放不进L3,但至少能在主存里快速访问。2.4GB的数据呢?操作系统开始用Swap(磁盘虚拟内存),速度直接从每秒几十GB掉到每秒几MB。

这就是内存墙。不是内存不够用的问题,是数据离CPU太远的问题。省内存的真正意义是让数据待在更快的存储层级里。

再强调一次:时间和空间不是守恒的。省内存不会自动变快,但省内存让数据进入更快的存储层级,缓存命中率提高,这才是变快的原因。

「自动变速箱」构想

我盯着那张对比表想:位图280MB零误判,但稀疏ID空间浪费;set支持稀疏但内存爆炸。能不能搞一个根据密度自动切换存储方式的布尔数组?

  • ID密集的时候,用位图紧凑存储,280MB搞定;
  • ID稀疏的时候,只记录已访问的ID(True的位置),内存和set一样省;
  • 密度变了就自动「换挡」。

连续密集

稀疏分散

URL ID

ID密度判断

位图模式:bytearray

稀疏模式:array存已访问ID

统一数组API

visited[i]访问/标记

关键设计:换挡只在创建数组和调用optimize()时发生。爬虫标记URL是高频操作(每秒可能标记几万个),如果每次标记都检查密度并可能触发换挡,那性能就完了。所以平时标记就待在当前挡位,等爬虫跑完一批或者你主动调optimize()的时候再换挡。

我觉得这个想法太妙了,当晚就开始写代码。

自己造轮子,十二天踩坑日记

  • 第一天:写了个BoolArray类,密集用bytearray,稀疏用array(‘I’)存下标,能跑。
  • 第二天:换挡阈值50%,结果ID分布在阈值附近波动时疯狂来回切,性能比不切还差。
  • 第三天:加了滞回区间,但判断逻辑写错,密集区和稀疏区数据对不上,标记了的URL查不到。
  • 第四天:稀疏区用array(‘I’)存ID,但ID超过2^32时越界(32位无符号最大42亿),静默溢出。
  • 第五天:想支持visited[start:end:step]批量标记,结果切片和稀疏区下标表完全对不上。
  • 第六天:按位取反(求未访问URL)写出来了,但取反后count(True)对不上——稀疏区取反后忘了交换True和False的语义。
  • 第七天:in操作支持了,但稀疏区用二分查找,密集区用位图直接查,两种路径返回值不一致。
  • 第八天:缓存了已访问数量,批量标记后缓存没更新,数字忽大忽小。
  • 第九天:optimize()写好了,但3亿数据一换挡就卡好几秒,期间爬虫全阻塞。
  • 第十天:pickle序列化存盘做断点续爬,读回来内部结构全乱。
  • 第十一天:写了rindex(找最后一个已访问URL),稀疏区返回的是下标表里的位置,不是真实ID。
  • 第十二天:发现还有一堆并发问题、内存对齐问题、大端小端问题,心态彻底崩了。

第十二天晚上,我意识到一个人写一个生产级的混合布尔数组,不是十二天能搞定的。去社区发帖。

转机:发帖求助,评论区集体推荐

帖子发出去,标题是:

「3亿URL去重,set爆内存、布隆有误判、位图不支持稀疏ID,怎么办?」

评论区第一条高赞直接点醒我:

「你要的就是bool-hybrid-array。它换挡只在创建和optimize()时发生,平时标记不换挡,所以不会抖。你之前写的换挡逻辑之所以崩,是因为你把换挡做成了每次标记都可能触发的高频操作——换挡是低频的,别跟标记混在一起。」

后面全是推荐:

  • pip install bool-hybrid-array,你这个爬虫去重场景它天生适合。」
  • 「稀疏场景内存和set一样省,密集场景和numpy一样快。」
  • memory_usage(detail=True)看真实内存,数字不骗人。」
  • 「密集区底层是numpy,稀疏区用array存下标,都是成熟方案。」
  • 「月下载过万,不是玩具。」
  • np.array(arr)直接转numpy,接你现有pipeline。」
  • 「MIT协议,商用随便。」
  • 「Python 3.9到3.14全支持。」
  • findrindex返回的是真实位置,不是下标表位置。」

说实话看着像水军,但我直接跑代码验:

frombool_hybrid_arrayimportBoolHybridArr# 模拟3亿URL ID空间,实际只有1%被访问(稀疏场景)visited=BoolHybridArr(Falsefor_inrange(3_000_000_000))# 标记一些已访问的URLforurl_idin[12345,67890,111111,222222,333333]:visited[url_id]=Truevisited.optimize()print(visited.memory_usage(detail=True))

跑出来的数字:稀疏场景下3亿布尔值只占几MB。我用tracemalloc独立验证,对得上。

memory_usage(detail=True)是库自己算的。我用tracemalloc测出来跟它一致,但「一致」不等于「永远一致」。

别信我,别信它,信你自己的测量。

同类方案横向对比

RoaringBitmap:集合运算之王

爬虫去重本质上就是「维护一个已访问ID集合」,RoaringBitmap是这个领域的工业标准:

fromroaringbitmapimportRoaringBitmap visited=RoaringBitmap()visited.add(12345)print(12345invisited)

它的优势:稀疏场景内存极省,集合运算(并交差)极快,Lucene/Spark都在用。但它的局限:不是数组。没有visited[i] = True这种按位置赋值的语义,不支持append/pop,不保留长度。

爬虫去重如果只需要「判断在不在」,RoaringBitmap完美;但如果你需要数组语义(比如按ID范围批量标记、求未访问URL列表),用起来就别扭。

完整对比表

方案3亿稀疏(1%)内存数组语义批量标记零误判支持删除爬虫去重适配
set存MD5~26GB❌ 集合碰撞风险内存爆炸
布隆过滤器~540MB❌ 假阳性有误判
bytearray~2.8GB(连续ID)稀疏ID浪费
numpy~280MB(连续ID)稀疏ID浪费
bitarray~35MB(连续ID)⚠️⚠️稀疏ID浪费
RoaringBitmap~3MB(只存已访问)❌ 集合集合场景最佳
bool-hybrid-array~3MB(稀疏区)数组+稀疏自适应

中立Benchmark

指标set布隆numpybool-hybrid-array
内存(1%稀疏)~26GB~540MB~280MB~3MB
单次标记O(1)哈希O(k)位运算O(1)O(1)
单次查询O(1)O(k)O(1)O(1)~O(log n)
批量标记1万次~0.01s~0.005s~0.001s~0.002s
遍历已访问O(n)不支持O(n)全扫O(k)只扫稀疏区
误判率碰撞极低0.1%00

怎么读:稀疏场景bool-hybrid-array内存和RoaringBitmap一个量级(~3MB),但保留了数组语义;密集场景自动切位图,速度和numpy一样;遍历已访问URL只扫稀疏区,不用全量遍历。

注意:均匀分布(50/50)是它和numpy打平的场景。但爬虫去重是典型的稀疏场景(已访问URL占少数),所以优势极大。

缺点与适用边界

第一,optimize()是低频操作。爬虫标记URL时别调optimize(),等一批URL爬完再调。频繁调等于频繁全量重建,性能崩。

第二,换挡瞬间O(n)。3亿数据从稀疏切位图要遍历整个数组,可能几秒。但爬虫场景只在初始化和批量处理后调,可接受。

第三,非线程安全。多线程爬虫并发标记要加锁。分布式爬虫建议每个worker用自己的实例,最后合并。

第四,生态年轻。文档和社区不如numpy成熟,冷门问题可能得看源码。

第五,均匀分布打平。50/50场景和numpy内存差不多,没有优势。但爬虫去重已访问URL永远是少数,不存在这个问题。

第六,memory_usage是自报数据。我用tracemalloc验证过,但生产环境请自己测。

适用场景:稀疏布尔标记 + 需要数组语义 + 零误判 + 支持删除/遍历。爬虫去重、用户在线状态、消息已读标记、特征工程的布尔特征列。

不适用场景:纯集合运算且不需要数组语义(用RoaringBitmap)、允许误判且内存极度敏感(用布隆过滤器)、均匀分布定长密集数组(用numpy)。

写在最后

bool-hybrid-array做爬虫URL去重后,3亿URL空间只占几MB内存(稀疏场景),零误判,支持删除和遍历,速度和set一样快。我甚至用它做了分布式爬虫的断点续爬——序列化存盘,下次启动直接load继续。

安装就一行:

pipinstallbool-hybrid-array

项目在Gitee和GitHub上都有(搜bool-hybrid-array),MIT协议。核心类BoolHybridArr,API和numpy高度兼容,np.array(arr)无缝接入。作者承诺no removal policy,现有公开接口不会删。但行为细节可能随版本变化,上生产前务必在你自己的数据上跑一遍

别信我,信你自己的测量。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/8 23:45:10

非官方API调用的随机化延迟(Jittering)调度优化

一、 背景:性能与风控的矛盾焦点 在执行批量任务(如批量创建 $1000$ 个外部群)时,自动化系统会连续发送大量的 API 请求。如果请求间隔是固定的(例如,每 $1000\text{ms}$ 一次),这种…

作者头像 李华
网站建设 2026/8/8 23:39:49

安信可PB-03F蓝牙模块烧录全攻略:从工具选择到实战避坑

1. 项目概述:安信可PB-03F模块烧录入门 如果你手头有一块安信可的PB-03F蓝牙模块,想要让它跑起来,第一件绕不开的事就是“烧录”。这听起来有点技术门槛,但说白了,就是把你写好的程序或者官方提供的固件,像…

作者头像 李华
网站建设 2026/8/8 23:37:56

解锁微信聊天数据的三种魔法:从记忆碎片到AI伙伴的奇妙旅程

解锁微信聊天数据的三种魔法:从记忆碎片到AI伙伴的奇妙旅程 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/…

作者头像 李华
网站建设 2026/8/8 23:36:01

大模型后训练实践指南:从SFT到RLHF的完整流程与避坑要点

这次我们来看一个来自 AI 研究社区的重要讨论:Nathan Lambert 发起的“后训练”教学反馈征集。这不是一个可以直接下载运行的软件或模型,而是一个关于如何更好地教授“后训练”这一关键 AI 技术环节的公开倡议。对于任何希望深入理解或实践大模型微调、R…

作者头像 李华
网站建设 2026/8/8 23:33:19

智能体框架选型成本差异分析:从LangChain到自研的5-30倍波动

这次我们来看一个关于智能体框架成本差异的技术话题。如果你正在规划或已经部署了基于大模型的智能体应用,那么框架选型可能直接导致你的项目成本产生5到30倍的波动。这不是危言耸听,而是架构决策中一个真实且容易被忽视的财务陷阱。本文将直接切入主题&…

作者头像 李华