aiohttp基本使用及并发控制

9,051 阅读3分钟

安装

# Python 版本大于 3.5.3
pip install asyncio  -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install aiohttp  -i https://pypi.tuna.tsinghua.edu.cn/simple

使用aiohttp作为服务端

之前对aiohttp的印象只停留在异步爬虫,直到最近看了它的文档,才发现它也可以作为服务端使用。

aiohttp版本:3.6.2

import time
from aiohttp import web


# 处理query请求
async def handle_query(request):
    time.sleep(3)
    name = request.match_info.get('name', "Anonymous")
    text = "Hello, " + name
    return web.Response(text=text)


# 处理json请求(接收和返回都使用json)
async def handle_json(request):
    json_data = await request.json()
    print('data', json_data)
    return web.json_response(data=json_data)


app = web.Application()
app.add_routes([web.get('/', handle_query),
                web.get('/{name}', handle_query),
                web.post('/json', handle_json),
                ])

if __name__ == '__main__':
    web.run_app(app, port=5000)

使用aiohttp作为客户端

大部分人使用aiohttp,应该是将其作为客户端使用,常见的有异步爬虫、异步代理池(本质也是爬虫)等。下面将会讲述aiohttp的基本使用以及如何限制并发数量

请求单个URL

async def fetch(session, url):
    # with语句保证在处理session的时候,总是能正确的关闭它
    async with session.get(url) as resp:
        # 1.如果想要得到结果,则必须使用await关键字等待请求结束,如果没有await关键字,得到的是一个生成器
        # 2.text()返回的是字符串的文本,read()返回的是二进制的文本
        data = await resp.text()
        print('data', data)
        return data


async def run():
    async with aiohttp.ClientSession() as session:
        html_data = await fetch(session, "http://127.0.0.1:5000/")
        print('html', html_data)


if __name__ == '__main__':
    # 创建一个事件循环
    loop = asyncio.get_event_loop()
    # run函数是一个异步函数,同时也是一个future对象,run_until_complete会等待future对象结束后才退出
    loop.run_until_complete(run())

请求多个URL(会重复创建session)

关于重复创建session的问题,官方文档提到,不建议每一个请求都创建一个session,建议同一个网站使用同一个session,当然还要看每个人的实际情况。

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

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = []  # I'm using test server localhost, but you can use any url
    url = "http://127.0.0.1:5000/"
    for i in range(10):
        task = asyncio.ensure_future(fetch(url))
        tasks.append(task)
    # 得到返回结果
    results = loop.run_until_complete(asyncio.wait(tasks))
    for r in results[0]:
        print('r', r.result())

请求多个URL(不会重复创建session)

此处代码摘抄至:stackoverflow.com/questions/3…

async def fetch(session, url):
    async with session.get(url) as resp:
        if resp.status != 200:
            resp.raise_for_status()
        data = await resp.text()
        return data


async def fetch_multi(session, urls):
    tasks = []
    for url in urls:
        task = asyncio.create_task(fetch(session, url))
        tasks.append(task)
    # gather: 搜集所有future对象,并等待返回
    results = await asyncio.gather(*tasks)
    return results


async def main():
    urls = ["http://127.0.0.1:5000/" for _ in range(10)]
    async with aiohttp.ClientSession() as session:
        datas = await fetch_multi(session, urls)
        print(datas)


if __name__ == '__main__':
    asyncio.run(main())

请求多个URL(错误做法)

其实如果你尝试运行这段代码,你会发现也执行了10次请求,那为什么说是错误做法呢?其实错不错误取决于你有没有加return语句,如果加上了,只会执行一次。

async def fetch():
    async with aiohttp.ClientSession() as session:
        for i in range(1, 10):
            url = "http://127.0.0.1:5000/"
            async with session.get(url) as resp:
                data = await resp.text()
                print('data', data)
                # 加上return语句只会执行一次
                # return data


if __name__ == '__main__':
    # 创建一个事件循环
    loop = asyncio.get_event_loop()
    loop.run_until_complete(fetch())

控制aiohttp并发数量

使用TCPConnector控制并发

这里请求的服务端所使用的代码可以参见上文。

为了更好地理解,建议调整 TCPConnector 的数量,比如一次是3,另一次是10,比较两次打印的时间是怎样的,应该能够更好地理解

async def fetch(session, url):
    async with session.get(url) as resp:
        if resp.status != 200:
            resp.raise_for_status()
        data = await resp.text()
        print('data', data + " " + time.ctime())
        return data


async def fetch_multi(session, urls):
    tasks = []
    for url in urls:
        task = asyncio.create_task(fetch(session, url))
        tasks.append(task)
    # gather: 搜集所有future对象,并等待返回
    results = await asyncio.gather(*tasks)
    return results


async def main():
    urls = ["http://127.0.0.1:5000/" for _ in range(10)]
    conn = aiohttp.TCPConnector(limit=3)
    async with aiohttp.ClientSession(connector=conn) as session:
        datas = await fetch_multi(session, urls)
        print(datas)


if __name__ == '__main__':
    asyncio.run(main())

使用Semaphore控制并发

Semaphore可以理解为一个线程计数器,它内部会维护一个值,比如下文的2。它的核心是 acquire() 和 release() 方法。

当执行 acquire() 方法时,会先判断内部维护的值是否小于0,如果大于0,获得锁,内部的值减一;如果小于0,阻塞,直到执行 release() 方法释放锁,内部值加一。

详情可以查看这篇文章:www.cnblogs.com/renpingshen…

async def fetch(url, semaphore):
    async with semaphore:
        async with aiohttp.ClientSession() as session:
            async with session.get(url) as response:
                data = await response.text()
                print('data', data + " " + time.ctime())
                return data


async def run():
    url = 'http://127.0.0.1:5000'
    semaphore = asyncio.Semaphore(2)  # 限制并发量为2
    to_get = [fetch(url, semaphore) for _ in range(10)]
    await asyncio.wait(to_get)


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(run())
    loop.close()

参考

Python-aiohttp百万并发
python---aiohttp的使用