带你们了解python协议

本文深入探讨了Python中的协程概念,解释了协程如何作为轻量级的微线程提升性能,减少了上下文切换的开销。同时,文章介绍了Python实现协程的方式,包括async/await语法,并展示了并发执行任务的例子。最后,通过实战案例爬取彼岸图网,演示了如何在实际项目中运用协程抓取和处理数据。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天为大家带来的内容是协程概念的详解以及配套实战内容。

协程

协程的概念

协程,又称作是微线程,协程是一种用户态的轻量级编程。

其实,怎么样来理解会更好一些呢?

我这样说吧,线程是系统级别的,它们由操作系统调度;协程是程序级别的,由程序员根据自己的需求去调度。我们把线程中一个个函数叫做子程序,那么子程序在执行过程中可以中断,然后去执行别的子程序;别的子程序也可以中断回来继续执行之前的子程序,这就是协程。也就是说,在同一个线程下的一段代码,在执行的过程中,有可能会发生中断,然后跳去执行另一段代码,当再次回来执行之前的代码块的时候,就从之前中断的地方开始执行。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来的时候,恢复之前保存的寄存器上下文和栈。因此:协程可以保留上一次调用时的状态,每次过程重入的时候,就相当于进入上一次调用的状态。换种说法就是进入上一次离开时所处逻辑留的位置。

协程的优点

  • 协程不需要像线程一样存在上下文切换的开销,避免了无意义的调度,由此可以提高性能。

  • 无需原子操作锁定及同步的开销

  • 方便切换控制流,简单化编程模型

  • 高并发+高拓展性+低成本:一个CPU支持上万的协程都不是问题,所有很合适用于高并发处理。

协程的缺点

  • 无法利用多核资源:协程的本质是单线程,因此它不能将单个CPU的多个核都用上,协程需要和进程配合才能运行在多核CPU上。

  • 进行阻塞操作时会阻塞掉整个程序

python如何实现协程

协程是通过async/await语法进行声明,是编写asyncio应用的推荐方式。具体代码,如下所示:

import asyncio


async def main():
 print('hello')
 await asyncio.sleep(5)
 print('world')


asyncio.run(main())

关键字await有两个作用,一是作为求值的关键字,二是将异步操作变为同步操作;如果方法中使用了await,那么在方法的前面就必须加上async。

一般来说,正常的函数在执行的时候是不会被中断的,所有想要写一个可以中断的函数,就需要添加async。

像上面的代码,await asyncio.sleep(5),就是说这个程序被挂起5秒,5秒后再回来执行程序。

await用来声明程序挂起,比如异步程序执行到某一步时需要等待很长的时间,就将挂起,去执行其他异步程序,当挂起条件消失之后,不管当前程序是否执行完毕,都必须退出,去执行之前的程序。

asyncio.run()函数用来运行最高层级的入口点“main()”函数。

等待协程

import asyncio
import time


async def say_after(delay, what):
 await asyncio.sleep(delay)
 print(what)


async def main():
 print(f'start at {time.strftime("%X")}')

 await say_after(2, 'hello')
 await say_after(2, 'world')

 print(f'finish at {time.strftime("%X")}')

asyncio.run(main())

运行结果,如下所示:

start at 13:59:21
hello
world
finish at 13:59:25

多任务协程

使用create_task()函数用来并发运行作为asyncio任务的多个协程。如何并发运行两个任务呢?

具体代码,如下所示:

import asyncio
import time


async def say_after(delay, what):
 await asyncio.sleep(delay)
 print(what)


async def main():
 task1 = asyncio.create_task(say_after(2, 'hello'))
 task2 = asyncio.create_task(say_after(2, 'world'))
 print(f'start at {time.strftime("%X")}')

 await task1
 await task2

 print(f'finish at {time.strftime("%X")}')

asyncio.run(main())

运行结果,如下所示:

start at 16:28:53
hello
world
finish at 16:28:55

预期的显示输出时间比之前快了2秒。

可等待对象

如果一个对象可以在await语句中使用,那么它就是可等待对象,可等待对象一共有三种类型:协程、任务与Future。

协程属于python的可等待对象,因此可以在其他协程中被等待,具体代码,如下所示:

import asyncio


async def nested():
 return 42


async def main():
 nested() # 报错
 print(await nested()) # 正常


asyncio.run(main())

对于上面的代码,有一点要注意,那就是当我们创建了一个协程方法时,需要调用,就一定要用关键字await。

协程函数指的是:定义形式为async def的函数。

协程对象指的是:调用协程函数所调用的对象。

任务用来并行的调度协程,当一个协程通过create_task()等函数封装为一个任务,该协程会被自动调度执行。

具体代码,如下所示:

import asyncio


async def nested():
 return 42


async def main():
 task = asyncio.create_task(nested())
 await task


asyncio.run(main())

Future是一种低层级的对象,表示一个异步操作的最终结果。当一个Future对象被等待,这意味着协程将保持等待直到该Future对象在其他地方操作完毕。通常情况下没有必要在应用级的代码中创建Future对象。

并发运行任务

具体代码,如下所示:

import asyncio


async def factorial(name, number):
 f = 1
 for i in range(1, number+1):
  print(f'Task {name}: Compute factorial({number}), currrently i = {i}...')
  await asyncio.sleep(1)
  f *= i
 print(f'Task {name}: factorial({number}) = {f}')
 return f


async def main():
 L = await asyncio.gather(factorial('A', 2), factorial('B', 3), factorial('C', 4))
 print(L)


asyncio.run(main())

运行结果,如下所示:

Task A: Compute factorial(2), currrently i = 1...Task B: Compute factorial(3), currrently i = 1...Task C: Compute factorial(4), currrently i = 1...Task A: Compute factorial(2), currrently i = 2...Task B: Compute factorial(3), currrently i = 2...Task C: Compute factorial(4), currrently i = 2...Task A: factorial(2) = 2Task B: Compute factorial(3), currrently i = 3...Task C: Compute factorial(4), currrently i = 3...Task B: factorial(3) = 6Task C: Compute factorial(4), currrently i = 4...Task C: factorial(4) = 24[2, 6, 24]

通过gather()方法,可以将一个协程对象作为任务调度,当所有的可等待对象都已经完成,结果将是由一个所有值聚合而成的列表。

实战-彼岸图网

图片

image-20210808025920195

如上图所示,本次的实战案例是爬取彼岸图网,网址如下:

https://pic.netbian.com/4kdongman/

本次实战,需要各位小伙伴们掌握xpath语法与正则表达式。

在本次项目中,最重要的就是找到4K图片的下载地址,这一点很重要。

图片

当我点击下载原图时,我便捕获到了真正的url地址,如下所示:

https://pic.netbian.com/downpic.php?id=27068&classid=66

这里有两个查询参数,一个是id,另一个是classid,对于这两个参数,我的第一想法是看看网页中是否存在这两个参数的值。

图片

从上图可以看到,在网页上存在这两个参数的值,那么只需要利用xpath与正则表达式即可。

创建协程函数

在这里,我主要有创建三个任务,一个是获取详情页的url,一个是获取图片的url,最后一个保存图片。

具体代码,如下所示:

async def get_url(url):
    proxies = {
        'http://': 'http://182.38.188.206:4210',
        'https://': 'http://182.38.188.206:4210'
    }
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
    }
    response = httpx.get(url, headers=headers, proxies=proxies)
    html = etree.HTML(response.text)
    hrefs = html.xpath('//ul[@class="clearfix"]/li/a/@href')
    hrefs = ['https://pic.netbian.com/' + href for href in hrefs]
    return hrefs


async def get_img_url(hrefs):
    args = []
    img_urls = []
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
    }
    proxies = {
        'http://': 'http://182.38.188.206:4210',
        'https://': 'http://182.38.188.206:4210'
    }
    for href in hrefs:
        response = httpx.get(href, headers=headers, proxies=proxies)
        html = etree.HTML(response.text)
        src = html.xpath('//script[last()]/@src')[0]
        arg = re.findall('.*?(classid=\d+&id=\d+).*?', src)[0]
        img_url = 'https://pic.netbian.com/downpic.php?' + arg
        img_urls.append(img_url)

    return img_urls


async def save_img(img_urls):
    headers = {
        'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36'
    }
    proxies = {
        'http://': 'http://182.38.188.206:4210',
        'https://': 'http://182.38.188.206:4210'
    }
    for img_url in img_urls:
        i = 1
        response = httpx.get(img_url, headers=headers, proxies=proxies)
        data = response.content
        with open(f'./picture/{i}.jpg', 'wb') as f:
            f.write(data)
        i += 1

创建任务

async def main():
    url = 'https://pic.netbian.com/4kdongman/'
    task1 = asyncio.create_task(get_url(url))
    hrefs = await task1
    task2 = asyncio.create_task(get_img_url(hrefs))
    img_urls = await task2
    print(img_urls)
    task3 = asyncio.create_task(save_img(img_urls))
    await task3

最后

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值