第十三课 python进阶-asyncio并发编程

第十三课 python进阶-asyncio并发编程

tags:

  • Docker
  • 慕课网

categories:

  • asyncio异步库
  • aiohttp库
  • aiomysql库

文章目录

  • 第十三课 python进阶-asyncio并发编程
    • 第一节 asyncio异步库介绍
      • 1.1 asyncio的一些常识
      • 1.2 asyncio获取返回值和回调
      • 1.3 asyncio的wait和gather
      • 1.4 asyncio的run_forever和run_until_complete
      • 1.5 协程中的嵌套协程理解
      • 1.6 call_soon、call_later、call_at、call_soon_threadsafe
    • 第二节 asyncio异步库使用
      • 2.1 协程中集成阻塞io-线程池(多线程)
      • 2.2 asyncio模拟http协议
      • 2.3 asycio中的类futures和tasks介绍
      • 2.4 asycio中的同步和通信
    • 第三节 aiohttp实现高并发爬虫实战
      • 3.1 aiohttp库介绍
      • 3.2 aiomysql库介绍
      • 3.3 爬虫实践
      • 3.4 异步库的总结

第一节 asyncio异步库介绍

第十三课 python进阶-asyncio并发编程_第1张图片

1.1 asyncio的一些常识

  1. 上一章节中知道协程编码模式脱离不了这三个要素。事件循环 + 回调(驱动生成器)+ epoll(IO多路复用)
  2. asyncio是python用于解决异步io编程的一整套解决方案。tornado、gevent、twisted (scrapy、 django channels)这些框架都封装了它。
  3. torando也是通过协程加事件循环方式完成了我们的高并发,但是它实现web服务器。可以直接部署,但是实际上我们还是会搭配nginx, 因为有很多其他功能: IP限制、Log服务、静态文件代理。它的驱动不能使用传统阻塞IO的驱动,如数据库驱动。
  4. django+flask传统的阻塞IO的编程模型,是一个web系统开发框架本身不提供web服务器(本身不会完成socket编码)。真正部署时会搭配第三方实现socket接口的框架如:uwsgi, gunicorn+nginx
  5. 我们称asyncio为异步IO库而不是协程库,因为不仅仅是协程,它整合了多线程多进程和协程,解决了异步IO中所有问题。
  6. asyncio包含的功能如下:
  • 包含各种特定系统实现的模块化事件循环
  • 传输和协议抽象
  • 对TCP、UDP、SSL、 子进程、延时调用以及其他的具体支持
  • 模仿futures模块但适用于事件循环使用的Future类
  • 基于yield from的协议和任务,可以让你用顺序的方式编写并发代码
  • 必须使用一个将产生阻塞IO的调用时,有接口可以把这个事件转移到线程池
  • 模仿threading模块中的同步原语、 可以用在单线程内的协程之间
  1. asyncio基本使用
import asyncio
import time


async def get_html(url):
    print("start get url")
    # 注意: 同步阻塞的接口不能使用在async中,虽然不会报错但是影响效率相当于顺序执行啦
    # 可以使用asyncio给我们提供的sleep
    # time.sleep(2)
    # 注意要加await 让它完成
    await asyncio.sleep(2)
    print("end get url")

if __name__ == "__main__":
    start_time = time.time()
    # asyncio给我们实现了时间循环 畅快
    loop = asyncio.get_event_loop()
    # loop.run_until_complete(get_html("http://www.imooc.com"))
    tasks = [get_html("http://www.imooc.com") for i in range(10)]
    # asyncio.wait()可以接收一个可迭代对象
    loop.run_until_complete(asyncio.wait(tasks))
    print(time.time()-start_time)

1.2 asyncio获取返回值和回调

  1. 获取返回值:asyncio.ensure_future()或者loop.create_task()
  2. 回调add_done_callback()
    • 默认会把task传给回调函数
    • 如果需要传参数给回调函数可以用偏函数:from functools import partial
      • 它可以把函数包装成为另外一个函数
      • 通过partial传的参数必须放到前面才行
import asyncio
import time
from functools import partial

async def get_html(url):
    print("start get url")
    # 注意: 同步阻塞的接口不能使用在async中,虽然不会报错但是影响效率相当于顺序执行啦
    # 可以使用asyncio给我们提供的sleep
    # time.sleep(2)
    # 注意要加await 让它完成
    await asyncio.sleep(2)
    print("end get url")
    return "bobby"


# 通过partial传的参数必须放到前面才行
def callback(url, future):
    print(url)
    print("send email to bobby")


if __name__ == "__main__":
    start_time = time.time()
    # asyncio给我们实现了时间循环 畅快
    loop = asyncio.get_event_loop()
    # 获取返回值asyncio.ensure_future()或者loop.create_task()
    # task是future类型的子类
    # get_future = asyncio.ensure_future(get_html("http://www.imooc.com"))
    task = loop.create_task(get_html("http://www.imooc.com"))
    # 调用回调函数 这里把task传给了callback
    task.add_done_callback(partial(callback, "http://www.imooc.com"))
    # loop.run_until_complete(get_future)
    loop.run_until_complete(task)
    print(task.result())
    print(time.time()-start_time)

1.3 asyncio的wait和gather

  1. gather和wait的区别。
    • gather更加high-level(高级)更加灵活,可以进行分组
    • 可以批量取消分组,编码时候可以优先考虑gather
  2. group2.cancel()用于请求取消Task对象,这将会再下一轮事件循环中抛出CanceledError;
  3. 如果不想因为抛出异常而中断运行,可以在gather中设置return_exception=True
# wait 和 gather
import asyncio
import time


async def get_html(url):
    print("start get url")
    await asyncio.sleep(2)
    print("end get url")

if __name__ == "__main__":
    start_time = time.time()
    loop = asyncio.get_event_loop()
    # tasks = [get_html("http://www.imooc.com") for i in range(10)]
    # asyncio.wait看源码是个协程 run_until_complete接收一个协程
    # loop.run_until_complete(asyncio.wait(tasks))
    # gather参数加个*, 把tasks解析成参数
    # loop.run_until_complete(asyncio.gather(*tasks))
    # print(time.time()-start_time)

    # gather和wait的区别
    # gather更加high-level, 可以进行分组
    group1 = [get_html("http://projectsedu.com") for i in range(2)]
    group2 = [get_html("http://www.imooc.com") for i in range(2)]
    # loop.run_until_complete(asyncio.gather(*group1, *group2))

    # 也可以分组管理
    group1 = asyncio.gather(*group1)
    group2 = asyncio.gather(*group2)
    # 用于请求取消Task对象,这将会再下一轮事件循环中抛出CanceledError;
    group2.cancel()

    # 如果不想因为抛出异常而中断运行,可以在`gather`中设置`return_exception=True`
    # 如果设置`return_exception=True`,这样异常会和成功的结果一样处理,并聚合至结果列表
    loop.run_until_complete(asyncio.gather(group1, group2, return_exceptions=True))
    print(time.time() - start_time)

1.4 asyncio的run_forever和run_until_complete

  1. run_forever和run_until_complete对比
    • run_forever整个loop一直循环不会停止
      • 如果想停止:future.add_done_callback(_run_until_complete_cb)
      • loop会被放到future中
    • run_until_complete运行完之后停止
      • 查看源码路径:C:\Python36\Lib\asyncio\base_events.py
      • def run_until_complete中有回调中有fut._loop.stop()停止掉。
import asyncio
loop = asyncio.get_event_loop()
loop.run_forever()
loop.run_until_complete()
  1. 实现需求:Ctrl+C取消任务的运行。
import asyncio
import time


async def get_html(sleep_times):
    print("waiting")
    await asyncio.sleep(sleep_times)
    print("done after {}s".format(sleep_times))


if __name__ == "__main__":
    task1 = get_html(2)
    task2 = get_html(3)
    task3 = get_html(3)

    tasks = [task1, task2, task3]
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(asyncio.wait(tasks))
        # 捕捉Ctrl+C命令
    except KeyboardInterrupt as e:
    	# 获取所有任务
        all_tasks = asyncio.Task.all_tasks()
        # 任务取消
        for task in all_tasks:
            print("cancel task")
            print(task.cancel())
        loop.stop()
        # stop后必须要再次run_forever, 否则报异常
        loop.run_forever()
    finally:
        loop.close()

1.5 协程中的嵌套协程理解

  1. 官方文档: https://docs.python.org/3.5/library/asyncio-task.html#example-chain-coroutines
  2. 有时需图可以帮我们理解。
    第十三课 python进阶-asyncio并发编程_第2张图片
import asyncio

async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(1.0)
    return x + y

async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result))

loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()

1.6 call_soon、call_later、call_at、call_soon_threadsafe

  1. loop.call_soon即刻执行。在队列中等待到下一个循环时它会立马执行。
  2. loop.call_later(2, callback, 2)指定时间去运行,2秒后callback参数2
  3. loop.call_at 指定时间。这里的时间是loop的单调时间,不是上面的时间
  4. call_soon_threadsafe 和call_soon一样,但是多了个self._write_to_self( )解决线程安全的问题
