Python培训
400-111-8989
为提高Python爬虫爬取数据的效率,我们最先想到的就是使用多线程或者多进程。但是从性能和计算机资源利用率上来讲,通过协程实现的异步爬取又似乎更加合适。
既然提到了协程,就不得不说一下进程和线程了。首先我们来简单了解一下python中进程、线程、协程。
01 进程、线程、协程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
如下图,最大的黑框可以看作一个运行着的程序,这个程序中包括了三个同时运行的进程,每个进程都是独立的。在每个进程中又包括了三个线程,从而构成了整个程序。
线程,又称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
协程,又称微线程,纤程。与线程的抢占式调度不同,它是协作式调度。如下图所示,协程最大的优势就是极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
协程的优点是无需线程上下文切换的开销,协程避免了无意义的调度,拥有极高的执行效率。无需原子操作锁定及同步的开销,它是线程安全的。方便切换控制流,简化编程模型。协程的缺点就是无法利用cpu多核。当然简单的解决方法就是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
02 asyncio实现异步操作
网络模型有很多种,为了实现高并发也有很多方案,多线程,多进程。无论多线程和多进程,IO的调度更多取决于系统,而协程的方式,调度来自用户,用户可以在函数中yield一个状态。使用协程可以实现高效的并发任务。能够实现协程的有asyncio,tornado和gevent,下面将介绍asyncio的使用。
从 Python 3.4 开始,Python 中加入了协程的概念,但这个版本的协程还是以生成器对象为基础的,在 Python 3.5 则增加了 async/await,使得协程的实现更加方便。
首先我们需要了解下面几个概念:
event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生的时候,就会调用对应的处理方法。
coroutine:中文翻译叫协程,在 Python 中常指代为协程对象类型,我们可以将协程对象注册到时间循环中,它会被事件循环调用。我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。
task:任务,它是对协程对象的进一步封装,包含了任务的各个状态。
future:代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。
定义一个协程
定义协程有两种方式,一种是使用asyncio.coroutine修饰器,需要使用yield from来挂起阻塞方法的执行,另一种是 Python 3.5 增加的,通过async定义一个协程,await用于挂起阻塞的异步调用接口。
上面两种方法基本上一样。第二种使用方法比较方便,所以推荐使用第二种方法。
通过async关键字来定义一个协程。协程不能直接运行,需要把协程加入到事件循环(loop),由loop在适当的时候调用协程。asyncio.get_event_loop方法可以创建一个事件循环,然后使用run_until_complete将协程注册到事件循环,并启动事件循环。因为本例只有一个协程,于是可以看见如下输出:
创建一个task
协程对象是不能直接运行,在注册事件循环的时候,其实是用run_until_complete方法将协程对象包装成为了一个任务(task)对象。task对象是Future类的子类。保存了协程运行后的状态,用于未来获取协程的结果。
可以看到输出结果为:
创建task后,task在加入事件循环之前是pending状态,因为do_some_work中没有耗时的阻塞操作,task很快就执行完毕了。后面打印的finished状态。
asyncio.ensure_future(coroutine) 和 loop.create_task(coroutine)都可以创建一个task,run_until_complete的参数是一个futrue对象。当传入一个协程,其内部会自动封装成task。
阻塞和await
使用async可以定义协程对象,使用await可以针对耗时的操作进行挂起,就像生成器里的yield一样,函数让出控制权。协程遇到await,事件循环将会挂起该协程,执行别的协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行。
耗时的操作一般是一些IO操作,例如网络请求、文件读取、数据存储等。我们使用asyncio.sleep函数来模拟IO操作。协程的目的也是让这些IO操作异步化。
结果如下:
在 sleep的时候,使用await让出控制权。即当遇到阻塞调用的函数的时候,使用await方法将协程的控制权让出,以便loop调用其他的协程。这个例子就用耗时的阻塞操作了。
并发和并行
并发和并行一直是容易混淆的概念。并发通常指有多个任务需要同时进行,并行则是同一时刻有多个任务执行。
asyncio实现并发,就需要多个协程来完成任务,每当有任务阻塞的时候就await,然后其他协程继续工作。创建多个协程的列表,然后将这些协程注册到事件循环中。
结果如下:
总时间为4s左右。4s的阻塞时间,足够前面两个协程执行完毕。如果是同步顺序的任务,那么至少需要7s。此时我们使用了aysncio实现了并发。asyncio.wait(tasks) 也可以使用 asyncio.gather(*tasks) ,前者接受一个task列表,后者接收一堆task。
03 asyncio+aiohttp实现异步爬虫
aiohttp是基于asyncio实现的HTTP框架。正常情况下如果需要并发http请求怎么办呢,通常是使用requests,但requests是同步的库,如果想异步的话需要引入aiohttp库。从中引入一个类ClientSession,首先要建立一个session对象,然后用session对象去打开网页。session可以进行多项操作,比如post, get, put, head等。
官方是推荐使用ClientSession来管理会话的,这个和requests中的session相似,用法也相似,使用session.get()去发送get请求,返回的resp中就有我们所需要的数据了,用法也和requests相似。
重点来了,aiohttp是异步的。在python3.5中,加入的asyncio/await 关键字,使得回调的写法更加直观和人性化。而aiohttp是一个提供异步web服务的库,asyncio可以实现单线程并发IO操作。requests写爬虫是同步的,是等待网页下载好才会执行下面的解析、入库操作,如果下载网页时间太长从而导致阻塞,这时候使用aiycnio异步执行就太合适了。当然使用进程或者多线程来加速爬虫也是一种方法。但是要考虑资源消耗和线程安全的问题。
我们现在使用的aiohttp是异步的,简单来说,就是不需要等待,你尽管去下载网页就好了,我不用傻傻的等待你完成才进行下一步,我还有别的活要干。这样就极大的提高了下载解析网页的效率。
下面就介绍一下aiohttp爬虫的常用的方法,与request中session方法基本相似,但稍有不同。
基本用法
#异步创建session对象
async with ClientSession() as session:
#异步get访问网站
async with session.get(url) as response:
发起一个session请求
上面的代码中,我们创建了一个 ClientSession 对象命名为session,然后通过session的get方法得到一个 ClientResponse 对象,命名为r,get方法中传入了一个必须的参数url,就是要获得源码的url。至此便通过协程完成了一个异步IO的get请求。
有get请求当然有post请求,并且post请求也是一个协程:
url= ‘#/post’
session.post(url=url, data=b'data')
用法和get是一样的,区别是post需要一个额外的参数data,即是需要post的数据。
除了get和post请求外,其他http的操作方法也是一样的:
在URL中传递参数
我们经常需要通过 get方法在url中传递一些参数,参数将会作为url问号后面的一部分发给服务器。在aiohttp的请求中,允许以dict的形式来表示问号后的参数。
不管是一个参数还是多个参数,都可以通过这种方式来传递。除了这种方式之外,还有另外一个方法,使用一个 list 来传递(它拥有一些dict形式无法完成的特殊用法,例如下面两个key,如果key1和key2的是相等的也可以正确传递):
除了上面两种,我们也可以直接通过传递字符串作为参数来传递,但是需要注意,通过字符串传递的特殊字符不会被编码:
响应的内容
运行之后,会打印出类似于如下的内容:
resp的text方法,会自动将服务器端返回的内容进行解码--decode,当然我们也可以自定义编码方式:
resp.text(encoding='gbk')
除了text方法可以返回解码后的内容外,我们也可以得到类型是字节的内容(如图片,视频):resp.read()
如果我们获取的页面的响应内容是json,aiohttp内置了更好的方法来处理json: resp.json()
也可以给json()方法指定一个解码方式resp.json(encoding='gb2312'))
响应头获取:resp.headers
响应吗获取:resp.status
cookie查看:resp.cookies
增加headers
设置代理和超时
另外一种代理设置:(只限于http的代理)
控制同时连接的数量
也可以理解为同时请求的数量,为了限制同时打开的连接数量,我们可以将限制参数传递给连接器:
conn = aiohttp.TCPConnector(limit=30)
#同时最大进行连接的连接数为30,默认是100,limit=0的时候是无限制
限制同时打开连接到同一端点的数量((host, port, is_ssl) 三的倍数),可以通过设置 limit_per_host 参数:
conn = aiohttp.TCPConnector(limit_per_host=30)
#默认是0
04 同步与异步对比总结
下面我们以爬取豆瓣热门电视剧前50页为例,来测试一下。在不使用多核下的情况下,requests库的同步爬虫与asynicio+aiohttp的异步爬虫做个对比:
如上图:经过反复测试,通过requests爬取50页的耗时大约是十几秒。因为每爬取一个页面都需阻塞等待响应。
如上图:经过反复测试,利用协程异步爬取50页的耗时都是一秒不到。
通过上面两个例子可以看到,在不利用多核的情况下,异步爬虫与同步爬虫相比,两者耗时相差十几倍,爬取效率很高。
正是由于通过协程实现异步,每次爬取一个页面都不需阻塞等待响应,只需要记住此栈区,继续执行其他任务,当阻塞结束后,回到之前的栈区完成之前的任务,从而大大的提高执行效率。当然如果需要利用计算机多核的话,可以使用多进程+协程,它不仅利用了计算机的多核,还又使每个核心的执行效率大大提高。
免责声明:内容和图片源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。
填写下面表单即可预约申请免费试听! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!