本文主要介绍如何通过使用aiohttp库将同步的http请求改成异步方式请求,从而降低等待网络IO过程中时间和计算资源的浪费。
主要包括如何将常见的requests请求改用aiohttp异步执行以及如何将异步的批量请求方法封装成普通方法/同步调用方式,给业务模块调用。
文章目录
- 场景说明
- 大量数据请求场景
- 用协程解决网络请求密集的任务
- 解决方案
- 使用requests库发送请求
- 异步请求代码的结构
- 批量请求的同步方式和异步方式代码对照
- 代码结构差异
- 导包差异
- send_single_request差异
- send_batch_requests差异
- 外部业务模块调用差异
- 差异说明
- 异步方式返回结果无序的解决方案
- 方式一:按请求顺序排序(请求顺序作为返回数据标识)
- 方式二:自定义返回数据标识,返回一个键值对
- 源代码
场景说明
大量数据请求场景
在爬虫通过定制化接口获取数据或者后端处理大量数据请求时,由于数据量较大,所以如果单纯使用同步方式的requests请求,对单台机器的利用率不高,数据处理的速度较慢,同时如果要提速需要增加机器会导致成本提高。
在这种情况下,使用并发是一种成本比较低的方式。在并发中多进程的并发是用于处理计算逻辑较多计算量较大的计算密集型任务,数据请求属于网络IO等待时间占处理时间的大头,不属于计算密集型,而是属于IO密集型任务。
用协程解决网络请求密集的任务
处理IO密集型任务,使用多线程或协程来实现效率的提升。而由于python默认的cpython解释器有全局解释器锁GIL的限制,使得python的多线程会有效率提高,但是无法完全发挥多线程的优势。
而协程在处理IO密集型任务上相当合适,但是在写法上跟我们习惯的同步方式执行的代码写法有些差别。
解决方案
在python中,将http请求使用异步方式执行,主要是使用aiohttp库来快速实现。
我们接下来将明确把一个同步方式的requests请求有哪几个部分,然后通过将各个部分替换成异步的aiohttp请求,来展示将同步请求改成异步请求的步骤。
使用requests库发送请求
最简单的请求代码如下:
import requests
response = requests.get("http://httpbin.org/get")
当然我们在业务中会需要使用到其他参数来满足我们的业务需求,所以可以考虑请求分为三个部分:
- 构造请求相关的参数/值
- 发送请求(考虑复用与目标服务器的连接,会需要使用session发送请求)
- 处理返回结果
代码结构如下:
"""请求过程分为三个部分:
1. 构造参数
2. 发送请求
3. 处理返回结果
"""
import requests
"""构造参数, 包括但不限于:
- headers
- url
- parameters
- proxy
- post请求的body
- ......
"""
headers = {
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Proxy-Connection': 'keep-alive',
'Referer': 'http://httpbin.org/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
'accept': 'application/json',
}
url = 'http://httpbin.org/get'
"""
# 新建会话,使用会话发送请求
# 也可以直接requests.get()进行请求
"""
session = requests.session()
response = session.get(url, headers=headers, verify=False)
"""处理返回结果
主要是对返回结果的状态,返回值进行条件检查
在满足某些条件时执行对应的处理逻辑
"""
print(response.status_code)
if response.status_code == 200:
print(response.json())
异步请求代码的结构
关于异步请求,我们只需要知道下面几点:
- 同步函数就是用
def
定义的函数 - 异步函数就是用
async def
定义的函数,同时函数体里面会包含await
语句 - 同步请求的逻辑是用循环语句一次接着一次进行"发起请求,等待返回,处理返回结果"的过程
- 异步请求的逻辑是生成不同的请求任务,然后(几乎)同时发出这批请求,然后请求返回的先后顺序,依次处理各个请求的返回结果
- 同步请求是一个
def
函数传入所有要请求的数据,然后依次传给另一个def
函数去执行单个请求的发送和处理逻辑 - 异步请求是一个
async def
函数接收所有要请求的数据,然后将各个数据传入另一个async def
函数来生成请求任务并批量执行这些请求任务。异步需要额外的一个def
函数来启动这个外层的async def
函数
批量请求的同步方式和异步方式代码对照
我们分别展示一下完整的代码结构,然后解释二者的差异。
假设我们批量请求的requests代码结构如下:
"""请求分为三个部分:
1. 构造参数
2. 发送请求
3. 处理返回结果
"""
import requests
def send_single_request(session, parameters: dict):
# 构造参数, 包括但不限于headers, url, parameters, proxy, post请求的body
headers = {
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Proxy-Connection': 'keep-alive',
'Referer': 'http://httpbin.org/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
'accept': 'application/json',
}
url = 'http://httpbin.org/get'
# 新建会话,使用会话发送请求
# (也可以直接requests.get()进行请求)
response = session.get(url, params=parameters, headers=headers)
# 处理返回结果
print(response.status_code)
if response.status_code == 200:
json_response = response.json()
print(json_response)
return json_response["args"]
def send_batch_requests(batch_data):
all_result = []
session = requests.session()
for parameters in batch_data:
request_result = send_single_request(session, parameters)
all_result.append(request_result)
return all_result
# 业务模块在有请求需求时调用接口的业务逻辑
def appliaction_logic_to_call_requests():
batch_data_to_request = [{"num": num} for num in range(1, 11)]
all_result = send_batch_requests(batch_data_to_request)
# process the result
appliaction_logic_to_call_requests()
则改成批量的aiohttp异步请求的方式,代码结构如下:
"""请求分为三个部分:
1. 构造参数
2. 发送请求
3. 处理返回结果
"""
import aiohttp
import asyncio
import json
async def send_single_request(session, parameters: dict):
# 构造参数, 包括但不限于headers, url, parameters, proxy, post请求的body
headers = {
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Proxy-Connection': 'keep-alive',
'Referer': 'http://httpbin.org/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
'accept': 'application/json',
}
url = 'http://httpbin.org/get'
# 新建会话,使用会话发送请求
# (也可以直接requests.get()进行请求)
async with session.get(url, params=parameters, headers=headers) as response:
raw_response = await response.read()
# 处理返回结果
print(response.status)
if response.status == 200:
json_response = json.loads(raw_response)
print(json_response)
return json_response["args"]
async def send_batch_requests(batch_data):
all_result = []
async with aiohttp.ClientSession() as session:
# # 我更喜欢使用列表推导式来生成任务,简单明了
# tasks = [asyncio.create_task(send_single_request(session, parameters)) for parameters in batch_data]
tasks = []
for parameters in batch_data:
single_task = asyncio.create_task(send_single_request(session, parameters))
tasks.append(single_task)
all_result = await asyncio.gather(*tasks)
return all_result
"""异步方式需要增加一个额外的函数给外部调用
这样对外部调用逻辑来说,批量请求是同步还是异步是隐藏的细节(封装)
"""
def logic_to_start_async_tasks(batch_data):
all_result = asyncio.run(send_batch_requests(batch_data))
return all_result
# 业务模块在有请求需求时调用接口的业务逻辑
def appliaction_logic_to_call_requests():
batch_data_to_request = [{"num": num} for num in range(1, 11)]
all_result = logic_to_start_async_tasks(batch_data_to_request)
# process the result
appliaction_logic_to_call_requests()
代码结构差异
用版本管理工具查看两个版本的代码,差异如下:
红色高亮代码为旧版本代码,即同步方式的requests
结构代码
绿色高亮代码为新版本代码,即异步方式的aiohttp
结构代码
导包差异
send_single_request差异
send_batch_requests差异
外部业务模块调用差异
差异说明
我们可以看到异步的的代码结构主要差异如下:
- 具体进行异步请求的函数需要使用
async def
进行定义 async def
的函数体必须包含await
语句:- 批量创建和调度任务的函数
send_batch_requests
将await
用于等待任务的执行和返回:await asyncio.gather(*tasks) - 具体发送请求和处理返回结果的函数
send_single_request
将await
用于等待外部网络服务器返回awiat session.get()
以及读取返回值await response.read()
send_single_request
中也可以对response
调用json()
方法,但是也要使用await
,(例:json_response = await response.json()
),可以自行选择使用哪种用法。
- 批量创建和调度任务的函数
- session创建用的函数不同:
requests
用requests.session()
创建一个可以复用的新会话aiohttp
用aiohttp.ClientSession()
创建一个可以复用的新会话aiohttp
的session不一定要搭配上下文管理器使用,也可以用session = aiohttp.ClientSession()
创建session;使用上下文可以更好避免session没有关闭的问题
- 可能存在一些属性或方法的名称有差异:
requests
中的response
状态码的属性名为status_code
aiohttp
中的response
状态码的属性名为status
- 异步方法需要增加一个同步函数来启动异步创建和执行任务的逻辑
- 为了让外部调用完全没有感知,我们在实际业务中会使用
send_batch_requests
作为同步函数的名称,异步函数可以改为async def create_and_execute_tasks
,这样对于外部来说,在同步和异步两个版本中调用的API名称没有变化,都是调用的send_batch_requests
方法
- 为了让外部调用完全没有感知,我们在实际业务中会使用
只要注意以上这些差异,就可以熟练地在异步和同步方式之间转换。
异步方式返回结果无序的解决方案
当我们使用同步的方式进行请求的时候,由于是完成上一个请求才会执行下一个请求,所以返回结果是有序的,如下:
而当我们使用异步的方式进行请求时,由于请求是直接接连发出的,不会等待上一个请求完成返回并处理后才发出下一个请求,并且请求返回的次序是不固定的(尽管我们是有序发出的请求),所以处理完的结果可能是无序的,如下:
由于我们在使用中需要知道哪个返回结果是对应哪个请求数据的,所以返回结果无序是需要解决的。解决的方法也很简单,异步请求的方法返回一个唯一标志,然后外部管理异步任务的异步方法根据返回的唯一标志进行排序或者其他处理。
方式一:按请求顺序排序(请求顺序作为返回数据标识)
代码变动如下:
这样一来,最后返回的结果就是有序的:
方式二:自定义返回数据标识,返回一个键值对
使用数据请求顺序来说可以让返回结果有序,但从业务实际应用来说,这种方式应用场景较少。
因为实际数据使用都是通过数据内容来定位数据,而不是数据的顺序来定位数据。
举一个具体例子:
我们要请求10个用户的数据,由于对每一个用户可能是走同一套逻辑,所以这种情况可以是批量请求,然后按顺序把"用户ID"和"根据用户ID请求到的用户数据"传入后续的处理逻辑中。
但事实上用户数据的不同信息可能是由不同请求去获取的,比如用户信息包括地址和手机,而地址和手机分别存在不同的数据库表中,或者由不同的API获取,所以我们可以是发出get/user/address
和get/user/telephone
两个请求,然后返回结果是{"user_id": "xxx", "telephone": "xxxx"}
和{"user_id": "xxx", "address": "xxxxxx"}
,这种时候我们是知道要请求哪些信息项的,比如info_items = ["address", "telephone"]
所以可以有两种写法,如下:
"""写法一:主要还是用顺序作为数据标识以及排序依据
然后根据item在info_items的顺序与在结果的顺序一一对应的特点来返回最终结果
"""
info_items = ["address", "telephone"]
tasks = []
for index, item in enumerate(info_items):
API = f"get/user/{item}"
session = aiohttp.ClientSession()
single_task = asyncio.create_task(send_single_request(session, item, identity=index))
tasks.append(single_task)
all_result = await asyncio.gather(*tasks)
all_result = sorted(all_result, key=lambda result: int(result[0]))
all_result = [result[1] for result in all_result]
user_info = {
info_item[index]: all_result[index] for index in range(len(info_items))
}
return user_info
"""方式二: 直接用item作为identity
然后直接用identity和返回结果生成键值对
"""
info_items = ["address", "telephone"]
tasks = []
for item in info_items:
API = f"get/user/{item}"
session = aiohttp.ClientSession()
single_task = asyncio.create_task(send_single_request(session, item, identity=item))
tasks.append(single_task)
all_result = await asyncio.gather(*tasks)
user_info = {
result[0]: result[1] for result in all_result
}
return user_info
个人推荐第二种方式,编写上更符合python的风格,阅读上也更清晰明了
源代码
完整的异步代码结构如下:
"""请求分为三个部分:
1. 构造参数
2. 发送请求
3. 处理返回结果
"""
import aiohttp
import asyncio
import json
async def send_single_request(session, parameters: dict, identity):
# 构造参数, 包括但不限于headers, url, parameters, proxy, post请求的body
headers = {
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Proxy-Connection': 'keep-alive',
'Referer': 'http://httpbin.org/',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36',
'accept': 'application/json',
}
url = 'http://httpbin.org/get'
# 新建会话,使用会话发送请求
# (也可以直接requests.get()进行请求)
async with session.get(url, params=parameters, headers=headers) as response:
raw_response = await response.read()
# 处理返回结果
# print(response.status)
if response.status == 200:
json_response = json.loads(raw_response)
# json_response = await response.json()
# print(json_response)
# return identity, json_response
return identity, {"args": json_response["args"], "url": json_response["url"]}
async def create_and_execute_tasks(batch_data):
all_result = {}
async with aiohttp.ClientSession() as session:
# # 我更喜欢使用列表推导式来生成任务,简单明了
# tasks = [asyncio.create_task(send_single_request(session, parameters)) for parameters in batch_data]
tasks = []
for index, parameters in enumerate(batch_data):
single_task = asyncio.create_task(send_single_request(session, parameters, identity=index))
tasks.append(single_task)
identity_and_data_pairs = await asyncio.gather(*tasks)
all_result = {pair[0]: pair[1] for pair in identity_and_data_pairs}
return all_result
"""异步方式需要增加一个额外的函数给外部调用
这样对外部调用逻辑来说,批量请求是同步还是异步是隐藏的细节(封装)
"""
def send_batch_requests(batch_data):
all_result = asyncio.run(create_and_execute_tasks(batch_data))
return all_result
# 业务模块在有请求需求时调用接口的业务逻辑
def appliaction_logic_to_call_requests():
batch_data_to_request = [{"num": num} for num in range(1, 11)]
all_result = send_batch_requests(batch_data_to_request)
return all_result
# process the result
print("异步方式返回结果")
print(appliaction_logic_to_call_requests())
在实际业务开发中,根据具体的业务需求修改send_single_request
以及create_and_execute_tasks
即可,比如:
send_single_request
中get请求方式改为post请求方式,并且增删使用的参数create_and_execute_tasks
从batch_data单个数据parameters中生成identity
- …
掌握了上面这些,使用python写异步的http请求,就手到擒来了(●ˇ∀ˇ●)
好书推荐:
- 流畅的python
- Python编程 从入门到实践 第2版
- Python数据结构与算法分析 第2版
好课推荐:
- 零基础学python
- python核心技术与实战
- python自动化办公实战
写文不易,如果对你有帮助的话,来一波点赞、收藏、关注吧~👇