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

Python异步爬虫实战:aiohttp并发采集与验证码异步处理完整教程

前言

爬虫效率是每个数据工程师都关心的问题。当你需要采集上万个页面时,同步请求一个一个排队等待的方式实在太慢了。

Python的asyncio + aiohttp组合可以让你的爬虫速度提升10-50倍,而且代码改动并不大。

本文将从零开始讲解异步爬虫的原理和实战,包括并发控制、错误处理、以及如何在异步流程中处理验证码。

同步 vs 异步:为什么差这么多

同步爬虫的瓶颈

importrequestsimporttime urls=[f'https://httpbin.org/delay/1'for_inrange(10)]start=time.time()forurlinurls:resp=requests.get(url)print(f'{resp.status_code}',end=' ')print(f'\n同步耗时:{time.time()-start:.1f}秒')# 输出: 同步耗时: 10.3秒 (每个请求等1秒,串行排队)

问题很明显:每个请求都在等网络IO返回,CPU其实在空闲。

异步爬虫的优势

importaiohttpimportasyncioimporttimeasyncdeffetch(session,url):asyncwithsession.get(url)asresp:returnresp.statusasyncdefmain():urls=[f'https://httpbin.org/delay/1'for_inrange(10)]asyncwithaiohttp.ClientSession()assession:tasks=[fetch(session,url)forurlinurls]results=awaitasyncio.gather(*tasks)print(f'状态码:{results}')start=time.time()asyncio.run(main())print(f'异步耗时:{time.time()-start:.1f}秒')# 输出: 异步耗时: 1.2秒 (10个请求并发,几乎同时完成)

10个请求从10秒变成1秒,这就是异步的威力。

aiohttp基础

安装

pipinstallaiohttp

Session管理

importaiohttpimportasyncioasyncdefmain():# 创建session(复用TCP连接,性能更好)asyncwithaiohttp.ClientSession()assession:# GET请求asyncwithsession.get('https://httpbin.org/get')asresp:data=awaitresp.json()print(data)# POST请求asyncwithsession.post('https://httpbin.org/post',json={'key':'value'})asresp:data=awaitresp.json()print(data)# 自定义请求头headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0','Accept-Language':'zh-CN,zh;q=0.9,en;q=0.8',}asyncwithsession.get('https://httpbin.org/headers',headers=headers)asresp:print(awaitresp.json())asyncio.run(main())

超时和代理

importaiohttpimportasyncioasyncdefmain():# 设置超时timeout=aiohttp.ClientTimeout(total=30,connect=10)# 设置代理proxy='http://user:pass@proxy:8080'asyncwithaiohttp.ClientSession(timeout=timeout)assession:asyncwithsession.get('https://httpbin.org/ip',proxy=proxy)asresp:print(awaitresp.json())asyncio.run(main())

并发控制:Semaphore

不加限制地并发会导致目标服务器拒绝连接,甚至把自己的IP封了。用Semaphore控制并发数:

importaiohttpimportasyncioasyncdeffetch_with_limit(sem,session,url):asyncwithsem:# 信号量控制并发try:asyncwithsession.get(url)asresp:text=awaitresp.text()return{'url':url,'status':resp.status,'length':len(text)}exceptExceptionase:return{'url':url,'error':str(e)}asyncdefmain():urls=[f'https://httpbin.org/get?page={i}'foriinrange(100)]sem=asyncio.Semaphore(10)# 最多同时10个请求connector=aiohttp.TCPConnector(limit=20)# TCP连接池上限asyncwithaiohttp.ClientSession(connector=connector)assession:tasks=[fetch_with_limit(sem,session,url)forurlinurls]results=awaitasyncio.gather(*tasks)success=[rforrinresultsif'error'notinr]failed=[rforrinresultsif'error'inr]print(f'成功:{len(success)}, 失败:{len(failed)}')asyncio.run(main())

实战:异步爬虫完整模板

