Python写爬虫太慢?这5个技巧让你的效率提升300%!

13 阅读5分钟

​免费python编程教程:pan.quark.cn/s/2c17aed36…

凌晨两点,技术群里弹出条消息:“求助,我这爬虫跑了一晚上,才抓了八千条数据,离十万的目标差远了,明天交不了差咋整?”发消息的是个做运营的朋友,临时接了个竞品数据分析的急活,想着用Python写个简单脚本应付一下。结果脚本跑了一整夜,进度条刚爬过十分之一。

这场景干爬虫的都熟。辛辛苦苦写了半天代码,一运行发现速度慢得让人怀疑人生。看着控制台里一行行缓缓跳出的日志,恨不得手动帮它点鼠标。很多人第一反应是“我代码写错了”,翻来覆去改了半天,速度还是那样。

其实Python爬虫慢,通常不是代码写错了,而是没用好这几招。实测能让效率翻几倍,甚至十几倍。

先说协程。很多人的爬虫还在用requests.get()一个个请求,发一个请求等半天响应,响应回来了再发下一个。这就像去超市买东西,排队结账时发现前面的人没带够钱,你站在原地等他回家拿钱回来再继续。这不叫写代码,这叫熬时间。

协程的逻辑很简单——不等。遇到网络IO这种耗时间的操作,主动告诉系统“你先干别的,数据回来了叫我”。单线程里可以同时发起几十上百个请求,谁先返回就处理谁。

实测数据很直观。爬取1000个商品页,用10个线程跑耗时42秒,用单线程加100个协程跑只要12.8秒,速度快了3倍多。CPU占用率反而从85%降到了30%,内存占用不到多线程的四成。

代码改起来也不复杂。把requests换成aiohttp,在请求函数前面加个async,调用的时候用await等着,最后用asyncio.run()启动事件循环。

import aiohttp
import asyncio

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = ["https://example.com"] * 100
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

asyncio.run(main())

转存失败,建议直接上传图片文件

这段代码看着简单,底层逻辑却值钱——协程的高并发能力需要配合连接池才能真正发挥出来。

再说连接池。很多人不知道,每次用requests.get()都相当于重新谈一次恋爱——建立TCP连接、三次握手、可能还要SSL握手,谈完了请求数据,拿到响应就分手,下次再从头开始。

这有多浪费?10个请求就要10次TCP握手,10个socket来回折腾。而用连接池的话,第一次请求建立连接后,后续请求复用同一条连接,10个请求只需要1次握手。

实测对比很刺激。同样10个请求,不用连接池耗时1.85秒,用连接池只要0.45秒,快了4倍。代码区别就是有没有共享同一个ClientSession实例。

# 错误示范:每次新建session
async with httpx.AsyncClient() as client:
    await client.get(url)

# 正确示范:复用同一个session
async with httpx.AsyncClient() as client:
    tasks = [fetch(client, url) for url in urls]

转存失败,建议直接上传图片文件

第三个技巧是关于缓存的。很多爬虫一遍遍抓同样的页面,今天抓一次,明天抓一次,甚至同一轮任务里重复抓同一个URL。这不叫采集,这叫给服务器做压力测试。

缓存策略能省掉大量重复请求。设置一个5分钟的缓存,5分钟内再次请求同一个URL,直接从本地拿数据,不再访问目标服务器。

用requests_cache库几行代码就搞定:

import requests
import requests_cache

requests_cache.install_cache('demo_cache', expire_after=300)

# 第一次请求,真的去抓
response1 = requests.get('https://example.com')

# 第二次请求,直接读缓存
response2 = requests.get('https://example.com')

转存失败,建议直接上传图片文件

电商价格监控这种场景特别适合。商品页5分钟更新一次价格就够了,用缓存把请求间隔控制好,既省自己带宽,也不招网站烦。

第四个技巧是关于去重的。高并发爬虫最怕的就是重复抓取。几千个请求并发出去,URL重叠是常态。用Python的set()存几百万个URL,内存分分钟上GB。

Bloom Filter是解决这个问题的利器。它用多个哈希函数把URL映射到一个位数组里,占内存极小,速度极快。单机百万URL查重耗时只要2到3毫秒,内存占用15MB左右。

配合Redis的HyperLogLog做全局统计,再加上每日持久化备份,既能高速查重,又能防止重启丢失数据。

from bitarray import bitarray
import mmh3

class BloomFilter:
    def __init__(self, size, hash_count):
        self.size = size
        self.hash_count = hash_count
        self.bit_array = bitarray(size)
        self.bit_array.setall(0)
    
    def add(self, item):
        for i in range(self.hash_count):
            digest = mmh3.hash(item, i) % self.size
            self.bit_array[digest] = 1
    
    def check(self, item):
        for i in range(self.hash_count):
            digest = mmh3.hash(item, i) % self.size
            if self.bit_array[digest] == 0:
                return False
        return True

转存失败,建议直接上传图片文件

第五个技巧是库的选型。不同场景用不同的库,硬用requests抓动态网页,等于是拿勺子喝汤——能喝,但费劲。

静态页面用Requests+BeautifulSoup,上手快,代码简洁,适合中小规模项目。动态内容用Selenium或Playwright,模拟真实浏览器操作,绕过90%的行为检测。大规模采集用Scrapy,内置异步框架、自动限速、分布式支持,日均处理2000万条数据不在话下。实时数据用aiohttp做异步流处理,单线程处理5000+并发连接。

选对了库,效率翻倍。爬知乎用户信息时,用requests.Session让HTTP连接复用率从12%升到89%,响应时间缩短40%。

这几个技巧组合起来,效果相当可观。同样是抓十万条数据,原来跑一晚上都完不成,优化后可能两个小时搞定。区别不在于加班熬夜,而在于用没用对方法。