当前位置: 首页 > news >正文

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

上个月我帮一个朋友救了个火,差点把我半条命搭进去。

他接了个电商数据采集的外包,要求3天内爬完1000万条商品数据。结果他写的爬虫跑了一晚上,才爬了不到100万条。一算时间,1000万条要爬整整10天,离deadline差了十万八千里。

他急得团团转,第一反应就是加机器。一口气租了10台4核8G的云服务器,把分布式搭起来,结果你猜怎么着?总QPS才从100跑到了800,1000万条还是要爬3天多。而且服务器一跑起来就疯狂报警,内存占用直逼100%,隔几个小时就崩一次。

我过去一看他的代码,差点没笑出声。还是最基础的requests同步爬虫,每个请求都在傻等,CPU利用率不到5%。花几万块租的服务器,95%的性能都在那闲着睡觉。

我花了一个周末的时间,帮他把整个爬虫从头到尾重构了一遍。没有加一台机器,就用原来那台4核8G的服务器,最终稳定跑到了12000请求/秒。1000万条数据,不到2小时就全部爬完了。

这件事让我感触特别深:90%的爬虫性能问题,根本就不是机器不够用,而是你的代码写得太烂了。很多人一遇到性能瓶颈就堆机器、搞分布式,结果钱花了不少,性能却没提升多少。

今天我就把整个优化过程毫无保留地分享出来,从最基础的异步编程到高级的内存池、分布式架构,每一步都有可直接复制的代码和实测数据。看完照着做,你也能把自己的爬虫性能提升100倍以上。

一、先做性能分析,别上来就瞎优化

90%的人优化爬虫的第一步就错了:上来就把requests换成aiohttp,然后疯狂开并发,结果服务器直接卡死,QPS反而更低。

优化的第一原则:先定位瓶颈,再针对性优化。

我接手那个项目时,先用py-spy做了一次性能采样,结果让我大吃一惊:

  • 92%的时间都在等待网络IO
  • 5%的时间在垃圾回收
  • 只有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):response=requests.get(url)returnresponse.textdefmain():urls=[f"https://example.com/page/{i}"foriinrange(100)]start=time.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=[f"https://example.com/page/{i}"foriinrange(100)]start=time.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配置:

