用Python的异步爬虫一天抓完200万条数据,这听起来像吹牛。我自己试过,确实能做到。关键在协程,不是多线程。多线程有锁,有上下文切换开销,协程把这些都省了。你干过爬虫,肯定遇到过IO等待,等网站响应,等数据库写入,这些时间白白浪费了。协程就是让CPU在这些等待间隙去干别的活。
先说你需要的工具。Python 3.7以上自带asyncio,再加个aiohttp发请求。别用requests,那是同步的,发一个请求等半天。aiohttp是异步的,可以同时发几百个请求。我试过,用10个worker协程,每秒能抓2000条数据。一天算下来,200万条松松的。当然,你电脑网速得够用。
写代码的第一个坑是事件循环。很多人一上来就搞复杂,其实一个loop就够了。核心代码就这些:
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def worker(sem, session, url):
async with sem:
data = await fetch(session, url)
处理数据,存数据库或文件
await save_data(data)
这里用到了信号量sem。你可能会问,一次发太多请求,网站会不会封IP。会。所以你得控制并发量。我一般设50到100个并发。太多反而慢,因为网卡带宽有限。信号量就是限制同时跑的worker数量,一个worker处理完才放新的进来。
协程的要点在await。你写代码的时候,所有耗IO的操作都要加await。比如读文件,写数据库,发请求。不加await,代码不会等,直接往下跑,会出乱子。我见过有人忘了await,结果请求没发出去,程序就结束了。检查一下,你的代码里有没有该await没加的地方。
数据存储是另一个瓶颈。你用普通的方式写数据库,每秒几百条就卡死了。得用批量插入。比如MySQL,攒够100条再执行executemany。MongoDB用insert_many。Redis用管道。我试过用批量写,从每秒300条提升到每秒3000条。差距很大。
还有个坑是异常处理。网站随时会挂,连接会超时,解析会失败。你得加try except。一个worker挂了,不能影响其他worker。我习惯在worker函数里包一层,捕获所有异常,记录日志,然后继续跑。代码大概这样:
async def safe_worker(sem, session, url):
try:
await worker(sem, session, url)
except Exception as e:
log.error(f'worker error: {url} {e}')
实际跑数据的时候,你会遇到动态加载的页面。这种不能用普通请求,得用异步浏览器驱动,比如playwright。它也有异步API,可以和aiohttp混用。一条规则是,能用aiohttp就别用playwright,后者慢很多,因为要渲染整个页面。
协程的调度是自动的。你在代码里用了await,控制权就交给了事件循环。事件循环会调度其他协程。这就像你把一堆任务扔给食堂阿姨,她一次只能打一份饭,但会轮着给每个窗口打。效率看着高,因为没人闲着。
我测试过,用一台4核8G的服务器,跑50个worker,抓取速度稳定在每秒2000条左右。一天按10小时算,就是7200万条。200万是小意思。当然,网站要能抗住,不能太慢。有的网站限制访问频率,你得加延时。可以用asyncio.sleep,在每个请求之间休息0.1秒。
代码跑起来后,看监控很重要。你实时看看CPU占用率。CPU占用不到50%,说明IO还没跑满,可以加worker。CPU跑到80%以上,说明计算密集,得优化解析逻辑。内存也要看,别让数据积压。我建议每抓完1万条数据,就写一次磁盘或数据库,清空内存。
最后说个容易忽略的,DNS解析。默认的DNS解析是阻塞的。你得用异步DNS,比如aiodns。安装后配置一下,事件循环会自动用它。没配的话,你会发现发起请求时卡住,因为解析域名占了几十毫秒,这时间累积起来很可观。
写代码的时候多测试。先抓100条试试,看流程通不通。再抓1000条,看性能稳不稳。没问题了再放200万。别一上来就全量跑,出错了都不知道在哪。我吃过这个亏,跑了半小时发现数据格式不对,全白干。