Python异步模块asyncio/aiohttp(链家爬虫实例)内附教程分享
一、写在开头
虽然用scrapy框架来爬信息已经够快了,再用aiohttp来爬链家有点重复造轮子的嫌疑,但还是有助于我对异步编程的理解。以下内容都是出于自己对于异步的理解写出来的,毕竟不是计算机专业,没法用专业的语言来表述,用的都是通俗口语化的文字,其中肯定有些地方也写的并不对,但目前只能这样了,待以后有更深入理解之后再来完善吧。
这是最终的效果(代码放在最后):
同步方式
异步方式
二、几个概念
为了尽可能的发挥出cpu的性能,使程序运行的更有效率,软件跑的更快,可以通过多种方法来实现,比如多进程、多线程或者异步等等。
先解释一下这几个概念:进程,线程、协程。专业的可以参照《进程、线程与协程的比较》一文。
举个例子来粗浅的理解这几个概念,假设在有一间屋子需要打扫卫生,打扫卫生包含了擦桌子、扫地、拖地、擦窗户。
- 如果是按顺序一件一件的完成这些事,那么这就是同步编程,打扫卫生的人就是进程;
- 如果有2个人来一起来打扫卫生,那么就有了2个进程,打扫的速度自然就快了一倍;
- 如果每个人的左手和右手也可以分别擦桌子和擦窗户,那么每个进程就有了2个线程,打扫速度又快了很多;
- 但上面都是按顺序来打扫卫生的(同步编程),先擦桌子,再扫地,再拖地,最后擦窗户。假设完成每件事情需要5分钟,其中拖地要拖5分钟然后等地面干燥10分钟,那么1个人完成所有事情的时间就是5+5+15+5=30分钟,2个进程则需要15分钟。
- 如果需要更快呢?
- 那就可以在等地面干燥时,先去擦窗户,擦完窗户之后再看看地面有没有干,如果干了,则事情就完成了。如果是1个人打扫房间,整个时间就直接缩短为擦桌子5分钟,扫地5分钟,5分钟拖地,5分钟擦窗户,再等待5分钟,共计25分钟。
- 这就是异步编程。
但对于程序来说,cpu的计算速度实在是太快了,速度只能用快的飞起来形容,大部分任务中耗时的是磁盘读取和写入、等待服务器响应等等,所以cpu通常都是1秒就做完了自己的事情(其实远没有1秒,可能只要毫秒或者纳秒),而要傻傻的等磁盘或者网络加载到天荒地老。
此时在编程中就需要采取异步的思想,cpu做完自己的事情后,可以先去干别的事情,让磁盘或者网络慢慢的去写入和加载,cpu只需要偶尔过来瞄一眼看看磁盘有没有做完,如果磁盘做完了事情,那么cpu就继续往下做事,如果磁盘还没做完,cpu就继续做别的事情,直到磁盘做完。
三、Python中实现异步
Python中关于异步的一个模块是asyncio,是英文asynchronous异步的,input输入和output输出三个单词的缩写。
Python3.6以前,async def只能用装饰器的方式来实现,await要通过yield from来实现。
import asyncio @asyncio.coroutine def to_do_something(): print('do something') yield from asyncio.sleep(2) print('something done!')
在Python3.6后,可以通过关键词async def来定义一个coroutine协程,协程就相当于未来需要完成的任务,多个协程就是多个需要完成的任务,多个协程可以进一步封装到一个task对象中,task就是一个储存任务的盒子。此时,装在盒子里的任务并没有真正的运行,需要把它接入到一个监视器中使它运行,同时监视器还要持续不断的盯着盒子里的任务运行到了哪一步,这个持续不断的监视器就用一个循环对象loop来实现。
那么,整个过程应该就是通过下面这种方式来实现:
import asyncio import time #定义第1个协程,协程就是将要具体完成的任务,该任务耗时3秒,完成后显示任务完成 async def to_do_something(i): print('第{}个任务:任务启动...'.format(i)) #遇到耗时的操作,await就会使任务挂起,继续去完成下一个任务 await asyncio.sleep(i) print('第{}个任务:任务完成!'.format(i)) #定义第2个协程,用于通知任务进行状态 async def mission_running(): print('任务正在执行...') start = time.time() #创建一个循环 loop = asyncio.get_event_loop() #创建一个任务盒子tasks,包含了3个需要完成的任务 tasks = [asyncio.ensure_future(to_do_something(1)), asyncio.ensure_future(to_do_something(2)), asyncio.ensure_future(mission_running())] #tasks接入loop中开始运行 loop.run_until_complete(asyncio.wait(tasks)) end = time.time() print(end-start)
程序运行的过程看起来就像是这个样子:
紫框--tasks中的第一个任务;
黄框--tasks中的第二个任务;
绿框--tasks中的第三个任务;
红线--程序运行路线
虚线--程序阻塞,出让函数的控制权
遇到需要等待的地方时,当前任务就会挂起,先去做第2个任务,然后继续挂起,再继续做第3个任务,最终完成任务时,总耗时则应该是2秒,而不是1秒+2秒=3秒。
程序实际运行的状态也确实如此,时间是2秒多一点,多出来的时间可能就是cpu本身的运算时间。
需要说明的是,3个任务的运行顺序是按照tasks列表里的顺序进行的,如果把顺序换一下,上面的程序运行图就不一样了。
#下面两个tasks运行的结果是不一样的 tasks = [asyncio.ensure_future(to_do_something(1)), asyncio.ensure_future(to_do_something(2)), asyncio.ensure_future(mission_running())] #################################################### tasks = [asyncio.ensure_future(to_do_something(1)), asyncio.ensure_future(mission_running()), asyncio.ensure_future(to_do_something(2))]
如果想通过异步的方式对网络发起请求,则还需要借助aiohttp模块,这个模块相当于requests模块的异步版本,使用方法极其相似,有小部分差异,只要习惯就好。
import asyncio import aiohttp async def get_info(url): async with aiohttp.ClientSession() as session: async with session.get(url,timeout=5) as resp: if resp.status != 200: url_lst_failed.append(url) else: url_lst_successed.append(url) r = await resp.text()
四、其它一些
为什么要用到循环loop?
程序是按照设定的顺序从头执行到尾,运行的次数也是完全按照设定。当在编写异步程序时,必然其中有部分程序的运行耗时是比较久的,需要先让出当前程序的控制权,让其在背后运行,让另一部分的程序先运行起来。当背后运行的程序完成后,也需要及时通知主程序已经完成任务可以进行下一步操作,但这个过程所需的时间是不确定的,需要主程序不断的监听状态,一旦收到了任务完成的消息,就开始进行下一步。loop就是这个持续不断的监视器。
链家爬虫代码可以看《链家爬虫代码_asyncio》
对比以前的版本,可以手动输入价格区间,采用二分法自动切割价格区间,将各个区间房屋数量缩到3000以内(链家限制了显示条目,最多只有3000)。
另外也还存在一个问题,在对比同步爬取和异步爬取得结果时,发现当url数量比较多时,会有较大一部分的url在采用异步爬取时不会被发起请求,导致异步的结果数量比同步的结果数量少了很多。希望有人能够解答这个问题。
我最后采用了一个笨办法解决了这个问题,将首次运行后未被发起请求的url再放入一个列表中,然后再次循环,直至所有url都被发起请求,最终实现抓取全部信息。
五、参考资料
- 廖雪峰的官方网站--异步IO
- Python 的异步 IO:Asyncio 简介
- python中重要的模块--asyncio
- aiohttp 简易使用教程
- Python中单线程、多线程和多进程的效率对比实验
最后,想学习Python的小伙伴们!
请关注+私信回复:“学习”就可以拿到一份我为大家准备的Python学习资料!
pytyhon学习资料
python学习资料