免费python编程教程:https://pan.quark.cn/s/2c17aed36b72
凌晨两点,技术群里弹出条消息:“求助,我这爬虫跑了一晚上,才抓了八千条数据,离十万的目标差远了,明天交不了差咋整?”发消息的是个做运营的朋友,临时接了个竞品数据分析的急活,想着用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%。
这几个技巧组合起来,效果相当可观。同样是抓十万条数据,原来跑一晚上都完不成,优化后可能两个小时搞定。区别不在于加班熬夜,而在于用没用对方法。