余晖落尽暮晚霞,黄昏迟暮远山寻
本站
当前位置:网站首页 > 编程知识 > 正文

在 Python 中用最快的速度发送 HTTP 请求(python发送http请求)

xiyangw 2022-11-24 16:43 41 浏览 0 评论

在 Python 中用最快的速度发送 HTTP 请求

在 Python 中用最快的速度发送 HTTP 请求(python发送http请求)

使用 requests 包可以轻松发送单个 HTTP 请求。但如果我想要异步地发送上百个甚至上百万个 HTTP 请求呢?这篇文章是一篇探索笔记,旨在找到发送多个 HTTP 请求的最快方式。

代码在云端的带有 Python 3.7 的 Linux(Ubuntu) VM 主机中运行。所有都代码存放在 Gist 中,都可以复制和运行。

方法 #1 : 使用同步

这是最简单易懂的方式,但也是最慢的方式。我通过这个神奇的 Python 列表操作符为测试伪造了 100 个链接:

url_list = ["https://www.google.com/","https://www.bing.com"]*50
复制代码

代码:

import requests
import time

def download_link(url:str) -> None:
    result = requests.get(url).content
    print(f'Read {len(result)} from {url}')
def download_all(urls:list) -> None:
    for url in urls:
        download_link(url)
        
url_list = ["https://www.google.com/","https://www.bing.com"]*50
start = time.time()
download_all(url_list)
end = time.time()
print(f'download {len(url_list)} links in {end - start} seconds')
复制代码

它用了十秒钟来完成下载 100 个链接。

...
download 100 links in 9.438416004180908 seconds
复制代码

作为一个同步的解决方案,这个代码仍有改进的空间。我们可以利用 Session 对象来提高速度。Session 对象将使用 urllib3 的连接池,这意味着对于对同一主机的重复请求,将重新使用 Session 对象的底层 TCP 连接,从而获得性能提升。

因此,如果您向同一主机发出多个请求,则底层 TCP 连接将被重用,这可能会导致性能显著提升 —— Session 对象

为了确保请求对象无论成功与否都退出,我将使用 with 语句作为上下文管理器。Python 中的 with 关键词只是替换 try…… finally…… 的一个干净的解决方案。

让我们看看将代码改成这样可以节省多少秒:

import requests
from requests.sessions import Session
import time

url_list = ["https://www.google.com/","https://www.bing.com"]*50

def download_link(url:str,session:Session):
    with session.get(url) as response:
        result = response.content
        print(f'Read {len(result)} from {url}')

def download_all(urls:list):
    with requests.Session() as session:
        for url in urls:
            download_link(url,session=session)

start = time.time()
download_all(url_list)
end = time.time()
print(f'download {len(url_list)} links in {end - start} seconds')
复制代码

看起来性能真的提升到了 5.x 秒。

...
download 100 links in 5.367443561553955 seconds
复制代码

但是这样还是很慢,让我们试试多线程的解决方案。

解决方案#2:多线程方法

Python 线程是一个危险的话题,有时,多线程可能会更慢!戴维·比兹利(David Beazley)带来了一场精彩的、涵盖了这个危险的话题的演讲。YouTube 链接在这里。

无论如何,我仍然会使用 Python 线程来完成 HTTP 请求工作。我将使用一个队列来保存 100 个链接并创建 10 个 HTTP 工作线程来异步下载这 100 个链接。

要使用 Session 对象,为 10 个线程创建 10 个 Session 对象是一种浪费,我只想要创建一个 Session 对象并在所有下载工作中重用它。为了实现这一点,代码将利用 threading 包中的 local 对象,这样 10 个线程工作将共享一个 Session 对象。

from threading import Thread,local
...
thread_local = local()
...
复制代码

代码:

import requests
from requests.sessions import Session
import time
from threading import Thread,local
from queue import Queue

url_list = ["https://www.google.com/","https://www.bing.com"]*50
q = Queue(maxsize=0)            #Use a queue to store all URLs
for url in url_list:
    q.put(url)
thread_local = local()          #The thread_local will hold a Session object

def get_session() -> Session:
    if not hasattr(thread_local,'session'):
        thread_local.session = requests.Session() # Create a new Session if not exists
    return thread_local.session

def download_link() -> None:
    '''download link worker, get URL from queue until no url left in the queue'''
    session = get_session()
    while True:
        url = q.get()
        with session.get(url) as response:
            print(f'Read {len(response.content)} from {url}')
        q.task_done()          # tell the queue, this url downloading work is done

def download_all(urls) -> None:
    '''Start 10 threads, each thread as a wrapper of downloader'''
    thread_num = 10
    for i in range(thread_num):
        t_worker = Thread(target=download_link)
        t_worker.start()
    q.join()                   # main thread wait until all url finished downloading

