Python中requests库在大量请求时如何优化性能?
网上有 grequests 库 但是有限制 。只能把所有的 url 拉出来一起请求、 我们的项目里要请求各种 url 并且是在不同代码块儿。 做不到拉出来一块儿请求 请问各位 python 网络编程的大佬 能不能支支招。
梦想中的场景 -- > 像 requests 一样的简单 但是是异步的。(就开开玩笑)
Python中requests库在大量请求时如何优化性能?
本地单独起个 tornado 服务
每次都请求 localhost:10000/fetch/{ip}/,代码里用 AsyncHttpClient
对于大量请求,用requests库直接搞确实会慢,主要是卡在同步I/O上。最直接的优化思路就是别让它干等着,上异步或者并发。
核心方案:用 requests + concurrent.futures 搞线程池并发。 这招对I/O密集型任务(比如网络请求)特管用,因为线程在等待响应时,CPU可以切去处理其他线程的任务。
下面是个完整例子,对比一下单线程和用线程池的差距,你就明白为啥要这么干了:
import requests
import concurrent.futures
import time
# 假设我们要请求的URL列表
urls = [
'https://httpbin.org/delay/1', # 这个端点会故意延迟1秒响应
'https://httpbin.org/delay/1',
'https://httpbin.org/delay/1',
'https://httpbin.org/delay/1',
'https://httpbin.org/delay/1',
] * 10 # 重复10次,模拟50个请求
def fetch_url(url):
"""单个请求任务"""
try:
resp = requests.get(url, timeout=5)
return resp.status_code
except Exception as e:
return str(e)
# 1. 传统单线程方式 (基线对比)
def sequential_requests():
start = time.time()
results = []
for url in urls:
results.append(fetch_url(url))
end = time.time()
print(f"顺序执行耗时: {end - start:.2f} 秒")
return results
# 2. 使用ThreadPoolExecutor并发执行
def concurrent_requests(max_workers=10):
start = time.time()
results = []
# 创建线程池,max_workers控制并发数
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
# 提交所有任务到线程池,map会保持返回结果的顺序
future_to_url = {executor.submit(fetch_url, url): url for url in urls}
for future in concurrent.futures.as_completed(future_to_url):
try:
result = future.result()
results.append(result)
except Exception as e:
results.append(str(e))
end = time.time()
print(f"线程池并发 (workers={max_workers}) 耗时: {end - start:.2f} 秒")
return results
if __name__ == '__main__':
print("开始测试...")
print(f"总请求数: {len(urls)}")
print("-" * 40)
# 跑一下单线程的
seq_results = sequential_requests()
# 跑一下线程池并发的,你可以调整max_workers试试效果
conc_results = concurrent_requests(max_workers=20)
print("-" * 40)
print("测试完成。")
简单解释一下:
ThreadPoolExecutor搞了一堆工人(线程),默认数量是CPU核心数*5,但I/O任务你可以大胆多开点,比如设成50或100,别太过分把人家服务器搞崩就行。executor.submit把任务(fetch_url函数和URL参数)丢进池子排队,立刻返回一个Future对象(可以理解为“欠条”)。as_completed哪个任务先完成就先处理哪个,不卡顺序。
跑一下你就会发现,50个每个延迟1秒的请求,单线程要50多秒,而用20个线程的池子可能就3秒左右搞定,这就是并发的威力。
如果还想更猛:
- 对于超级大量的请求(比如上万),可以考虑
aiohttp+asyncio的纯异步方案,理论上比线程池开销更小、效率更高。 - 记得配个 会话 (
requests.Session) 并设置合理的 连接池适配器 (HTTPAdapter),比如调大pool_connections和pool_maxsize,这样复用TCP连接能省掉大量握手开销。 - 该加 超时 (
timeout) 和 重试机制 的地方也得加上,生产环境别裸奔。
总结:换并发或异步,别让程序傻等。
gevent + httplib2
直接 gevent spawn 然後 joinall 吧, 要不然直接 python3.6
gevent celery 都可以解决
在异步的前提下用 aiorequests
要不试试我的库 https://github.com/maliubiao/simple_http
还能控制并发量, 超时时间
### 异步方式shell <br><br>In [21]: def print_it(x): <br> import pprint<br> ....: pprint.pprint(x)<br> ....: <br><br>In [22]: async_http.repeat_tasks([{"url": "<a target="_blank" href="http://www.baidu.com" rel="nofollow noopener">http://www.baidu.com</a>", "parser": print_it}])<br>{'chain': None,<br> 'chain_idx': 0,<br> 'con': <socket._socketobject object at 0x2812bb0>,<br> 'fd': 5,<br> 'header_only': False,<br> 'parser': <function print_it at 0x283da28>,<br> 'proxy': '',<br> 'random': '60804c2a0b053fbd',<br> 'recv': <cStringIO.StringO object at 0x283a3e8>,<br> 'redirect': 0,<br> 'res_cookie': {'BAIDUID': {'domain': '.baidu.com',<br> 'expires': 'Thu, 31-Dec-37 23:55:55 GMT',<br> 'max-age': '2147483647',<br> 'path': '/',<br> 'value': 'BCB0BBBB4312D00C88BCDC9EEAAE3726:FG=1'},<br> 'BD_LAST_QID': {'Max-Age': '1',<br> 'path': '/',<br> 'value': '16069052107084303783'},<br> 'BIDUPSID': {'domain': '.baidu.com',<br> 'expires': 'Thu, 31-Dec-37 23:55:55 GMT',<br> 'max-age': '2147483647',<br> 'path': '/',<br> 'value': 'BCB0BBBB4312D00C88BCDC9EEAAE3726'}},<br> 'res_header': {'Connection': 'Keep-Alive',<br> 'Content-Length': '215',<br> 'Content-Type': 'text/html',<br> 'Date': 'Thu, 21 May 2015 15:50:43 GMT',<br> 'Location': '<a target="_blank" href="https://www.baidu.com/'" rel="nofollow noopener">https://www.baidu.com/'</a>,<br> 'P3P': 'CP=" OTI DSP COR IVA OUR IND COM "',<br> 'Server': 'BWS/1.1',<br> 'Set-Cookie': 'BAIDUID=BCB0BBBB4312D00C88BCDC9EEAAE3726:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r\nBIDUPSID=BCB0BBBB4312D00C88BCDC9EEAAE3726; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com\r\nBD_LAST_QID=16069052107084303783; path=/; Max-Age=1',<br> 'X-UA-Compatible': 'IE=Edge,chrome=1'},<br> 'res_status': {'message': 'Moved Temporarily',<br> 'protocol': 'HTTP/1.1',<br> 'status': 302},<br> 'retry': 0,<br> 'send': <cStringIO.StringO object at 0x25fb8f0>,<br> 'ssl': False,<br> 'start': 1432223278.489937,<br> 'status': 512,<br> 'text': '<html>\r\n<head><title>302 Found</title></head>\r\n<body bgcolor="white">\r\n<center><h1>302 Found</h1></center>\r\n<hr><center>pr-nginx_1-0-221_BRANCH Branch\nTime : Wed May 20 10:35:46 CST 2015</center>\r\n</body>\r\n</html>\r\n',<br> 'url': '<a target="_blank" href="http://www.baidu.com" rel="nofollow noopener">http://www.baidu.com</a>'}<br>async_http Thu May 21 23:47:58 2015: 'acnt: 1, fcnt: 0, time: 0'<br>
要么用多线程,要么用协程咯
看看 treq 这个包?
request_future ?
gevent monkey patch 试试?
去状态化+分布式负载均衡
哇 真的是好想法…崭新的思路!
如果后端是 url 是同一个域名的,用 requests 的 session 长连接,性能会快一点点。
要不然就用 gevent 和 celery
实测这个真是聊胜于无,感觉就是每次帮你把 cookie 自动带上的语法糖而已,可能是我使用的方式不对,如果知道具体运作原理还请指教。
你要是开启过 debug 日志的话就知道如果是同一个 session 下会保持 keepalive,不会每次访问一个 url 都重新开一个新的短链接
thanks,我试一下,如果是长链接的话,省去了频繁的握手,按理说效果应该不错啊。
requests 的 session 不仅仅是 cookie 保存。 还有个特性是 keepalive 长连接,不用每次都重新建立 tcp 连接。
少年,请起,站起来吧
requests 的 session 是 keepalive 的,通过传递 session 的方式可以多个请求共享 TCP 连接。
其次,文档就有提到: http://docs.python-requests.org/en/master/user/advanced/#blocking-or-non-blocking
最后,现在问 Python 的问题已经不用带上 Python 的版本了吗???🤔
requests 周边生态是 23 兼容的
本来跪一下就想起来了 但是看到这么多大神的回复以后。 我已经起不来了


