python爬虫 --协程

目录

前言

协程的主要概念

一个简单的协程

注册协程的三种方式

使用协程的一般步骤

并发运行协程

gather与wait(此示例来自ChatAl)

aiohttp(此示例来自ChatAl)

异步上下文管理器

通过生成器理解协程

前言

        爬虫是IO密集型任务,在爬取过程中需要等待网站的响应,在这个等待过程中我们可以使用并发来加快爬取的效率,实现并发的可以是多线程多进程,那为什么还要使用协程呢?

并发主要是利用程序IO操作时CPU空闲时间调用其他程序运行的方式来提高资源的利用率,但是不管是进程还是线程,切换时都会带来巨大开销,CPU内核的切换,上下文的保存和恢复等。而协程,相较线程来说更加轻量级的;它不被操作系统的内核所管理,完全由程序所控制,不需要CPU内核的切换,也能实现并发。

协程的主要概念

evenloop:事件循环中心,提供了注册,取消,回调任务和回调方法,是一个永久循环,注册函数到这个事件循环中心中,当满足某些条件时再调用此函数。

coroutine:协程,表示协程的对象类型,用async关键字声明的方法为协程方法

await:在协程方法中,await关键字会将耗时等待的操作挂起,让出控制权。如果协程在执行的时候遇到await关键字,事件循环就会将此协程挂起,转而执行别的协程,直到其他协程挂起或者执行完毕。

await后面可等待对象的类型:coroutine,task,future

可以将协程注册到事件循环中心,等待被调用。

一个简单的协程

import asyncio
import time
async def a():
    print('suspending a')
    await asyncio.sleep(3)
    print('dead a')
async def b():
    print('suspending b')
    await asyncio.sleep(2)
    print('dead b')
async def main():
    print(f"started at {time.strftime('%X')}")
    await a()#挂起协程a
    await b()#挂起协程b
    print(f"finished at {time.strftime('%X')}")
if __name__ == '__main__':
    asyncio.run(main())
started at 10:59:25
suspending a
dead a
suspending b
dead b
finished at 10:59:30

这个程序声明了三个协程,分别是a,b,main

可以发现都是通过async声明一个协程,并在协程方法中通过await关键字挂起耗时操作

观察main协程方法,可以发现在方法中通过await挂起a,b协程(它们是可等待对象),main方法中传递协程a和协程b,这样我们只需将main注册到事件循环中,那么协程都可以执行了

注意:协程只用注册到事件循环中才能正常执行。

注册协程的三种方式

1、由上面的协程程序,可以发现通过asncio.run()方法可以将协程注册到事件循环中,这个方法是python3.7出现的高层API。用于运行一个协程方法并管理底层的事件循环。当然还有下面的两种方法

2、

loop = asyncio.get_event_loop()#获取当前的事件循环

loop.run_until_complete(main())#运行协程

3、

loop=asyncio.new_event_loop()#创建一个新的事件循环

loop.run_until_complete(main())#运行协程

使用协程的一般步骤

  1. 定义异步函数:使用async关键字定义一个异步函数,可以在函数名前面加上async修饰符。异步函数可以包含await关键字来等待其他的异步操作完成。

  2. 创建事件循环:在Python中,异步操作通常在事件循环(Event Loop)中执行。通过调用asyncio.get_event_loop()来获取默认的事件循环对象,或使用asyncio.new_event_loop()或者asyncio.run()来创建新的事件循环对象。

  3. 执行协程:通过await关键字等待协程函数的执行结果。将协程函数包装在asyncio.run()函数中,或使用事件循环的run_until_complete()方法来启动协程的执行。

并发运行协程

根据上述的协程程序可以发现,程序运行了5秒,并没有实现并发,程序中其实是顺序运行的,那么该如何让它实现并发呢?

先了解下task对象

Task:Task对象用于封装一个协程函数,包括协程的各个状态,以便在事件循环中进行调度和执行。Task对象提供了一种方便、灵活和高效的方式来管理异步任务,实现并发执行,并且提供了错误处理、回调函数、取消等额外的功能。

封装的函数会被自动调度执行, 函数以异步方式并发运行协程

1、asyncio.create_task() 封装协程

import asyncio
import time
async def a():
    print('suspending a')
    await asyncio.sleep(3)
    print('dead a')
async def b():
    print('suspending b')
    await asyncio.sleep(2)
    print('dead b')
async def main():
    print(f"started at {time.strftime('%X')}")
    taska=asyncio.create_task(a())
    taskb=asyncio.create_task(b())
    await taska#挂起协程a
    await taskb#挂起协程b
    print(f"finished at {time.strftime('%X')}")