print("start work")
start = time.time()
download_all(url_list)
end = time.time()
print(f'download {len(url_list)} links in {end - start} seconds')
复制代码

结果:

...
download 100 links in 1.1333789825439453 seconds
复制代码

下载的速度非常快!比同步解决方案快得多。

解决方案 #3:通过 ThreadPoolExecutor 进行多线程

Python 还提供了 ThreadPoolExecutor 来执行多线程工作,我很喜欢 ThreadPoolExecutor。

在 Thread + Queue 的版本中,HTTP 请求工作中有一个 while True 循环,这使得 worker 函数与 Queue 纠缠不清,从同步版本变更到异步版本的代码需要额外的改动。

有了 ThreadPoolExecutor 及其 map 函数,我们可以创建一个代码非常简洁的多线程版本,只需要从同步版本中进行很小的代码更改。

代码:

import requests
from requests.sessions import Session
import time
from concurrent.futures import ThreadPoolExecutor
from threading import Thread,local

url_list = ["https://www.google.com/","https://www.bing.com"]*50
thread_local = local()

def get_session() -> Session:
    if not hasattr(thread_local,'session'):
        thread_local.session = requests.Session()
    return thread_local.session

def download_link(url:str):
    session = get_session()
    with session.get(url) as response:
        print(f'Read {len(response.content)} from {url}')

def download_all(urls:list) -> None:
    with ThreadPoolExecutor(max_workers=10) as executor:
        executor.map(download_link,url_list)

start = time.time()
download_all(url_list)
end = time.time()
print(f'download {len(url_list)} links in {end - start} seconds')
复制代码

最后的输出和线程队列版本一样快:

...
download 100 links in 1.0798051357269287 seconds
复制代码

解决方案 #4:asyncio 与 aiohttp

每个人都说 asyncio 就是未来,而且速度很快。有些人使用它 来用 Python 的 asyncio 和 aiohttp 包发出 100 万次 HTTP 请求。尽管 asyncio 非常快,但它没有使用 Python 多线程。

你敢相信吗,asyncio 只有在一个线程、一个 CPU 核心中运行!

在 asyncio 中实现的事件循环几乎与 Javascript 中使用的相同。

Asyncio 非常快,几乎可以向服务器发送任意数量的请求,唯一的限制是您的设备和互联网带宽。

发送过多的 HTTP 请求会表现得像“攻击”。当检测到太多请求时,某些网站可能会封锁您的 IP 地址,甚至 Google 也会封锁您。为了避免被封锁,我使用了一个自定义 TCP 连接器对象,该对象将最大 TCP 连接数指定为 10。(将其更改为 20 应该也是安全的。)

my_conn = aiohttp.TCPConnector(limit=10)
复制代码

这个代码非常简短:

import asyncio
import time 
import aiohttp
from aiohttp.client import ClientSession

async def download_link(url:str,session:ClientSession):
    async with session.get(url) as response:
        result = await response.text()
        print(f'Read {len(result)} from {url}')

async def download_all(urls:list):
    my_conn = aiohttp.TCPConnector(limit=10)
    async with aiohttp.ClientSession(connector=my_conn) as session:
        tasks = []
        for url in urls:
            task = asyncio.ensure_future(download_link(url=url,session=session))
            tasks.append(task)
        await asyncio.gather(*tasks,return_exceptions=True) # the await must be nest inside of the session

url_list = ["https://www.google.com","https://www.bing.com"]*50
print(url_list)
start = time.time()
asyncio.run(download_all(url_list))
end = time.time()
print(f'download {len(url_list)} links in {end - start} seconds')
复制代码

上面的代码在 0.74 秒内完成了 100 个链接的下载!

...
download 100 links in 0.7412574291229248 seconds
复制代码

请注意,如果您想在 Jupyter Notebook 或 IPython 中运行代码,请安装 nest-asyncio 包。 (感谢这个 StackOverflow 链接。感谢 Diaf Badreddine。)

pip install nest-asyncio
复制代码

并在代码开头添加以下两行代码:

import nest_asyncio
nest_asyncio.apply()
复制代码

解决方案#5:如果是 Node.js 呢?

我想知道,如果我在具有内置事件循环的 Node.js 中做同样的工作会怎样?

这是完整的代码。

const requst = require('request')
//build a 100 links array
url_list = []
url_list.push(...Array(50).fill("https://www.google.com"))
url_list.push(...Array(50).fill("https://www.bing.com"))

const batch_num = 10;     // send 10 Http requesta at one time
let batch_index = batch_num;
let resolvehandler = null;
function download_link(url){
    requst({
        url: url,
        timeout:1000
    },function(error,response,body){
        if (body){
            console.log(body.length)
        }
        batch_index = batch_index -1
        if(batch_index==0){
            batch_index = batch_num
            resolvehandler()
        }
    });
}

