python-协程/asyncio/aiohttp(流畅的python)

 

本文是流畅的python18章的例子,因为我看的是2015版的,不知道最新版有没有修改,关于协程那部分旧的api到现在已经不适用了,我都做了一些修改,保证代码能正常运行,有任何错误欢迎指出来。有关于协程asynicoaiohttp都在后面都有简单介绍。

协程例子-网络下载

了解协程之前,直接先看2个例子,从例子入手,先感受一下协程,这2个例子也是参考书本,去掉了多线程那个例子,因为本文说的是协程,就直接上协程的例子,关于协程部分如上述做了修改以便能运行。这个例子是网络下载的二种风格,因为网络下载具有很高IO延迟,所以不浪费CPU周期去等待,最好收到响应之前做其他的事。

以下例子环境:

Python v3.7.4
requests v2.23.0
aiohttp 3.7.4.post0

normal.py, 需要在代码运行的当前目录下先创建flags的文件夹。

import os
import time
import sys
import requests

POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
'MX PH VN ET EG DE IR TR CD FR').split()

BASE_URL = 'http://flupy.org/data/flags'

DEST_DIR = './flags'

def save_flag(img, filename):
    path = os.path.join(DEST_DIR, filename)
    with open(path, 'wb') as fp:
        fp.write(img)


def get_flag(cc):
    url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
    resp = requests.get(url)

    return resp.content

def show(text):
    print(text, end=' ')
    sys.stdout.flush()

def download_many(cc_list):
    for cc in sorted(cc_list):
        image = get_flag(cc)
        show(cc)
        save_flag(image, cc.lower() + '.gif')
    
    return len(cc_list)

def main(download_many):
    t0 = time.time()
    count = download_many(POP20_CC)
    elapsed = time.time() - t0
    msg = '\n{} flags downloaded in {:.2f}s'
    print(msg.format(count, elapsed))

if __name__ == '__main__':
    main(download_many)

运行

python normal.py
BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN
20 flags downloaded in 16.94s

asyncio_test.py, 一样要在代码运行目录下创建flags文件夹,如果nomal.py运行过了,先把文件夹内的图片删干净,再运行asyncio_test.py。

import os
import sys
import time
import asyncio
import aiohttp

POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
    'MX PH VN ET EG DE IR TR CD FR').split()

BASE_URL = 'http://flupy.org/data/flags'
DEST_DIR = './flags'

def save_flag(img, filename):
    path = os.path.join(DEST_DIR, filename)
    with open(path, 'wb') as fp:
        fp.write(img)

def show(text):
    print(text, end=' ')
    sys.stdout.flush()

async def get_flag(cc):
    url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            image = await resp.read()
    return image

async def download_one(cc):
    image = await get_flag(cc)
    show(cc)
    save_flag(image, cc.lower() + '.gif')
    return cc

async def download_many(cc_list):

    to_do = [asyncio.create_task(download_one(cc)) for cc in sorted(cc_list)]
    done, pending = await asyncio.wait(to_do)

    return len(done)

def main():
    t0 = time.time()
    count = asyncio.run(download_many(POP20_CC))
    elapsed = time.time() - t0
    msg = '\n{} flags downloaded in {:.2f}s'
    print(msg.format(count, elapsed))


if __name__ == '__main__':
    main()
python asyncio_test.py
TR EG BR BD ID FR VN JP CN IN RU NG DE MX PH CD ET US IR PK
20 flags downloaded in 0.97s

从这个地方可以看出协程的效果了,顺序下载和协程的下载时间,天差地别。

上述例子是运用了异步IO库asynico, 将介绍它的来由和用法,内容比较多,要了解asynico它的由来,工作流程和运用,得了解生成器原理,了解yied from,最后在到了解asynico一些api。

在流程的python里面,它一步步徐徐展开的,从第十四章就开始从可迭代对象/迭代器到生成器,生成器函数,到十六章开始讲基于生成器的协程,异常处理,yield from,第十八章协程例子正式用到asyncio,秉承这样一步步把这些章节自己所能理解过一遍。

基于生成器的协程