import asyncio

# def callback(sleep_times):
#    print("success time {}".format(sleep_times))


def callback(sleep_times, loop):
    print("success time {}".format(loop.time()))


def stoploop(loop):
    loop.stop()


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    now = loop.time()
    # loop.call_soon(callback, 2) # 参数两秒
    # 指定时间去运行
    # loop.call_later(4, callback, 4)  # 四秒中之后执行第一个callback
    # loop.call_later(2, callback, 2)  # 两秒中之后执行第一个callback
    # loop.call_later(3, callback, 3)  # 三秒中之后执行第一个callback
    # call_at 指定loop的单调时间
    loop.call_at(now+2, callback, 2, loop)
    loop.call_at(now+1, callback, 1, loop)
    loop.call_at(now+3, callback, 3, loop)
    # 适当时停止我们的forever循环
    # loop.call_soon(stoploop, loop)
    # loop.call_soon(callback, 4, loop)
    # 需要用forever因为callback不是一个协程
    loop.run_forever()

第二节 asyncio异步库使用

2.1 协程中集成阻塞io-线程池(多线程)

  1. 上面我们说过,协程中不能使用阻塞的方法和库。比如:数据库sqlachemy就是同步的。因为会严重影响到运行的效率。
  2. 那如果我们需要用到怎么解决呢? 使用线程池, 把阻塞部分放到线程池中运行,这样它就不会影响到我们协程的运行
    • from concurrent.futures import ThreadPoolExecutor
    • executor = ThreadPoolExecutor(3)
    • loop.run_in_executor(executor, get_url, url)
  3. 如果对实现感兴趣:C:\Python36\Lib\asyncio\base_events.py 可以看下源码run_in_executor
    • 对线程的future进行包装,包装成协程的future
    • futures.wrap_future(executor.submit(func, *args), loop=self)
# 使用多线程:在协程中集成阻塞io
# asyncio 既可以完成协程 也可以完成多线程和多进程 它是一套异步解决方案
import asyncio
from concurrent.futures import ThreadPoolExecutor
import socket
from urllib.parse import urlparse


# 阻塞的接口
def get_url(url):
    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"

    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((host, 80)) # 阻塞不会消耗cpu

    # 不停的询问连接是否建立好, 需要while循环不停的去检查状态
    # 做计算任务或者再次发起其他的连接请求

    client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))

    data = b""
    while True:
        d = client.recv(1024)
        if d:
            data += d
        else:
            break

    data = data.decode("utf8")
    html_data = data.split("\r\n\r\n")[1]
    print(html_data)
    client.close()


if __name__ == "__main__":
    import time
    start_time = time.time()
    loop = asyncio.get_event_loop()
    # 为了测试性能 把线程池设置为3 操作阻塞IO它肯定避免不了线程池的弊端
    executor = ThreadPoolExecutor(3)
    tasks = []
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        # 把阻塞IO的函数放到run_in_executor 线程池executor去运行
        # get_url 阻塞函数
        task = loop.run_in_executor(executor, get_url, url)
        tasks.append(task)
    loop.run_until_complete(asyncio.wait(tasks))
    print("last time:{}".format(time.time()-start_time))

2.2 asyncio模拟http协议

  1. asyncio没有提供http协议的接口,它提供的是更底层的TCP和UDP协议。
  2. aiohttp库专门用来做http服务的。搭建http服务器或者爬虫的异步requests
  3. 协程方法asyncio.open_connection()可以让我们跟我们的服务端建立一个连接。看下源码理解。
  4. 可以把之前读数据的过程 直接异步化async for raw_line in reader
import asyncio
import socket
from urllib.parse import urlparse