function download_batch(url_list){
    for(j = 0;j<batch_num;j++){
        download_link(url_list[j])
    }
}

async function download_all(url_list){
    let loop_count = url_list.length/batch_num;
    console.time('test')
    for(var i =0;i<loop_count;i++){
        await new Promise(function(resolve,reject){
            download_batch(url_list.slice(i,i+10))
            resolvehandler = resolve
        })
    }
    console.timeEnd('test')
}

download_all(url_list)
复制代码

这个代码需要 1.1 到 1.5 秒,您可以运行它在您的设备中查看结果。

...
test: 1195.290ms
复制代码

Python 赢得了这场速度的游戏!

(看起来 Node.js 的 request 包已经被弃用了,但这个示例只是为了测试 Node.js 的事件循环与 Python 的事件循环相比如何执行。)


如果您有更好/更快的解决方案,请告诉我。如果您有任何问题,请发表评论,我会尽力回答,如果您发现错误,请不要犹豫,将它们标记出来。谢谢阅读。

相关推荐

前后端分离 Vue + NodeJS(Koa) + MongoDB实践

作者:前端藏经阁转发链接:https://www.yuque.com/xwifrr/gr8qaw/vr51p4写在前面闲来无事,试了一下Koa,第一次搞感觉还不错,这个项目比较基础但还是比较完整了,...

MongoDB 集群如何工作?

一、什么是“MongoDB”?“MongoDB”是一个开源文档数据库,也是领先的“NoSQL”数据库,分别用“C++”“编程语言”编写,使用带有“Schema”的各种类似JSON的文档,是也分别被认为...

三部搭建mongo,和mongo UI界面

三步搭建mongo,和mongoUI界面安装首先你需要先有一个docker的环境检查你的到docker版本docker--versionDockerversion18.03.1-ce,b...

Mongodb 高可用落地方案

此落地方案,用于实现高可用。复制集这里部署相关的复制集,用于实现MongoDB的高可用。介绍MongoDB复制集用于提供相关的数据副本,当发生硬件或者服务中断的时候,将会从副本中恢复数据,并进行自动...

一次线上事故,我顿悟了MongoDB的精髓

大家好,我是哪吒,最近项目在使用MongoDB作为图片和文档的存储数据库,为啥不直接存MySQL里,还要搭个MongoDB集群,麻不麻烦?让我们一起,一探究竟,继续学习MongoDB分片的理论与实践,...

IDEA中安装MongoDB插件-再也无要nosql manager for mongodb

大家都知道MongoDB数据库作为典型的非关系型数据库被广泛使用,但基于MongoDB的可视化管理工具-nosqlmanagerformongodb也被用的较多,但此软件收费,所以国内的破解一般...

数据库监控软件Lepus安装部署详解

Lepus安装部署一、软件介绍Lepus是一套开源的数据库监控平台,目前已经支持MySQL、Oracle、SQLServer、MongoDB、Redis等数据库的基本监控和告警(MySQL已经支持复...

YAPI:从0搭建API文档管理工具

背景最近在找一款API文档管理工具,之前有用过Swagger、APIManager、Confluence,现在用的还是Confluence。我个人一直不喜欢用Swagger,感觉“代码即文档”,让代...

Mac安装使用MongoDB

下载MongoDB包:https://www.mongodb.com/download-center解压mongodb包手动解压到/usr/local/mongodb文件夹配置Mac环境变量打开环境...

保证数据安全,不可不知道的MongoDB备份与恢复

大家在项目中如果使用MongoDB作为NOsql数据库进行存储,那一定涉及到数据的备份与恢复,下面给大家介绍下:MongoDB数据备份方法在MongoDB中我们使用mongodump命令来备...

MongoDB数据备份、还原脚本和定时任务脚本

备注:mongodump和mongorestore命令需要在MongoDB的安装目录bin下备份脚本备份格式/usr/local/mongodb/bin/mongodump -h ...

等保2.0测评:mongoDB数据库

一、MongoDB介绍MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之间的产...

MongoDB入门实操《一》

什么是MongoDBMongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之...

Python安装PyMongo的方法详细介绍

欢迎点击右上角关注小编,除了分享技术文章之外还有很多福利,私信学习资料可以领取包括不限于Python实战演练、PDF电子文档、面试集锦、学习资料等。前言本文主要给大家介绍的是关于安装PyMongo的...

第四篇:linux系统中mongodb的配置

建议使用普通用户进行以下操作。1、切换到普通用户odysee。2、准备mongodb安装包,自行去官网下载。3、解压安装包并重命名为mongodb4.04、配置mongodbcdmongod...

取消回复欢迎 发表评论: