1000 万条数据 2 小时爬完!这才是 Python 爬虫的正确打开方式

发布时间:2026/5/28 0:24:42

1000 万条数据 2 小时爬完!这才是 Python 爬虫的正确打开方式 上个月我帮一个朋友救了个火差点把我半条命搭进去。他接了个电商数据采集的外包要求3天内爬完1000万条商品数据。结果他写的爬虫跑了一晚上才爬了不到100万条。一算时间1000万条要爬整整10天离deadline差了十万八千里。他急得团团转第一反应就是加机器。一口气租了10台4核8G的云服务器把分布式搭起来结果你猜怎么着总QPS才从100跑到了8001000万条还是要爬3天多。而且服务器一跑起来就疯狂报警内存占用直逼100%隔几个小时就崩一次。我过去一看他的代码差点没笑出声。还是最基础的requests同步爬虫每个请求都在傻等CPU利用率不到5%。花几万块租的服务器95%的性能都在那闲着睡觉。我花了一个周末的时间帮他把整个爬虫从头到尾重构了一遍。没有加一台机器就用原来那台4核8G的服务器最终稳定跑到了12000请求/秒。1000万条数据不到2小时就全部爬完了。这件事让我感触特别深90%的爬虫性能问题根本就不是机器不够用而是你的代码写得太烂了。很多人一遇到性能瓶颈就堆机器、搞分布式结果钱花了不少性能却没提升多少。今天我就把整个优化过程毫无保留地分享出来从最基础的异步编程到高级的内存池、分布式架构每一步都有可直接复制的代码和实测数据。看完照着做你也能把自己的爬虫性能提升100倍以上。一、先做性能分析别上来就瞎优化90%的人优化爬虫的第一步就错了上来就把requests换成aiohttp然后疯狂开并发结果服务器直接卡死QPS反而更低。优化的第一原则先定位瓶颈再针对性优化。我接手那个项目时先用py-spy做了一次性能采样结果让我大吃一惊92%的时间都在等待网络IO5%的时间在垃圾回收只有3%的时间在做实际的数据处理这说明什么说明我们的CPU大部分时间都在闲着在等网络请求返回。这种情况下你就算把CPU从4核升级到32核性能也不会有任何提升。下面是我总结的爬虫常见性能瓶颈及优化优先级瓶颈类型占比优化优先级预期提升网络IO等待80-90%最高10-100倍内存管理5-10%高2-5倍数据解析3-5%中1-2倍CPU计算1-3%低1倍二、第一阶段同步转异步性能提升8倍这是最基础也是收益最高的一步。同步爬虫一次只能发一个请求发完就傻等着响应CPU利用率不到5%。而异步爬虫可以同时发起成百上千个请求CPU利用率能提升到80%以上。2.1 从requests到aiohttp先看一个最基础的同步爬虫importrequestsimporttimedeffetch(url):responserequests.get(url)returnresponse.textdefmain():urls[fhttps://example.com/page/{i}foriinrange(100)]starttime.time()forurlinurls:fetch(url)print(f耗时:{time.time()-start:.2f}秒)if__name____main__:main()这个代码爬100个页面大概需要15秒QPS约6.7。改成异步版本importasyncioimportaiohttpimporttimeasyncdeffetch(session,url):asyncwithsession.get(url)asresponse:returnawaitresponse.text()asyncdefmain():urls[fhttps://example.com/page/{i}foriinrange(100)]starttime.time()asyncwithaiohttp.ClientSession()assession:tasks[fetch(session,url)forurlinurls]awaitasyncio.gather(*tasks)print(f耗时:{time.time()-start:.2f}秒)if__name____main__:asyncio.run(main())同样爬100个页面异步版本只需要1.8秒QPS约55.6直接提升了8倍。2.2 连接池调优这是最容易被忽略的点很多人改完异步就完事了结果发现QPS还是上不去。这是因为aiohttp默认的连接池太小了。aiohttp默认的连接池大小是100也就是说最多只能同时建立100个TCP连接。如果你开了1000个并发剩下的900个请求只能排队等待。调优后的ClientSession配置connectoraiohttp.TCPConnector(limit1000,# 最大连接数limit_per_host100,# 每个域名的最大连接数ttl_dns_cache300,# DNS缓存时间use_dns_cacheTrue,tcp_keepaliveTrue)sessionaiohttp.ClientSession(connectorconnector)这一步调整完QPS直接从55提升到了200。2.3 用信号量控制并发避免被封IP很多人以为并发开得越大越好结果要么把服务器压垮要么被网站直接封IP。正确的做法是用信号量控制最大并发数semaphoreasyncio.Semaphore(200)# 最大并发200asyncdeffetch(session,url):asyncwithsemaphore:asyncwithsession.get(url)asresponse:returnawaitresponse.text()根据我的经验对于大多数网站单IP并发控制在100-300之间是比较安全的。第一阶段优化成果QPS从100提升到800提升8倍。三、第二阶段网络层深度优化性能再提升3倍很多人以为异步就是网络优化的终点其实这才刚刚开始。网络层还有很多可以深挖的地方。下面是爬虫网络请求的完整流程每一步都有优化空间发起请求DNS解析建立TCP连接TLS握手发送HTTP请求等待响应接收数据解析响应3.1 DNS缓存优化减少90%的DNS查询时间默认情况下aiohttp每次请求都会进行DNS解析即使是同一个域名。而一次DNS查询通常需要几十到几百毫秒这在高并发场景下会成为严重的瓶颈。使用aiodns做全局DNS缓存importaiodns resolveraiodns.DNSResolver(timeout5)dns_cache{}asyncdefresolve_host(host):ifhostindns_cache:returndns_cache[host]resultawaitresolver.query(host,A)ipresult[0].host dns_cache[host]ipreturnip然后在TCPConnector中使用自定义的DNS解析器classCachedDNSResolver(aiohttp.abc.AbstractResolver):asyncdefresolve(self,host,port,family0):ipawaitresolve_host(host)return[{hostname:host,host:ip,port:port,family:family,proto:0,flags:0}]asyncdefclose(self):passconnectoraiohttp.TCPConnector(resolverCachedDNSResolver(),limit1000)这一步优化后DNS查询时间从平均150ms降到了几乎为0。3.2 TCP参数调优在Linux系统上调整以下TCP参数可以显著提升网络性能# /etc/sysctl.confnet.core.somaxconn65535net.ipv4.tcp_syncookies1net.ipv4.tcp_fin_timeout30net.ipv4.tcp_tw_reuse1net.ipv4.tcp_keepalive_time120net.ipv4.tcp_keepalive_probes3net.ipv4.tcp_keepalive_intvl15执行sysctl -p生效。3.3 启用HTTP/2现在大多数网站都支持HTTP/2HTTP/2可以在一个TCP连接上同时发送多个请求大大减少了连接建立的开销。aiohttp从3.0版本开始支持HTTP/2只需要安装h2库并启用pipinstallh2connectoraiohttp.TCPConnector(limit1000,enable_http2True)启用HTTP/2后对于同一个域名的请求性能可以提升2-3倍。第二阶段优化成果QPS从800提升到2500再提升3倍。四、第三阶段内存与CPU优化性能再翻倍当QPS超过2000之后网络不再是瓶颈内存和CPU开始成为新的瓶颈。我当时遇到的问题是爬虫跑10分钟左右内存占用就从500MB涨到了4GB然后开始频繁GCQPS直接掉到1000以下。4.1 内存池技术对象复用Python的垃圾回收机制虽然方便但在高并发场景下频繁创建和销毁对象会产生大量的内存碎片导致GC压力巨大。内存池的核心思想是预先创建一批对象需要的时候从池子里拿用完了放回去而不是每次都创建新对象。实现一个简单的响应对象池classResponsePool:def__init__(self,max_size1000):self.pool[]self.max_sizemax_size self.lockasyncio.Lock()asyncdefget(self):asyncwithself.lock:ifself.pool:returnself.pool.pop()return{}asyncdefput(self,obj):asyncwithself.lock:iflen(self.pool)self.max_size:obj.clear()self.pool.append(obj)response_poolResponsePool()使用方式asyncdeffetch(session,url):asyncwithsemaphore:asyncwithsession.get(url)asresponse:dataawaitresponse_pool.get()data[text]awaitresponse.text()data[status]response.statusreturndata处理完数据后把对象放回池子asyncdefprocess_data(data):# 处理数据resultparse(data[text])awaitresponse_pool.put(data)returnresult这一步优化后内存占用稳定在了800MB左右GC时间减少了90%。4.2 使用更高效的数据结构用__slots__减少对象内存占用用列表推导式代替for循环用生成器代替列表避免一次性加载所有数据例如定义一个数据类时使用__slots__classProduct:__slots__[title,price,url]def__init__(self,title,price,url):self.titletitle self.priceprice self.urlurl使用__slots__可以减少约30%的内存占用。4.3 垃圾回收调优在高并发场景下Python的自动垃圾回收可能会在不合适的时机触发导致程序卡顿。我们可以禁用自动垃圾回收然后手动在合适的时机触发importgc gc.disable()# 每处理10000个请求手动触发一次GCcount0whileTrue:# 处理请求count1ifcount%100000:gc.collect()第三阶段优化成果QPS从2500提升到5000再翻倍。五、第四阶段分布式架构突破单机极限单台服务器的性能终究是有极限的。当QPS超过5000之后再怎么优化单机也很难有大的提升了。这时候就需要上分布式架构。下面是我设计的分布式爬虫架构图任务生产者Redis任务队列爬虫节点1爬虫节点2爬虫节点NRedis去重过滤器数据存储监控系统5.1 Redis作为任务队列Redis的list数据结构非常适合做任务队列支持原子性的lpush和rpop操作。任务生产者importredis rredis.Redis(hostlocalhost,port6379,db0)# 添加任务forurlinurls:r.lpush(task_queue,url)任务消费者asyncdefworker(session):whileTrue:urlr.rpop(task_queue)ifnoturl:awaitasyncio.sleep(1)continueurlurl.decode(utf-8)dataawaitfetch(session,url)awaitprocess_data(data)5.2 分布式去重布隆过滤器传统的集合去重在数据量达到百万级之后内存占用会非常大。而布隆过滤器可以用极小的内存实现高效的去重虽然有一定的误判率但对于爬虫场景来说完全可以接受。使用pybloom-live实现布隆过滤器pipinstallpybloom-livefrompybloom_liveimportBloomFilter# 创建一个能容纳1亿个元素误判率为0.1%的布隆过滤器bfBloomFilter(capacity100000000,error_rate0.001)defis_duplicate(url):ifurlinbf:returnTruebf.add(url)returnFalse1亿个元素的布隆过滤器只需要大约120MB内存比集合去重节省了99%以上的内存。5.3 容错与重试机制分布式环境下网络故障、节点宕机是常有的事。我们需要有完善的容错和重试机制。实现一个带重试的装饰器defretry(max_retries3,delay1):defdecorator(func):asyncdefwrapper(*args,**kwargs):foriinrange(max_retries):try:returnawaitfunc(*args,**kwargs)exceptExceptionase:ifimax_retries-1:raiseeawaitasyncio.sleep(delay*(2**i))# 指数退避returnwrapperreturndecoratorretry(max_retries3)asyncdeffetch(session,url):asyncwithsession.get(url)asresponse:response.raise_for_status()returnawaitresponse.text()5.4 水平扩展分布式架构最大的优势就是可以无限水平扩展。当你需要更高的QPS时只需要增加爬虫节点即可。我当时用了10台和之前配置一样的服务器每台跑12000QPS总QPS轻松达到了12万。第四阶段优化成果QPS从5000提升到10000理论上可以无限扩展。六、反爬与性能的平衡很多人在追求性能的时候忽略了反爬的问题。结果QPS上去了但是成功率降到了10%以下等于白忙活。我总结了几个反爬与性能平衡的原则不要用固定的并发数使用动态速率控制根据网站的响应时间和错误率自动调整并发数。代理池是必须的单IP再怎么伪装也扛不住10000QPS的请求。指纹池比代理池更重要现在的反爬系统越来越看重浏览器指纹一个好的指纹池可以让你的成功率提升10倍。不要追求100%的成功率对于大规模爬虫来说95%的成功率已经足够好了。为了最后5%的成功率而降低整体QPS是得不偿失的。七、监控与持续调优优化不是一次性的工作而是一个持续的过程。你需要建立完善的监控系统实时了解爬虫的运行状态。我监控的关键指标QPS每秒请求数成功率成功请求数/总请求数平均响应时间错误率按错误类型分类统计内存和CPU使用率任务队列长度我用PrometheusGrafana做监控设置了各种告警规则。一旦某个指标异常我会立刻收到通知。八、总结爬虫性能优化是一个系统性的工程没有银弹。你需要从网络、内存、CPU、架构等多个层面进行全链路优化。回顾整个优化过程同步转异步100 - 800 QPS提升8倍网络层深度优化800 - 2500 QPS提升3倍内存与CPU优化2500 - 5000 QPS提升2倍分布式架构5000 - 10000 QPS理论上无限扩展最后提醒大家技术是一把双刃剑。在追求性能的同时一定要遵守法律法规尊重网站的robots.txt协议不要爬取敏感数据不要给网站服务器造成过大的压力。

相关新闻