# 这里改成协程
async def get_url(url):
    # 通过socket请求html
    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"

    # 协程方法 建立socket连接 这里调用完成后会继续这个过程
    reader, writer = await asyncio.open_connection(host, 80)
    writer.write("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
    all_lines = []
    # 可以把之前读数据的过程 直接异步化
    async for raw_line in reader:
        data = raw_line.decode("utf8")
        all_lines.append(data)
    html = "\n".join(all_lines)
    return html


async def main():
    tasks = []
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        tasks.append(asyncio.ensure_future(get_url(url)))
    for task in asyncio.as_completed(tasks):
        result = await task
        print(result)

if __name__ == "__main__":
    import time
    start_time = time.time()
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    print('last time:{}'.format(time.time()-start_time))

2.3 asycio中的类futures和tasks介绍

  1. 这里的futures几乎和线程池的futures是一致的。可以看成一个结果容器,运行完之后调用我们的callback.
    • C:\Python36\Lib\asyncio\futures.py
    • 函数set_result() ->_schedule_callbacks -> self._loop.call_soon(callback, self)
    • 将callback放到loop的Ready队列里面去
  2. tasks线程池中没有。它是我们协程和futures的一个桥梁。
    • 第一件事就是激活我们的协程self._loop.call_soon(self._step)
    • 第二件事将最后StopIteration异常的结果做了处理

2.4 asycio中的同步和通信

  1. parse_stuff(), use_stuff()都异步调用了get_staff函数
  2. 而且这两个请求可以非常耗时
  3. 可能被后台反扒抓到
  4. 我们想加一把锁,当parse_stuff()执行get_staff获得锁,只有锁释放后use_stuff才能使用它
  5. 虽然协程没有锁的概念,但是还是要用到同步的机制。把get_staff代码保护起来
  6. from asyncio import Lock
    • 这个库中的Lock的acquire函数是@coroutine协程的 await Lock.acquire()
    • 因为实现__enter__和__exit__魔法函数 所以可以用with with await lock:
    • 它实现了魔法函数__await__和__aenter__所以更简单的写法 async with lock:
    • 看源码:这个acquire只是用户级别的(非常简单),不会对系统的所产生任何其他影响
      • 源码的理解跟上面的yield from理解紧密相关。所以一定要把上面吃透
  7. 通信用:from asyncio import Queue千万别用多线程的Queue因为会阻塞的(队列满和空)。
    • 这里的Queue的put和get方法的调用都要加await
    • Queue的使用场景:为什么不用列表呢,因为Queue可以用来限流。它是有个长度的
import asyncio
import aiohttp
from asyncio import Lock, Queue
cache = {}
lock = Lock()
# 为什么不用列表呢,因为Queue可以用来限流。它是有个长度的
# queue = Queue()
# await queue.get()


# 把函数变成协程的函数
async def get_staff(url):
    # 这个库中的Lock的acquire函数是@coroutine协程的 需要await
    # await Lock.acquire()
    # 因为实现__enter__和__exit__魔法函数 所以可以用with
    # with await lock:
    # 它实现了魔法函数__await__和__aenter__所以更简单的写法如下
    async with lock:
        for url in cache:
            return cache[url]
        # aiohttp 异步库
        stuff = await aiohttp.request('GET', url)
        cache[url] = stuff
        return stuff
        # lock.release()


async def parse_stuff(url):
    stuff = await get_staff(url)
    # 解析


async def use_stuff(url):
    stuff = await get_staff(url)
    # 使用


if __name__ == "__main__":
    # parse_stuff(), use_stuff()都异步调用了get_staff函数
    # 1. 而且这两个请求可以非常耗时 2. 可能被后台反扒抓到
    # 我们想加一把锁,当parse_stuff()执行get_staff获得锁,只有锁释放后use_stuff才能使用它
    # 虽然协程没有锁的概念,但是还是要用到同步的机制。把get_staff代码保护起来
    # from asyncio import Lock
    tasks = [parse_stuff(), use_stuff()]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))

第三节 aiohttp实现高并发爬虫实战

3.1 aiohttp库介绍

  1. 基于asyncio实现的http: https://github.com/aio-libs/aiohttp
  2. 既可以实现客户端也可以实现服务器端。相当于一个支持异步IO的Django
  3. 官方文档:https://docs.aiohttp.org/en/stable/
  4. 搜索 pypi aiohttp 可以看到支持哪个版本的python
  5. 高并发web服务器sanic可以和go语言媲美。

3.2 aiomysql库介绍

  1. 基于asyncio实现的mysql:https://github.com/aio-libs/aiomysql
  2. 三种模式:
    • Basic Example 基本模式
    • Connect Pool 模式(这里我们用这个)
    • 可以集成SqlAchemy

3.3 爬虫实践

  1. 爬取博客网站,因为伯乐在线已经挂了。所以选择其他网站也是一样的。这里选择CSDN技术开发交流网站https://blog.csdn.net 进行爬取。
  2. pip install aiohttp -i https://pypi.douban.com/simple
  3. pip install aiomysql -i https://pypi.douban.com/simple
  4. pip install pyquery -i https://pypi.douban.com/simple
  5. 如果安装包失败,通过这个网址解决:https://www.lfd.uci.edu/~gohlke/pythonlibs/