if __name__ == '__main__':
    asyncio.run(main())
started at 11:21:34
suspending a
suspending b
dead b
dead a
finished at 11:21:37

运行时间3秒,由此可以协程实现并发运行了,当然将协程封装成task对象还有下面两种方式

2、asncio.ensure_future :接收的参数不仅包括协程还包括Future对象或者是awaitable对象

import asyncio
import time
async def a():
    print('suspending a')
    await asyncio.sleep(3)
    print('dead a')
async def b():
    print('suspending b')
    await asyncio.sleep(2)
    print('dead b')
async def main():
    print(f"started at {time.strftime('%X')}")
    taska=asyncio.ensure_future(a())
    taskb=asyncio.ensure_future(b())
    await taska#挂起协程a
    await taskb#挂起协程b
    print(f"finished at {time.strftime('%X')}")
if __name__ == '__main__':
    asyncio.run(main())

3、loop.create_task:接收的参数为一个协程

import asyncio
import time
async def a():
    print('suspending a')
    await asyncio.sleep(3)
    print('dead a')
async def b():
    print('suspending b')
    await asyncio.sleep(2)
    print('dead b')
async def main():
    print(f"started at {time.strftime('%X')}")
    # taska=asyncio.create_task(a())
    # taskb=asyncio.create_task(b())
    # taska=asyncio.ensure_future(a())
    # taskb=asyncio.ensure_future(b())
    loop=asyncio.get_event_loop()
    taska=loop.create_task(a())
    taskb=loop.create_task(b())
    await taska#挂起协程a
    await taskb#挂起协程b
    print(f"finished at {time.strftime('%X')}")
if __name__ == '__main__':
    asyncio.run(main())

运行结果同样为3秒 如下:

started at 11:23:44
suspending a
suspending b
dead b
dead a
finished at 11:23:47

gather与wait(此示例来自ChatAl)

如果我们要并发运行很多协程方法并获取到结果,仅通过封装成task已经力不从心了,ok,gather和wait可以派上用场了。

gather:让多个协程实现并发执行,返回值为协程的运行结果,而且它会按照输入协程的顺序返回

wait:让多个协程实现并发执行,返回值有两项,第一项表示为完成任务的列表,第二项表示等待future任务完成的结果,每个任务都是task实例,可以使用task.result()获取协程返回值,此外wait还支持返回的时机。

1、当使用asyncio.gather()函数时,可以简单地传递多个协程函数(或者其他可等待对象),然后它会同时执行这些协程,并收集它们的结果。

import asyncio

async def coroutine1():
    print("Coroutine 1 started")
    await asyncio.sleep(2)  # 模拟耗时操作
    print("Coroutine 1 completed")
    return "Result from Coroutine 1"

async def coroutine2():
    print("Coroutine 2 started")
    await asyncio.sleep(1)  # 模拟耗时操作
    print("Coroutine 2 completed")
    return "Result from Coroutine 2"

async def main():
    tasks = [coroutine1(), coroutine2()]
    results = await asyncio.gather(*tasks)  # 并发执行协程,并收集结果
    print("Results:", results)

asyncio.run(main())

在这个例子中,我们定义了两个协程函数coroutine1()coroutine2()。每个协程函数打印开始和完成的消息,然后使用asyncio.sleep()来模拟耗时操作。

main()函数中,我们创建一个任务列表tasks,包含了这两个协程函数。然后,我们使用await asyncio.gather(*tasks)将这些任务传递给asyncio.gather()函数。通过使用*tasks语法,我们将任务列表中的每个任务作为单独的参数传递给asyncio.gather()

asyncio.gather()函数会同时运行这些协程函数,并在所有协程都完成后返回结果列表。在这个示例中,我们将结果赋值给results变量,并打印出来。

最后,我们使用asyncio.run(main())来运行主协程。

2、wait

asyncio.wait()函数用于并发执行多个协程任务,并等待它们的完成。它接受一个可迭代的协程任务集合作为参数,并返回一个包含已完成任务集合和未完成任务集合的元组。

import asyncio

async def coroutine1():
    print("Coroutine 1 started")
    await asyncio.sleep(2)  # 模拟耗时操作
    print("Coroutine 1 completed")
    return "Result from Coroutine 1"

async def coroutine2():
    print("Coroutine 2 started")
    await asyncio.sleep(1)  # 模拟耗时操作
    print("Coroutine 2 completed")
    return "Result from Coroutine 2"

async def main():
    tasks = [coroutine1(), coroutine2()]
    done, pending = await asyncio.wait(tasks)  # 并发执行协程,并等待完成

    # 处理已完成的任务
    for task in done:
        result = await task  # 获取任务的结果
        print("Task result:", result)

