更多课程 选择中心


Python培训

400-111-8989

如何提高Python爬取效率?

  • 发布:Python培训
  • 来源:米么骚客
  • 时间:2019-06-13 13:22

为提高Python爬虫爬取数据的效率,我们最先想到的就是使用多线程或者多进程。但是从性能和计算机资源利用率上来讲,通过协程实现的异步爬取又似乎更加合适。

既然提到了协程,就不得不说一下进程和线程了。首先我们来简单了解一下python中进程、线程、协程。

01 进程、线程、协程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

如下图,最大的黑框可以看作一个运行着的程序,这个程序中包括了三个同时运行的进程,每个进程都是独立的。在每个进程中又包括了三个线程,从而构成了整个程序。

如何提高Python爬取效率

线程,又称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

协程,又称微线程,纤程。与线程的抢占式调度不同,它是协作式调度。如下图所示,协程最大的优势就是极高的执行效率,因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

如何提高Python爬取效率

协程的优点是无需线程上下文切换的开销,协程避免了无意义的调度,拥有极高的执行效率。无需原子操作锁定及同步的开销,它是线程安全的。方便切换控制流,简化编程模型。协程的缺点就是无法利用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用于挂起阻塞的异步调用接口。

如何提高Python爬取效率

上面两种方法基本上一样。第二种使用方法比较方便,所以推荐使用第二种方法。

如何提高Python爬取效率

通过async关键字来定义一个协程。协程不能直接运行,需要把协程加入到事件循环(loop),由loop在适当的时候调用协程。asyncio.get_event_loop方法可以创建一个事件循环,然后使用run_until_complete将协程注册到事件循环,并启动事件循环。因为本例只有一个协程,于是可以看见如下输出:

如何提高Python爬取效率

创建一个task

协程对象是不能直接运行,在注册事件循环的时候,其实是用run_until_complete方法将协程对象包装成为了一个任务(task)对象。task对象是Future类的子类。保存了协程运行后的状态,用于未来获取协程的结果。

如何提高Python爬取效率

可以看到输出结果为:

如何提高Python爬取效率

创建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操作异步化。

如何提高Python爬取效率

结果如下:

如何提高Python爬取效率

在 sleep的时候,使用await让出控制权。即当遇到阻塞调用的函数的时候,使用await方法将协程的控制权让出,以便loop调用其他的协程。这个例子就用耗时的阻塞操作了。

并发和并行

并发和并行一直是容易混淆的概念。并发通常指有多个任务需要同时进行,并行则是同一时刻有多个任务执行。

如何提高Python爬取效率

asyncio实现并发,就需要多个协程来完成任务,每当有任务阻塞的时候就await,然后其他协程继续工作。创建多个协程的列表,然后将这些协程注册到事件循环中。

如何提高Python爬取效率

结果如下:

如何提高Python爬取效率

总时间为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请求

如何提高Python爬取效率

上面的代码中,我们创建了一个 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的操作方法也是一样的:

如何提高Python爬取效率

在URL中传递参数

我们经常需要通过 get方法在url中传递一些参数,参数将会作为url问号后面的一部分发给服务器。在aiohttp的请求中,允许以dict的形式来表示问号后的参数。

如何提高Python爬取效率

不管是一个参数还是多个参数,都可以通过这种方式来传递。除了这种方式之外,还有另外一个方法,使用一个 list 来传递(它拥有一些dict形式无法完成的特殊用法,例如下面两个key,如果key1和key2的是相等的也可以正确传递):

如何提高Python爬取效率

除了上面两种,我们也可以直接通过传递字符串作为参数来传递,但是需要注意,通过字符串传递的特殊字符不会被编码:

响应的内容

如何提高Python爬取效率

运行之后,会打印出类似于如下的内容:

如何提高Python爬取效率

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

如何提高Python爬取效率

设置代理和超时

如何提高Python爬取效率

另外一种代理设置:(只限于http的代理)

如何提高Python爬取效率

控制同时连接的数量

也可以理解为同时请求的数量,为了限制同时打开的连接数量,我们可以将限制参数传递给连接器:

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的异步爬虫做个对比:

如何提高Python爬取效率

如上图:经过反复测试,通过requests爬取50页的耗时大约是十几秒。因为每爬取一个页面都需阻塞等待响应。

如何提高Python爬取效率

如上图:经过反复测试,利用协程异步爬取50页的耗时都是一秒不到。

通过上面两个例子可以看到,在不利用多核的情况下,异步爬虫与同步爬虫相比,两者耗时相差十几倍,爬取效率很高。

正是由于通过协程实现异步,每次爬取一个页面都不需阻塞等待响应,只需要记住此栈区,继续执行其他任务,当阻塞结束后,回到之前的栈区完成之前的任务,从而大大的提高执行效率。当然如果需要利用计算机多核的话,可以使用多进程+协程,它不仅利用了计算机的多核,还又使每个核心的执行效率大大提高。

免责声明:内容和图片源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

预约申请免费试听课

填写下面表单即可预约申请免费试听! 怕学不会?助教全程陪读,随时解惑!担心就业?一地学习,可全国推荐就业!

上一篇:如何应对普通反Python爬虫机制?
下一篇:Python的内存管理机制及调优手段是怎么回事?

Python培训这么多,靠谱的Python培训班怎么选?

人工智能工程师证书怎么考?

AI怎么裁剪多余的部分?

ai开发选择哪种编程语言?

  • 扫码领取资料

    回复关键字:视频资料

    免费领取 达内课程视频学习资料

Copyright © 2023 Tedu.cn All Rights Reserved 京ICP备08000853号-56 京公网安备 11010802029508号 达内时代科技集团有限公司 版权所有

选择城市和中心
黑龙江省

吉林省

河北省

湖南省

贵州省

云南省

广西省

海南省