在asynico库里,基于生成器的协程是async/await的前身,并且是用yield from语句的生成器。下面只做简单介绍,因为关于这部分asyncio库里面描述到将来要弃用,但是目前还是兼容的, 它将在python 3.10移除,但是我觉得从它的前身开始理解有好处,清楚它的整个发展。

生成器函数

当一个函数里面有yield语句就是生成器函数,这也是它和普通函数唯一区别,简单看个书本例子:

>>> def gen_123(): 
...    yield 1
...    yield 2
...    yield 3

>>> gen_123 # doctest: +ELLIPSIS 
<function gen_123 at 0x...>
>>> gen_123() # doctest: +ELLIPSIS
<generator object gen_123 at 0x...>
>>> for i in gen_123(): 
... print(i) 123
>>> g = gen_123()
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
Traceback (most recent call last): 
... StopIteration

 可以看到直接调用gen_123()是不会有任何执行的,生成器函数会创建一个生成器对象,包装生成器函数的定义体。把生成器传给 next(...) 函数时,生成器函数会向前,执行函数定义体中的 下一个 yield 语句,返回产出的值,并在函数定义体的当前位置暂 停。最终,函数的定义体返回时,外层的生成器对象会抛出 StopIteration 异常——这一点与迭代器协议一致。

生成器进化成协程

协程协程,其实是一个过程,原来的生产器函数是产出值的,每次调用next就产出一个值,但是现在不单单想要生成器函数产出值,还想控制它每次调用到yield语句时给它发送个值从而影响它的逻辑,也就是说这个过程与调用方协作,产出由调用方提供的值。

如果我说的不够清楚,OK,先看下面例子,simple_coro2.

>>> def simple_coro2(a):
...     print('-> Started: a =', a)
...     b = yield a
...     print('-> Received: b =', b)
...     c = yield a + b
...     print('-> Received: c =', c)
...

>>> my_coro2 = simple_coro2(14)
>>> from inspect import getgeneratorstate
>>> getgeneratorstate(my_coro2)
'GEN_CREATED'
>>> next(my_coro2)
-> Started: a = 14
14
>>> getgeneratorstate(my_coro2)
'GEN_SUSPENDED'
>>> my_coro2.send(28)
-> Received: b = 28
42
>>> my_coro2.send(99)
-> Received: c = 99
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> getgeneratorstate(my_coro2)
'GEN_CLOSED'

从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的 函数。可是,在协程中,yield 通常出现在表达式的右边(例如,datum = yield),可以产出值,也可以不产出——如果 yield 关键字后面没有表达式,那么生成器产出 None。协程可能会从调用方 接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方 法,而不是 next(...) 函数。通常,调用方会把值推送给协程。

如果上面例子还是不够直观,直接贴上原来书本的流程图:

三个阶段。

(1) 调用 next(my_coro2),打印第一个消息,然后执行 yield a,产出数字 14。

(2) 调用 my_coro2.send(28),把 28 赋值给 b,打印第二个消息,然后执行 yield a + b,产出数字 42。

(3) 调用 my_coro2.send(99),把 99 赋值给 c,打印第三个消息,协 程终止。

基于 yield from 生成器

要理解yield from记住记住几个关键词,委托双向通道。

先来看一张图,有包含调用方,委派生成器,自生成器。委派生成器在 yield from 表达式处暂停时,调用方可以直 接把数据发给子生成器,子生成器再把产出的值发给调用方。子生成器返回之后,解释器会抛出 StopIteration 异常,并把返回值附 加到异常对象上,此时委派生成器会恢复

1.调用方:指代调用委派生成器的客户端代码。
2.委派生成器 :包含 yield from <iterable> 表达式的生成器函数。
3.子生成器:从 yield from 表达式中 <iterable> 部分获取的生成器。

如果能直接理解最好了,有关于yield from语义问题其实也有人提到为什么要那么复杂,我这里就提一下我的理解,不一定正确,提一个现实世界一个例子,有点类似中间商一样,当客户(调用方)有任何需求,可以通过中间商(委派生成器)传达给真正的厂家(自生成器)。

看一下yield from例子,书本上的例子太长,这边借鉴别人的例子,都有注释,参考的连接会放在文章底部:

# 子生成器
def average_gen():
    total = 0
    count = 0
    average = 0
    while True:
        new_num = yield average
        if new_num is None:
            break
        count += 1
        total += new_num
        average = total/count

    # 每一次return,都意味着当前协程结束。
    return total,count,average

# 委托生成器
def proxy_gen():
    while True:
        # 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。
        total, count, average = yield from average_gen()
        print("计算完毕!!\n总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))

# 调用方
def main():
    calc_average = proxy_gen()
    next(calc_average)            # 预激协程
    print(calc_average.send(10))  # 打印:10.0
    print(calc_average.send(20))  # 打印:15.0
    print(calc_average.send(30))  # 打印:20.0
    calc_average.send(None)      # 结束协程
    # 如果此处再调用calc_average.send(10),由于上一协程已经结束,将重开一协程

if __name__ == '__main__':
    main()

还有一点,前面有说道,在asynico库里,基于生成器的协程是async/await的前身,并且是用yield from语句的生成器。基于生成器的协程应该使用 @asyncio.coroutine装饰,虽然并非强制,但使用这样能在一众普通的函数中把协程凸显出来,也有助于调试。

asyncio介绍

asyncio 包使用的 协程 是较严格的定 义。适合 asyncio API 的协程在定义体中必须使用 yield from , 而不能使用 yield 。在基于生成器的协程yield from也已经不推荐使用了,推荐用async/await。关于asyncio的中文文档,链接

推荐高级API使用方式,直接抄两个例子:

例子1:

import asyncio
import time

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

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

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

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

asyncio.run(main())
started at 17:13:52
hello
world
finished at 17:13:55

例子2:

async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    task2 = asyncio.create_task(
        say_after(2, 'world'))

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

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")
started at 17:14:32
hello
world
finished at 17:14:34

注意,预期的输出显示代码段的运行时间比之前快了 1 秒,在asyncio里面任务被用来“并行的”调度协程,也就是说当一个协程封装成任务时候,在遇到IO等待时候,会调度其他的任务,达到并行目的。

aiohttp介绍

aiohttp是基于asyncio的,所以要用aiohttp得先按照asyncio。摘抄一段官网的简单例子:

import aiohttp
import asyncio

async def main():
    async with aiohttp.ClientSession() as session:
        async with session.get('http://httpbin.org/get') as resp:
            print(resp.status)
            print(await resp.text())


loop = asyncio.get_event_loop()
loop.run_until_complete(main())

未完待续

参考文献:

https://www.cnblogs.com/wongbingming/p/9085268.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
爬虫(Web Crawler)是一种自动化程序,用于从互联网上收集信息。其主要功能是访问网页、提取数据并存储,以便后续分析或展示。爬虫通常由搜索引擎、数据挖掘工具、监测系统等应用于网络数据抓取的场景。 爬虫的工作流程包括以下几个关键步骤: URL收集: 爬虫从一个或多个初始URL开始,递归或迭代地发现新的URL,构建一个URL队列。这些URL可以通过链接分析、站点地图、搜索引擎等方式获取。 请求网页: 爬虫使用HTTP或其他协议向目标URL发起请求,获取网页的HTML内容。这通常通过HTTP请求库实现,如Python中的Requests库。 解析内容: 爬虫对获取的HTML进行解析,提取有用的信息。常用的解析工具有正则表达式、XPath、Beautiful Soup等。这些工具帮助爬虫定位和提取目标数据,如文本、图片、链接等。 数据存储: 爬虫将提取的数据存储到数据库、文件或其他存储介质中,以备后续分析或展示。常用的存储形式包括关系型数据库、NoSQL数据库、JSON文件等。 遵守规则: 为避免对网站造成过大负担或触发反爬虫机制,爬虫需要遵守网站的robots.txt协议,限制访问频率和深度,并模拟人类访问行为,如设置User-Agent。 反爬虫应对: 由于爬虫的存在,一些网站采取了反爬虫措施,如验证码、IP封锁等。爬虫工程师需要设计相应的策略来应对这些挑战。 爬虫在各个领域都有广泛的应用,包括搜索引擎索引、数据挖掘、价格监测、新闻聚合等。然而,使用爬虫需要遵守法律和伦理规范,尊重网站的使用政策,并确保对被访问网站的服务器负责。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喝醉的鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值