connector=aiohttp.TCPConnector(limit=1000,# 最大连接数limit_per_host=100,# 每个域名的最大连接数ttl_dns_cache=300,# DNS缓存时间use_dns_cache=True,tcp_keepalive=True)session=aiohttp.ClientSession(connector=connector)

这一步调整完,QPS直接从55提升到了200+。

2.3 用信号量控制并发,避免被封IP

很多人以为并发开得越大越好,结果要么把服务器压垮,要么被网站直接封IP。

正确的做法是用信号量控制最大并发数:

semaphore=asyncio.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 resolver=aiodns.DNSResolver(timeout=5)dns_cache={}asyncdefresolve_host(host):ifhostindns_cache:returndns_cache[host]result=awaitresolver.query(host,'A')ip=result[0].host dns_cache[host]=ipreturnip

然后在TCPConnector中使用自定义的DNS解析器:

classCachedDNSResolver(aiohttp.abc.AbstractResolver):asyncdefresolve(self,host,port,family=0):ip=awaitresolve_host(host)return[{'hostname':host,'host':ip,'port':port,'family':family,'proto':0,'flags':0}]asyncdefclose(self):passconnector=aiohttp.TCPConnector(resolver=CachedDNSResolver(),limit=1000)

这一步优化后,DNS查询时间从平均150ms降到了几乎为0。

3.2 TCP参数调优

在Linux系统上,调整以下TCP参数可以显著提升网络性能:

# /etc/sysctl.confnet.core.somaxconn=65535net.ipv4.tcp_syncookies=1net.ipv4.tcp_fin_timeout=30net.ipv4.tcp_tw_reuse=1net.ipv4.tcp_keepalive_time=120net.ipv4.tcp_keepalive_probes=3net.ipv4.tcp_keepalive_intvl=15

执行sysctl -p生效。

3.3 启用HTTP/2

现在大多数网站都支持HTTP/2,HTTP/2可以在一个TCP连接上同时发送多个请求,大大减少了连接建立的开销。

aiohttp从3.0版本开始支持HTTP/2,只需要安装h2库并启用:

pipinstallh2
connector=aiohttp.TCPConnector(limit=1000,enable_http2=True)

启用HTTP/2后,对于同一个域名的请求,性能可以提升2-3倍。

第二阶段优化成果:QPS从800提升到2500,再提升3倍。

四、第三阶段:内存与CPU优化,性能再翻倍

当QPS超过2000之后,网络不再是瓶颈,内存和CPU开始成为新的瓶颈。

我当时遇到的问题是:爬虫跑10分钟左右,内存占用就从500MB涨到了4GB,然后开始频繁GC,QPS直接掉到1000以下。

4.1 内存池技术:对象复用

Python的垃圾回收机制虽然方便,但在高并发场景下,频繁创建和销毁对象会产生大量的内存碎片,导致GC压力巨大。

内存池的核心思想是:预先创建一批对象,需要的时候从池子里拿,用完了放回去,而不是每次都创建新对象。

实现一个简单的响应对象池:

classResponsePool:def__init__(self,max_size=1000):self.pool=[]self.max_size=max_size self.lock=asyncio.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_pool=ResponsePool()

使用方式:

asyncdeffetch(session,url):asyncwithsemaphore:asyncwithsession.get(url)asresponse:data=awaitresponse_pool.get()data['text']=awaitresponse.text()data['status']=response.statusreturndata

处理完数据后,把对象放回池子:

asyncdefprocess_data(data):# 处理数据result=parse(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.title=title self.price=price self.url=url

使用__slots__可以减少约30%的内存占用。

4.3 垃圾回收调优

在高并发场景下,Python的自动垃圾回收可能会在不合适的时机触发,导致程序卡顿。

我们可以禁用自动垃圾回收,然后手动在合适的时机触发:

importgc gc.disable()# 每处理10000个请求手动触发一次GCcount=0whileTrue:# 处理请求count+=1ifcount%10000==0:gc.collect()

第三阶段优化成果:QPS从2500提升到5000,再翻倍。

五、第四阶段:分布式架构,突破单机极限

单台服务器的性能终究是有极限的。当QPS超过5000之后,再怎么优化单机也很难有大的提升了。这时候就需要上分布式架构。

下面是我设计的分布式爬虫架构图:

任务生产者

Redis任务队列

爬虫节点1

爬虫节点2

爬虫节点N

Redis去重过滤器

数据存储

监控系统

5.1 Redis作为任务队列

Redis的list数据结构非常适合做任务队列,支持原子性的lpush和rpop操作。

任务生产者:

importredis r=redis.Redis(host='localhost',port=6379,db=0)# 添加任务forurlinurls:r.lpush('task_queue',url)

任务消费者:

asyncdefworker(session):whileTrue:url=r.rpop('task_queue')ifnoturl:awaitasyncio.sleep(1)continueurl=url.decode('utf-8')data=awaitfetch(session,url)awaitprocess_data(data)

5.2 分布式去重:布隆过滤器

传统的集合去重在数据量达到百万级之后,内存占用会非常大。而布隆过滤器可以用极小的内存实现高效的去重,虽然有一定的误判率,但对于爬虫场景来说完全可以接受。

使用pybloom-live实现布隆过滤器:

pipinstallpybloom-live
frompybloom_liveimportBloomFilter# 创建一个能容纳1亿个元素,误判率为0.1%的布隆过滤器bf=BloomFilter(capacity=100000000,error_rate=0.001)defis_duplicate(url):ifurlinbf:returnTruebf.add(url)returnFalse

1亿个元素的布隆过滤器只需要大约120MB内存,比集合去重节省了99%以上的内存。

5.3 容错与重试机制

分布式环境下,网络故障、节点宕机是常有的事。我们需要有完善的容错和重试机制。

实现一个带重试的装饰器:

defretry(max_retries=3,delay=1):defdecorator(func):asyncdefwrapper(*args,**kwargs):foriinrange(max_retries):try:returnawaitfunc(*args,**kwargs)exceptExceptionase:ifi==max_retries-1:raiseeawaitasyncio.sleep(delay*(2**i))# 指数退避returnwrapperreturndecorator@retry(max_retries=3)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%以下,等于白忙活。

我总结了几个反爬与性能平衡的原则:

  1. 不要用固定的并发数:使用动态速率控制,根据网站的响应时间和错误率自动调整并发数。
  2. 代理池是必须的:单IP再怎么伪装,也扛不住10000QPS的请求。
  3. 指纹池比代理池更重要:现在的反爬系统越来越看重浏览器指纹,一个好的指纹池可以让你的成功率提升10倍。
  4. 不要追求100%的成功率:对于大规模爬虫来说,95%的成功率已经足够好了。为了最后5%的成功率而降低整体QPS是得不偿失的。

七、监控与持续调优

优化不是一次性的工作,而是一个持续的过程。你需要建立完善的监控系统,实时了解爬虫的运行状态。

我监控的关键指标:

  • QPS:每秒请求数
  • 成功率:成功请求数/总请求数
  • 平均响应时间
  • 错误率:按错误类型分类统计
  • 内存和CPU使用率
  • 任务队列长度

我用Prometheus+Grafana做监控,设置了各种告警规则。一旦某个指标异常,我会立刻收到通知。

八、总结

爬虫性能优化是一个系统性的工程,没有银弹。你需要从网络、内存、CPU、架构等多个层面进行全链路优化。

回顾整个优化过程:

  1. 同步转异步:100 -> 800 QPS,提升8倍
  2. 网络层深度优化:800 -> 2500 QPS,提升3倍
  3. 内存与CPU优化:2500 -> 5000 QPS,提升2倍
  4. 分布式架构:5000 -> 10000+ QPS,理论上无限扩展

最后提醒大家:技术是一把双刃剑。在追求性能的同时,一定要遵守法律法规,尊重网站的robots.txt协议,不要爬取敏感数据,不要给网站服务器造成过大的压力。

http://www.jsqmd.com/news/899915/

相关文章:

  • 2026年 宝钢冷轧双相钢推荐榜:HC600/980QP-EL高强钢,汽车轻量化与冲压性能深度解析 - 品牌企业推荐师(官方)
  • 045、PCB丝印与装配图输出
  • AI原生游戏开发实战:零代码构建塔防游戏的全流程解析
  • 麒麟OS+海光CPU编译部署实战
  • P16283 [蓝桥杯 2026 省 Python A 组] 平面选点 题解
  • 基于YOLOv8与边缘计算的智能交通信号自适应控制系统实践
  • ThinkPHP 3.2.3 反序列化漏洞实战:从SQL注入到RCE的三种攻击路径剖析
  • 2026现阶段,寻找全国信誉与实力兼备的定制家居代运营直销公司,答案就在这里 - 2026年企业资讯
  • 创业团队如何利用Taotoken快速原型验证并兼顾成本与扩展性
  • STM32与W5500的嵌入式物联网网关实战
  • 如何高效使用B站视频下载神器:BiliDownloader完整专业指南
  • VMware vSphere 7.0 核心组件许可密钥全解析与实战部署指引
  • 体验旗舰模型Qwen三点七通过聚合平台首发更新的便捷性
  • 如何高效使用Bilibili视频下载器:突破大会员限制的完整实战指南
  • TVA如何准确高效处理各种复杂应用场景?
  • Android 12 窗口调试革命:WinScope 可视化追踪实战
  • 面向MIMO基带干扰消除的高灵活性异构多核体系结构设计开发【附程序】
  • 比 Playwright 快 774 倍!这个 AI 爬虫直接干翻 Cloudflare 企业版
  • AI工具如何重塑开发者工作流:从Gemini到NotebookLM的实践指南
  • 2026论文降AIGC网站:11款工具实测谁敢称“靠谱之王”?
  • 随机过程(1.3)—— 特征函数:从傅里叶变换到概率分布的桥梁
  • AI大模型集体沦陷?Unicode隐形注入攻击揭秘:深度学习技术溯源与LLM防御策略
  • 基于GD32F4与涂鸦MCU-SDK的智能照明系统快速开发实战
  • 哪家发动机缸盖工厂专业?2026年5月推荐TOP5对比铸造工艺案例与价格 - 品牌推荐
  • 别再手动拖滑块了!用SkinnedMeshRenderer代码精准控制Unity角色表情(附完整C#脚本)
  • 从电磁仿真到电路板:HFSS射频器件导入Altium Designer全流程解析
  • GPLT字符重排:从算法竞赛题到字符串处理的通用模式
  • 【Claude Code】会话/周/Opus 使用额度耗尽报错与解决方案
  • Claude API成本优化实战:五大策略削减95%账单
  • 避坑指南:银河麒麟V10手动添加Ubuntu源并安装Wine的完整流程(附依赖冲突解决方案)