asyncio.run(main())

在这个例子中,我们定义了两个协程函数coroutine1()coroutine2(),它们模拟了耗时的操作。

main()函数中,我们创建了一个任务列表tasks,包含了这两个协程函数。

使用await asyncio.wait(tasks)来等待并发执行这些协程任务。asyncio.wait()函数返回一个元组(done, pending),其中done是已完成的任务集合,pending是未完成的任务集合。

我们通过遍历done集合,并使用await task获取各个任务的结果。这里的taskasyncio.Task对象,我们通过await关键字等待并获取任务的结果。

最后,我们使用asyncio.run(main())来运行主协程。

aiohttp(此示例来自ChatAl)

aiohttp是一个基于asyncio的异步HTTP网络模块,他提供了服务端和客户端。在客户端中我们可以发起请求,类似于使用requests发送的一个HTTP请求。注意:requests模块不能在协程中使用,因为requests模块是同步的。

import aiohttp
import asyncio

async def fetch(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as response:
            return await response.text()

async def main():
    url = "https://api.example.com/data"  # 替换为你要请求的URL
    response = await fetch(url)
    print("Response:", response)

asyncio.run(main())

在这个示例中,我们首先定义了一个fetch()函数,它使用aiohttp发送异步的HTTP GET请求。

在fetch()函数中,我们使用aiohttp.ClientSession()创建一个客户端会话对象。然后,我们使用session.get(url)发送GET请求,并使用response.text()方法获取响应的文本内容。

在main()函数中,我们定义了要请求的URL,然后调用fetch()函数并等待其返回响应结果。最后,我们打印出响应的内容。

需要注意的是,aiohttp.ClientSession()是一个上下文管理器,使用async with语法来确保会话在请求完成后正确关闭和释放资源。

最后,我们使用asyncio.run(main())来运行主协程。

异步上下文管理器

它相当于一个协程,在异步编程中,我们可以使用async with语法来使用上下文管理器。上下文管理器提供了进一步管理异步任务执行的能力,并确保在进入和退出上下文时正确地处理资源分配和清理。异步上下文管理器是Python 3.7中引入的一个特性,用于在异步环境中更好地管理资源。

通过生成器理解协程

协程的实现原理是利用了生成器的特性,下面通过生成器来简单了解协程的原理

这个任务实现需要三个步骤,每个步骤在处理前都通过yield交出控制权,

def test(data):
    print('task with data {} starter'.format(data))
    yield
    print('step one for data {} finished'.format(data))
    yield
    print('step two for data {} finished'.format(data))
    yield
    print('step over for data {} finished'.format(data))

testa=test('a')

if __name__ == '__main__':
    print(type(testa))
    next(testa)
    next(testa)
    next(testa)
  • 含yield关键字的函数为生成器函数
  • 生成器函数不能被直接使用,类型为generator
  • 调用next函数可激活生成器函数,在执行到yield关键字处,程序会将控制权交出
  • 生成器函数在交出控制权时可以记住自己执行的位置,下次在被调用时从上次位置接着执行
  • 再次调用next(),程序会从上次yield出开始执行直到遇到yield关键字,或者程序执行完毕抛出异常

task with data a starter
step one for data a finished
step two for data a finished
step over for data a finished
Traceback (most recent call last):
  File "d:/python/repitile/request/asnciolearning.py", line 43, in 
    next(testa)
StopIteration

协程调度

def test(data):
    print('task with data {} starter'.format(data))
    yield
    print('step one for data {} finished'.format(data))
    yield
    print('step two for data {} finished'.format(data))
    yield
    print('step over for data {} finished'.format(data))

if __name__ == '__main__':
    test_a=test('a')
    test_b=test('b')
    test_c=test('c')
    #调度协程
    next(test_a)
    next(test_a)
    next(test_a)
    
    next(test_b)
    next(test_c)
    next(test_b)
    next(test_c)
    next(test_b)
    next(test_c)
  • 该程序执行了三个任务,分别为tast_a,tast_b,tast_c
  • 在程序中调度tast_a执行先执行三次,tast_b,tast_c交替执行
  • yield模拟了程序在遇到网络IO的情况,协程的处理方式,将该程序暂时挂起,执行其他任务。
  • 通过人为的调用来作为事件调度器,在协程中这个角色是由事件循环(evenloop)来担任的。
task with data a starter
step one for data a finished
step two for data a finished
task with data b starter
task with data c starter
step one for data b finished
step one for data c finished
step two for data b finished
step two for data c finished

好的,就到这里了,屏幕前的你真好看

你可能感兴趣的:(python爬虫,python,爬虫)