importaiohttpimportasynciofromdataclassesimportdataclassfromtypingimportOptionalimportloggingimportrandom logging.basicConfig(level=logging.INFO)logger=logging.getLogger(__name__)@dataclassclassScrapedItem:url:strtitle:strcontent:strstatus:intclassAsyncScraper:def__init__(self,concurrency=10,delay_range=(0.5,2.0),proxy=None):self.concurrency=concurrency self.delay_range=delay_range self.proxy=proxy self.sem=asyncio.Semaphore(concurrency)self.results=[]self.errors=[]asyncdeffetch_page(self,session,url):asyncwithself.sem:# 随机延迟,避免请求过于规律awaitasyncio.sleep(random.uniform(*self.delay_range))try:asyncwithsession.get(url,proxy=self.proxy)asresp:ifresp.status==200:html=awaitresp.text()item=self.parse(url,html,resp.status)self.results.append(item)logger.info(f'[OK]{url}')returnitemelifresp.status==403:logger.warning(f'[403]{url}- 可能需要处理验证码')self.errors.append({'url':url,'status':403})else:logger.warning(f'[{resp.status}]{url}')self.errors.append({'url':url,'status':resp.status})exceptasyncio.TimeoutError:logger.error(f'[TIMEOUT]{url}')self.errors.append({'url':url,'error':'timeout'})exceptExceptionase:logger.error(f'[ERROR]{url}:{e}')self.errors.append({'url':url,'error':str(e)})defparse(self,url,html,status):# 替换为你的解析逻辑fromhtml.parserimportHTMLParser title=''classTitleParser(HTMLParser):defhandle_starttag(self,tag,attrs):nonlocaltitle self._in_title=tag=='title'defhandle_data(self,data):nonlocaltitleifgetattr(self,'_in_title',False):title=data self._in_title=Falseparser=TitleParser()parser.feed(html[:5000])returnScrapedItem(url=url,title=title,content=html[:500],status=status)asyncdefrun(self,urls):timeout=aiohttp.ClientTimeout(total=30)connector=aiohttp.TCPConnector(limit=self.concurrency*2)headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/124.0.0.0','Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8','Accept-Language':'zh-CN,zh;q=0.9,en;q=0.8',}asyncwithaiohttp.ClientSession(timeout=timeout,connector=connector,headers=headers)assession:tasks=[self.fetch_page(session,url)forurlinurls]awaitasyncio.gather(*tasks,return_exceptions=True)logger.info(f'完成:{len(self.results)}成功,{len(self.errors)}失败')returnself.results# 使用asyncdefmain():scraper=AsyncScraper(concurrency=5,delay_range=(1.0,3.0))urls=[f'https://httpbin.org/get?id={i}'foriinrange(50)]results=awaitscraper.run(urls)foriteminresults[:3]:print(f'{item.title}-{item.url}')asyncio.run(main())

异步流程中处理验证码

异步爬虫遇到验证码时,不能阻塞整个事件循环。正确做法是将验证码解决也异步化:

importaiohttpimportasynciofrompassxapiimportAsyncPassXAPIclassAsyncScraperWithCaptcha:def__init__(self,captcha_api_key,concurrency=10):self.sem=asyncio.Semaphore(concurrency)self.solver=AsyncPassXAPI(api_key=captcha_api_key)asyncdeffetch_with_captcha(self,session,url):asyncwithself.sem:asyncwithsession.get(url)asresp:html=awaitresp.text()# 检测验证码if'data-sitekey'inhtml:token=awaitself._solve_captcha(html,url)iftoken:asyncwithsession.post(url,data={'cf-turnstile-response':token,'g-recaptcha-response':token,})asretry_resp:returnawaitretry_resp.text()returnhtmlasyncdef_solve_captcha(self,html,url):importrematch=re.search(r'data-sitekey="([^"]+)"',html)ifnotmatch:returnNonesitekey=match.group(1)if'cf-turnstile'inhtml:result=awaitself.solver.solve_turnstile(sitekey=sitekey,url=url)elif'h-captcha'inhtml:result=awaitself.solver.solve_hcaptcha(sitekey=sitekey,url=url)else:result=awaitself.solver.solve_recaptcha(sitekey=sitekey,url=url)returnresult.get('token')asyncdefrun(self,urls):asyncwithaiohttp.ClientSession()assession:tasks=[self.fetch_with_captcha(session,url)forurlinurls]returnawaitasyncio.gather(*tasks,return_exceptions=True)# 使用asyncdefmain():scraper=AsyncScraperWithCaptcha(captcha_api_key='your_passxapi_key',concurrency=10)urls=['https://protected-site.com/page/1','https://protected-site.com/page/2']results=awaitscraper.run(urls)asyncio.run(main())

生产者-消费者模式

对于大规模爬取,推荐使用asyncio.Queue实现生产者-消费者模式:

importasyncioimportaiohttpasyncdefproducer(queue,urls):forurlinurls:awaitqueue.put(url)# 发送结束信号for_inrange(5):# worker数量awaitqueue.put(None)asyncdefconsumer(queue,session,results,worker_id):whileTrue:url=awaitqueue.get()ifurlisNone:breaktry:asyncwithsession.get(url)asresp:data=awaitresp.text()results.append({'url':url,'length':len(data)})print(f'[Worker-{worker_id}]{url}->{len(data)}bytes')exceptExceptionase:print(f'[Worker-{worker_id}] Error:{url}->{e}')queue.task_done()asyncdefmain():urls=[f'https://httpbin.org/get?id={i}'foriinrange(30)]queue=asyncio.Queue(maxsize=20)results=[]asyncwithaiohttp.ClientSession()assession:# 启动1个生产者 + 5个消费者producer_task=asyncio.create_task(producer(queue,urls))workers=[asyncio.create_task(consumer(queue,session,results,i))foriinrange(5)]awaitproducer_taskawaitasyncio.gather(*workers)print(f'总计采集:{len(results)}个页面')asyncio.run(main())

