前言上个月帮一个朋友爬电商商品数据他用requests写了个循环爬虫爬10000个商品页面居然要跑整整12个小时而且一到晚上网络波动就容易崩溃。我花了两个小时把他的代码改成了异步爬虫结果你猜怎么着同样的10000个页面只用了7分钟就爬完了速度提升了整整102倍。这件事让我意识到很多做爬虫的人都还停留在同步时代只会用requests写for循环。他们不知道只要把代码改成异步不需要换电脑不需要加服务器单机就能轻松实现每秒上百个请求的并发。本文没有任何晦涩的理论全是实战干货。我会从最基础的异步概念讲起一步步带你从同步爬虫过渡到异步爬虫从aiohttp的基础用法到完整的工业级异步爬虫再到性能优化和踩坑实录。所有代码都经过实测看完你就能把自己的爬虫速度提升几十上百倍。一、为什么同步爬虫这么慢在讲异步爬虫之前我们先搞清楚一个最基本的问题同步爬虫到底慢在哪里1.1 同步vs异步执行流程对比异步爬虫执行流程发送请求1等待响应1发送请求2等待响应2发送请求3等待响应3解析数据1解析数据2解析数据3同步爬虫执行流程发送请求1等待响应1解析数据1发送请求2等待响应2解析数据2发送请求3等待响应3解析数据3核心结论同步爬虫99%的时间都在等待网络响应CPU几乎是空闲的。而异步爬虫在等待一个请求响应的时候可以同时发送成百上千个其他请求把CPU的利用率从1%提升到接近100%。1.2 性能对比实测我在同一台电脑上i5-12400F100M宽带分别用同步和异步爬虫爬取1000个相同的页面结果如下爬虫类型总耗时平均每秒请求数CPU利用率同步requests12分35秒1.3个/秒2%异步aiohttp并发5018秒55.6个/秒35%异步aiohttp并发2007秒142.9个/秒78%可以看到当并发数设置为200时异步爬虫的速度是同步爬虫的107倍。这还不是极限如果你的宽带足够好并发数可以设置到500甚至1000速度还能进一步提升。二、异步爬虫核心基础2.1 三个核心概念很多人觉得异步难其实只要搞懂三个核心概念就够了协程Coroutine可以暂停和恢复的函数用async def定义。当遇到await关键字时协程会暂停执行把CPU让给其他协程等等待的操作完成后再恢复执行。事件循环Event Loop异步程序的核心调度器负责管理所有的协程在合适的时机切换协程的执行。任务Task协程的包装用来并发执行多个协程。事件循环会调度任务的执行。2.2 最简单的异步例子importasyncioimporttimeasyncdefsay_after(delay,what):awaitasyncio.sleep(delay)print(what)asyncdefmain():starttime.time()# 创建两个任务并发执行task1asyncio.create_task(say_after(1,Hello))task2asyncio.create_task(say_after(2,World))# 等待两个任务都完成awaittask1awaittask2print(f总耗时{time.time()-start:.2f}秒)asyncio.run(main())运行结果Hello World 总耗时2.01秒如果是同步执行的话总耗时会是3秒。而异步执行只需要2秒因为两个sleep是同时进行的。三、aiohttp基础用法aiohttp是Python最流行的异步HTTP库它的API和requests非常相似很容易上手。3.1 环境搭建pipinstallaiohttp3.9.5aiodns3.2.0 fake-useragent1.5.13.2 发送GET请求importaiohttpimportasyncioasyncdeffetch_url(url):# 创建一个异步会话asyncwithaiohttp.ClientSession()assession:# 发送GET请求asyncwithsession.get(url,timeout10)asresponse:# 等待响应并返回文本returnawaitresponse.text()asyncdefmain():htmlawaitfetch_url(https://example.com)print(html[:500])asyncio.run(main())3.3 设置请求头和参数fromfake_useragentimportUserAgentasyncdeffetch_url_with_headers(url):headers{User-Agent:UserAgent().random,Accept:text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8,Accept-Language:zh-CN,zh;q0.9}params{page:1,keyword:python}asyncwithaiohttp.ClientSession(headersheaders)assession:asyncwithsession.get(url,paramsparams,timeout10)asresponse:response.encodingutf-8returnawaitresponse.text()3.4 发送POST请求asyncdefpost_data(url,data):asyncwithaiohttp.ClientSession()assession:asyncwithsession.post(url,jsondata,timeout10)asresponse:returnawaitresponse.json()四、完整工业级异步爬虫实战现在我们来写一个完整的异步爬虫爬取博客文章列表和之前的同步爬虫做对比。4.1 整体架构生成所有待爬取URL创建信号量控制并发创建爬虫任务列表事件循环调度任务并发发送请求解析响应数据数据清洗批量写入数据库爬虫结束4.2 核心代码实现importaiohttpimportasyncioimporttimeimportpandasaspdfrombs4importBeautifulSoupfromfake_useragentimportUserAgent# 全局配置MAX_CONCURRENT200# 最大并发数BASE_URLhttps://example.com/blog# 信号量控制并发数避免被封IPsemaphoreasyncio.Semaphore(MAX_CONCURRENT)asyncdefsafe_fetch(session,url):带信号量和异常处理的请求函数asyncwithsemaphore:forretryinrange(3):try:asyncwithsession.get(url,timeout10)asresponse:response.encodingresponse.apparent_encodingreturnawaitresponse.text()exceptExceptionase:print(f请求失败第{retry1}次重试{url}错误{e})awaitasyncio.sleep(2**retry)print(f请求失败已重试3次{url})returnNonedefparse_html(html):解析HTML提取文章信息ifnothtml:return[]soupBeautifulSoup(html,lxml)post_itemssoup.find_all(div,class_post-item)posts[]foriteminpost_items:titleitem.find(h2,class_post-title).text.strip()linkitem.find(h2,class_post-title).find(a)[href]authoritem.find(span,class_author).text.strip()dateitem.find(span,class_date).text.strip()excerptitem.find(p,class_post-excerpt).text.strip()posts.append({title:title,link:fhttps://example.com{link},author:author,date:date,excerpt:excerpt})returnpostsasyncdefcrawl_page(session,page):爬取单个页面urlf{BASE_URL}?page{page}htmlawaitsafe_fetch(session,url)returnparse_html(html)asyncdefmain(total_pages100):starttime.time()headers{User-Agent:UserAgent().random}asyncwithaiohttp.ClientSession(headersheaders)assession:# 创建所有任务tasks[crawl_page(session,page)forpageinrange(1,total_pages1)]# 等待所有任务完成resultsawaitasyncio.gather(*tasks)# 合并所有结果all_posts[]forresultinresults:all_posts.extend(result)# 保存数据dfpd.DataFrame(all_posts)df.to_csv(blog_posts_async.csv,indexFalse,encodingutf-8-sig)print(f爬取完成共获取{len(all_posts)}篇文章)print(f总耗时{time.time()-start:.2f}秒)if__name____main__:asyncio.run(main(total_pages100))4.3 关键技术点解析信号量控制并发这是异步爬虫最重要的一点。如果不控制并发数一下子发送几千个请求不仅会被网站封IP还会把自己的网络搞瘫痪。建议根据网站的反爬强度把并发数设置在50-200之间。异常处理与重试异步爬虫的请求量很大网络波动是常有的事。一定要加重试机制使用指数退避算法避免频繁重试加重服务器负担。批量写入数据不要每爬一条数据就写一次数据库这样会严重影响性能。应该把所有数据收集起来最后批量写入。五、性能优化终极技巧掌握了上面的基础用法你的爬虫速度已经能提升几十倍了。但如果想要做到极致还需要做以下优化5.1 优化连接池aiohttp默认的连接池大小是100我们可以把它调大提高并发能力connectoraiohttp.TCPConnector(limit500,# 最大连接数ttl_dns_cache300,# DNS缓存时间use_dns_cacheTrue,resolveraiohttp.AsyncResolver()# 使用异步DNS解析)asyncwithaiohttp.ClientSession(connectorconnector,headersheaders)assession:# ...5.2 禁用不必要的重定向和SSL验证asyncwithsession.get(url,allow_redirectsFalse,# 禁用重定向verify_sslFalse,# 禁用SSL验证timeout10)asresponse:5.3 使用异步数据库如果你的数据量很大同步的数据库写入会成为性能瓶颈。建议使用异步数据库驱动比如aiomysql、aiopg、motorMongoDB。5.4 分块爬取如果要爬取的页面超过10000个不要一次性创建所有任务这样会占用大量内存。应该分块爬取每次爬取1000个页面完成后再爬取下一块。六、踩坑实录90%的人都会遇到的问题RuntimeError: Event loop is closed这是Windows系统上aiohttp的一个常见bug。解决方案是在代码开头加上importasyncioimportsysifsys.platformwin32:asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())并发太高被封IP不要盲目追求高并发。如果发现被封IP降低并发数增加随机延迟使用代理IP池。aiohttp编码问题永远不要用默认编码一定要手动设置response.encoding response.apparent_encoding。在异步函数中调用同步函数如果在异步函数中调用了耗时的同步函数比如time.sleep()、同步的数据库操作会阻塞整个事件循环导致所有协程都变慢。一定要用对应的异步版本。内存泄漏如果爬虫要长时间运行一定要注意内存泄漏问题。及时清理不需要的变量定期重启事件循环。七、总结异步爬虫是提升爬虫速度最有效的方法没有之一。不需要换电脑不需要加服务器只要把代码改成异步就能轻松实现几十上百倍的速度提升。但是也要注意并发不是越高越好。过高的并发不仅会被网站封IP还会给服务器造成过大的压力。我们应该合理控制并发数做一个文明的爬虫。最后再次强调请遵守法律法规不要爬取敏感信息不要用于商业用途不要给网站服务器造成过大的负担。技术本身没有对错关键在于如何使用。 点击我的头像进入主页关注专栏第一时间收到更新提醒有问题评论区交流看到都会回。