06爬虫-异步协程

Posted lishuntao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了06爬虫-异步协程相关的知识,希望对你有一定的参考价值。

1. 前言(目的就是大大提升爬虫效率)

  在执行IO密集型任务的时候,代码常常遇到IO操作而等待。例如我们在爬虫的时候,用到requests请求的时候,网页响应慢,一直等待着,那么爬虫的效率会大大的降低。

为了解决这类问题,本文就来探讨一下 Python 中异步协程来加速的方法,此种方法对于 IO 密集型任务非常有效。如将其应用到网络爬虫中,爬取效率甚至可以成百倍地提升。

注:本文协程使用 async/await 来实现,需要 Python 3.5 及以上版本。

2. 基本了解

在了解异步协程之前,我们首先的先了解阻塞和非阻塞、同步和异步、多进程和协程。

2.1 阻塞(常见的输入输出IO阻塞(必须等待完成才可以继续下一步))

  阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。

常见的阻塞形式有:网络 I/O 阻塞、磁盘 I/O 阻塞、用户输入阻塞等。阻塞是无处不在的,包括 CPU 切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。如果是多核 CPU 则正在执行上下文切换操作的核不可被利用。

2.2 非阻塞(当遇到IO操作然后不等待继续做别的事情)

  程序在等待某操作过程中,自身不被阻塞,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。非阻塞并不是在任何程序级别、任何情况下都可以存在的。

仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。

2.3 同步(也就是按照顺序执行走)

  不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,称这些程序单元是同步执行的。强制排队顺序执行,那更新库存的操作是同步的。

简言之,同步意味着有序。

2.4 异步(也就是不按照顺序走,例如回调函数这种就叫异步)

  为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式,不相关的程序单元之间可以是异步的。例如,爬虫下载网页。调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。

简言之,异步意味着无序。

2.5 多进程(同一时间并行的执行多个任务)

  多进程就是利用 CPU 的多核优势,在同一时间并行地执行多个任务,可以大大提高执行效率。

  2.5.1 多线程(同一时间并发(不能同时进行)的执行多个任务)

2.6 协程(程序员发明可以来回在程序中切换的)

  协程,英文叫做 Coroutine,又称微线程,纤程,协程是一种用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入时,就相当于进入上一次调用的状态。

协程本质上是个单进程,协程相对于多进程来说,无需线程上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。

我们可以使用协程来实现异步操作,比如在网络爬虫场景下,我们发出一个请求之后,需要等待一定的时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他的事情,等到响应得到之后才切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是异步协程的优势。

3. 异步协程用法

  接下来让我们来了解下协程的实现,从 Python 3.4 开始,Python 中加入了协程的概念,但这个版本的协程还是以生成器对象为基础的,在 Python 3.5 则增加了 async/await,使得协程的实现更加方便。

Python 中使用协程最常用的库莫过于 asyncio,所以本文会以 asyncio 为基础来介绍协程的使用。

首先我们需要了解下面几个概念:

  • event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生的时候,就会调用对应的处理方法。

  • coroutine:中文翻译叫协程,在 Python 中常指代为协程对象类型,我们可以将协程对象注册到时间循环中,它会被事件循环调用。我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。

  • task:任务,它是对协程对象的进一步封装,包含了任务的各个状态。

  • future:代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。

另外我们还需要了解 async/await 关键字,它是从 Python 3.5 才出现的,专门用于定义协程。其中,async 定义一个协程,await 用来挂起阻塞方法的执行。

3.1 定义协程

首 

import asyncio


async def request(url):
    print("请求成功!",url)
    print("下载成功!",url)
#1、实例化一个协程对象a
a = request("www.baidu.com")
# print(a)#<coroutine object request at 0x000001C62C136DC8>这是个协程对象
# print(type(a))#<class ‘coroutine‘>
#2、导入asyncio包并实例化一个事件循环对象loop
loop = asyncio.get_event_loop()
#3、创建一个任务对象,将协程对象放入任务对象中
#task = loop.create_task(a)
#第二种创建任务对象的方式
task = asyncio.ensure_future(a)
print("task:",task)#<Task pending coro=<request() 这里面显示未运行pending(即将发生的)
#4、将创建的任务对象,放入任务循环对象中启动循环事件
loop.run_until_complete(task)
print(task)#<Task finished coro=<request() 现在显示循环已经完成了

协程的回调函数:

import asyncio
async def request(url):
    print("1")
    print("2")
    return "ok 1"
#1、创建协程对象
c = request("www.baidu.com")
#默认要传入task函数,不然会出现报错
def callback_func(task):
    print("这是在回调函数")
    print( task.result())
#2、创建事件循环对象
loop = asyncio.get_event_loop()
#3、创建任务对象
# task = loop.create_task(c)
task = asyncio.ensure_future(c)
#5、给任务对象绑定一个回调函数
task.add_done_callback(callback_func)#先执行完协程对象后才执行回调函数
#4、任务对象载入,事件循环开始
loop.run_until_complete(task)
print(task)

多任务异步协程:

import time
import asyncio
urls = [www.baidu.com,www.sogou.com,www.biying.com]
start = time.time()
async def request(url):
    print("请求中:",url)
    # time.sleep(2)#6.000685453414917
    #显示的结果没有产生变化,因此可能是时间这里出了问题
    await asyncio.sleep(2) #必须在前面添加等待 否则结果也没有等待
    # 添加await之后的结果2.000333309173584
    print("结束了:",url)
#实例化事件循环对象
loop = asyncio.get_event_loop()
#将大量任务分别实例化参数传入协程对象添加到列表中
tasks = []
for url in urls:
    #实例化协程对象
    c = request(url=url)
    #实例化每个任务对象
    # task = loop.create_task(c)
    task = asyncio.ensure_future(c)
    tasks.append(task)
#启动事件循环,执行时候必须添加协程等待 否则报错
loop.run_until_complete(asyncio.wait(tasks))
#执行是前面的执行到等待立即切换下一个协程当时间到了又切换回来
print(time.time()-start)
######重点#######
"""
完全实现了单线程多携程的高效爬取任务,大大提高效率,
这就叫多任务异步协程(除此之外,提高爬虫效率,还有多线程)
"""

多任务异步协程爬虫的应用:

利用flaskServer来练习多任务异步爬虫:

from flask import Flask
import time

app = Flask(__name__)


@app.route(/lishuntao)
def index_bobo():
    time.sleep(2)
    return Hello lishuntao

@app.route(/li)
def index_jay():
    time.sleep(2)
    return Hello li

@app.route(/wang)
def index_tom():
    time.sleep(2)
    return Hello wang

if __name__ == __main__:
    app.run(threaded=True)

启动服务端:然后复制路由到浏览器,正确路由有返回值,说明服务端开启成功

多任务异步协程爬虫代码:

 

# import requests
# import asyncio
# import time
# urls = [
#     ‘http://127.0.0.1:5000/li‘,
#     ‘http://127.0.0.1:5000/lishuntao‘,
#     ‘http://127.0.0.1:5000/wang‘
# ]
#################第一种方法不能实现提速############################
# start = time.time()
# async def get_Text(url):
#     #从结果看时间没有提升,因此函数的代码requests出了问题
#
#    # 从这里可以总结出time.sleep()和requests不能提升效率
#     #因此应该换第二种方法
#     c = requests.get(url)#6.037005186080933
#     print(c.text)
#
# #实例化事件循环对象
# loop = asyncio.get_event_loop()
# tasks = []
# for url in urls:
#     #实例化协程对象
#     a = get_Text(url)
#     # 创建任务对象 多个任务添加到列表中
#     # task = loop.create_task(a)
#     task = asyncio.ensure_future(a)
#     tasks.append(task)
# #启动事件循环,并且参数需要等待(异步从这里体现出来)
# loop.run_until_complete(asyncio.wait(tasks))
# print(time.time()-start)
############################################################################
#单线程加异步协程
import asyncio
import time
import aiohttp
#第二个可以代替requests请求的模块(可以记住就在协程中使用)


urls = [
    http://127.0.0.1:5000/li,
    http://127.0.0.1:5000/lishuntao,
    http://127.0.0.1:5000/wang
]
start = time.time()
async def get_Text(url):
    #从结果看时间没有提升,因此函数的代码requests出了问题
    # c = requests.get(url)#6.037005186080933
    #这下面是函数,因此需要转换为协程函数,再加上名字很长使用不方便
    #因此别名后再声明是协程函数
    async with aiohttp.ClientSession() as s:
        #2、下面是请求函数,所以需要异步请求等待,名字太长,再别名,
        # 因为这是函数所以声明是协程函数
        async with await s.get(url) as response:#这个请求效率变成2s了 效率大大提升了
            #下面直接text()函数,因此发现需要挂起来
            page_text = await response.text()
            return page_text
#########直接运行发现返回值没有出来因此将前面知识结合起来
#########给他绑定回调函数让回调函数解析数据####################
def parser(task):
    text = task.result()
    print(text,"数据开始准备解析!!!")
#实例化事件循环对象
loop = asyncio.get_event_loop()
tasks = []
for url in urls:
    #实例化协程对象
    a = get_Text(url)
    # 创建任务对象 多个任务添加到列表中
    # task = loop.create_task(a)
    task = asyncio.ensure_future(a)
    #给task绑定回调函数,下面数据请求执行完回来执行数据解析(参数默认就是task)
    task.add_done_callback(parser)
    tasks.append(task)
#启动事件循环,并且参数需要等待(异步从这里体现出来)
loop.run_until_complete(asyncio.wait(tasks))
print(time.time()-start)

总结:异步协程(高效率单线程多任务异步协程)爬取数据,第一步声明协程对象(async),第二步声明事件循环对象(loop = asyncio.get_event_loop()),第三步实例化task任务对象(task=loop.create_task() or task = asyncio.ensure_future()),多任务就实例化多次,添加到一个列表中。第四步给task绑定回调函数(task.add_done_callback(函数名)),第五步启动事件循环(loop.run_until_complete())(参数是tasks,注意需要挂起等待不然报错asyncio.wait())。

以上是关于06爬虫-异步协程的主要内容,如果未能解决你的问题,请参考以下文章

python爬虫--多任务异步协程, 快点,在快点......

异步爬虫-多任务异步协程示例

爬虫 异步协程

Python爬虫之协程,异步协程和多任务异步协程

异步爬虫

高性能异步爬虫02