性能优化技巧

1. 连接池复用

# 配置TCP连接池connector=aiohttp.TCPConnector(limit=100,# 总连接数上限limit_per_host=10,# 单个域名连接数ttl_dns_cache=300,# DNS缓存5分钟enable_cleanup_closed=True,)```### 2. 流式读取大文件```pythonasyncdefdownload_file(session,url,filepath):asyncwithsession.get(url)asresp:withopen(filepath,'wb')asf:asyncforchunkinresp.content.iter_chunked(8192):f.write(chunk)```### 3. 优雅关闭```pythonimportsignalasyncdefgraceful_shutdown(scraper):print('收到退出信号,正在优雅关闭...')scraper.running=False# 等待当前任务完成awaitasyncio.sleep(2)loop=asyncio.get_event_loop()loop.add_signal_handler(signal.SIGINT,lambda:asyncio.create_task(graceful_shutdown(scraper)))

常见坑

  1. 不要在async函数中使用time.sleep:会阻塞整个事件循环,用await asyncio.sleep()
    1. Session要复用:每个请求创建新Session浪费TCP连接
    1. 并发不是越多越好:过高并发会触发反爬,建议5-20
    1. 异常要捕获:一个任务的异常不应该影响其他任务

总结

异步爬虫是提升采集效率的最有效手段:

  1. aiohttp + asyncio可以轻松实现10倍以上的速度提升
    1. 用Semaphore控制并发数,避免被封IP
    1. 验证码解决也要异步化,不能阻塞事件循环
    1. 生产者-消费者模式适合大规模采集场景
      验证码异步解决方案可以参考:passxapi-python,提供AsyncPassXAPI异步客户端,完美融入asyncio工作流。

觉得有帮助请点赞收藏,有问题欢迎评论区讨论。

http://www.cnnetsun.cn/news/1543039.html

相关文章:

  • 深耕.NET开发三载,我靠技术实力买下人生第一套房
  • 如何高效使用网页时光回溯器:永久保存与恢复消失的网络内容
  • Agentic AI时代:新型安全框架为智能体套上硬核枷锁
  • HUNYUAN-MT 7B翻译终端微信小程序集成案例:实现实时语音对话翻译
  • OpenClaw技能扩展:为nanobot镜像添加自定义自动化模块
  • 揭秘某黄鱼x-sign算法:从Native层Hook到Unidbg全链路解析
  • OBS多平台直播插件obs-multi-rtmp:一次编码,全网覆盖的终极解决方案
  • Linux系统编程(十一)--- 动态库、静态库
  • 终极指南:10分钟语音数据打造专业级AI变声模型
  • 终极指南:Windows三指拖拽功能的完整解决方案
  • 面试硬核双杀!合并 K 个升序链表 + LRU 缓存|力扣高频手撕原题全解
  • 终极解决方案:如何用G-Helper一键恢复ROG游戏本色彩配置文件
  • 为什么92%的金融级Python项目已在Q1完成AOT安全迁移,而你还在用CPython解释器?
  • 【Python原生AOT编译2026终极指南】:6大高频报错根源定位+3步热修复方案(PyO3/CPython 3.14+实测有效)
  • 为什么你的Python网关在EMC测试中随机重启?深度拆解CPython嵌入式移植的6大实时性盲区(附FreeRTOS+Python3.11混合调度方案)
  • 别再只盯着像素了!从镜头到屏幕:一次搞懂影响你手机成片效果的完整链路(附避坑指南)
  • volatile vs synchronized:Java 并发两大护法
  • Transformer回顾与BERT模型学习:小白程序员必备收藏指南
  • 思源宋体终极编译指南:从源码到可部署字体的完整流程
  • Qwen-Image-Lightning参数详解:10个关键设置提升生成质量
  • Docker Desktop+WSL2自定义安装路径实战指南
  • 键盘优化:机械键盘连击修复与输入稳定解决方案实战指南
  • 用Python+海康工业相机(MV-CH120-60UM)搭建一个简易的条形码扫描器(附完整代码)
  • Java毕业设计基于springboot+vue的武汉周边农家乐信息管理系统
  • OpCore-Simplify:2024年最完整的黑苹果自动化EFI构建终极指南
  • KITTI数据集实战指南:从下载到3D物体检测的完整流程(附避坑技巧)
  • open_clip:多模态模型工业化落地全方案
  • 【uniapp实战】相册图片二维码识别:从压缩优化到原生API调用的完整指南
  • OpenClaw调试技巧:GLM-4.7-Flash复杂任务拆解的5个可视化工具
  • DanKoe 视频笔记:说服性沟通:21 世纪的核心技能 [特殊字符]