# asyncio爬虫、去重、入库
import asyncio
import re
import aiohttp
import aiomysql
from pyquery import PyQuery

start_url = "https://me.csdn.net/qq_35190492"
# 声明两个队列
# asyncio中的Queue和列表都可以通信
waitting_url = []
# 暂时用set()集合来去重,但是如果上亿条数据它肯定不行 内存消耗太大。可以用布隆过滤器了解一下
seen_urls = set()
stopping = False

# 控制它的并发
sem = asyncio.Semaphore(3)


# 定义一个协程用来请求URL
async def fetch(url, session):
    # 没有必要反复建立连接, 并发比较高没有必要每个都建立连接, 把它放到consumer中
    # async with aiohttp.ClientSession() as session:
    # 控制并发
    async with sem:
        # 模拟一秒钟请求一个
        await asyncio.sleep(1)
        try:
            async with session.get(url) as resp:
                print("Url Status: {}".format(resp.status))
                print(resp.status)
                if resp.status in [200, 201]:
                    print(await resp.text())
                    data = await resp.text()
                    return data
        except Exception as e:
            print(e)


# 解析不是IO耗时操作, 是CPU操作 不需要协程
def extract_url(html):
    urls = []
    pq = PyQuery(html)
    for link in pq.items("a"):
        url = link.attr("href")
        # url存在, 去掉js链接, 没有被爬取过(过滤url)
        if url and re.match("https://blog.csdn.net/qq_35190492/article/details/\d+$", url) and url not in urls:
            print(url)
            if url not in seen_urls:
                urls.append(url)
                waitting_url.append(url)
    return urls


async def init_urls(url, session):
    html = await fetch(url, session)
    seen_urls.add(url)
    extract_url(html)


async def article_handle(url, session, pool):
    # 获取文章详情 并解析入库
    html = await fetch(url, session)
    seen_urls.add(url)
    # extract_url(html) 获取详情页的url
    pq = PyQuery(html)
    title = pq('.title-article').text()
    print(title)
    # 存入数据库
    async with pool.acquire() as conn:
        async with conn.cursor() as cur:
            insert_sql = "insert into article_test(title) values('{}')".format(title)
            await cur.execute(insert_sql)


# 定义一个协程 抓取具体内容
async def consumer(pool):
    async with aiohttp.ClientSession() as session:
        while not stopping:
            if len(waitting_url) == 0:
                await asyncio.sleep(0.5)
                continue
            url = waitting_url.pop()
            print("start get url:{}".format(url))
            if re.match("https://blog.csdn.net/qq_35190492/article/details/\d+$", url):
                if url not in seen_urls:
                    asyncio.ensure_future(article_handle(url, session, pool))
                    # 防止发起过多请求
                    # asyncio.sleep(30)
            # 调试屏蔽掉 防止请求频繁
            # else:
            #     if url not in seen_urls:
            #         asyncio.ensure_future(init_urls(url, session))


async def main(loop):
    # 等待mysql连接建立完成
    # 两个坑 charset="utf8", autocommit=True
    # charset="utf8"不设置遇到中文不报错,无数据。
    # autocommit=True 不设置不报错,同样无数据
    pool = await aiomysql.create_pool(host='127.0.0.1', port=3306,
                                  user='root', password='123456', db='aiomysql_test',
                                  loop=loop, charset="utf8", autocommit=True)
    # 这样逻辑上也没错, 刚开始不爬取,其他爬虫的协程运行不了
    # html = await fetch(start_url)
    # extract_url(html)
    # 把协程扔到时间循环中去
    # asyncio.ensure_future(init_urls(start_url))
    # 把数据库连接池传过去 全局global 也是可以的
    # asyncio.ensure_future(consumer(pool))
    async with aiohttp.ClientSession() as session:
        html = await fetch(start_url, session)
        extract_url(html)
    asyncio.ensure_future(consumer(pool))


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    asyncio.ensure_future(main(loop))
    loop.run_forever()

3.4 异步库的总结

  1. asyncio使我们python现在和将来重点打造的部分。让python可以想go和node.js一样完成高并发的基础
  2. python并发的解决方案
    • Tonardo
    • Twisted
    • gevent
    • asyncio
  3. Tonardo和Gevent采用了我们的python中的生成器。
  4. Gevent底层通过C语言实现协程来完成。他有个最大的问题,采用了猴子补丁,将很多内置的库进行补丁。很难捕捉到内部的异常。
  5. asyncio是python开发者重点打造的模块。所以很重要。

你可能感兴趣的:(python的语言总结学习)