Muy a menudo, algunos administradores establecen límites en la velocidad de descarga de archivos, esto reduce la carga en la red, pero al mismo tiempo es muy molesto para los usuarios, especialmente cuando necesita descargar un archivo grande (desde 1 GB), y la velocidad fluctúa alrededor de 1 megabit por segundo (125 kilobytes por segundo). En base a estos datos, concluimos que la velocidad de descarga será de al menos 8192 segundos (2 horas 16 minutos 32 segundos). Aunque nuestro ancho de banda nos permite transferir hasta 16 Mbps (2 MB por segundo) y tardará 512 segundos (8 minutos 32 segundos).
Estos valores no se toman al azar, para tal descarga, inicialmente utilicé exclusivamente Internet 4G.
Caso:
La utilidad desarrollada por mí y presentada a continuación funciona solo si:
¿Para qué sirven estas restricciones?
¿Cómo se implementa esta desaceleración?
Nginx
location /static/ { ... limit rate 50 k; -> 50 kilobytes per second for a single connection
... } location /videos/ { ... limit rate 500 k; -> 500 kilobytes per second for a single connection
limit_rate_after 10 m; -> after 10 megabytes download speed, will 500 kilobytes per second for a single connection
... }
Función con archivo zip
Se descubrió una característica interesante al descargar un archivo en la extensión zip, cada parte le permite mostrar parcialmente los archivos en el archivo, aunque la mayoría de los archivadores dirán que el archivo está roto y no es válido, algunos de los nombres de contenido y archivo serán desplegado.
Análisis de código:
Para crear este programa, necesitamos Python, asyncio, aiohttp, aiofiles. Todo el código será asíncrono para aumentar el rendimiento y minimizar la sobrecarga en términos de memoria y velocidad. También es posible ejecutar en subprocesos y procesos, pero al cargar un archivo grande, pueden ocurrir errores cuando no se puede crear un subproceso o proceso.
async def get_content_length ( url ):
async with aiohttp.ClientSession() as session: async with session.head(url) as request: return request.content_length
Esta función devuelve la longitud del archivo. Y la solicitud en sí usa HEAD en lugar de GET, lo que significa que solo obtenemos los encabezados, sin el cuerpo (contenido en la URL dada).
def parts_generator ( size, start= 0 , part_size= 10 * 1024 ** 2 ):
while size - start > part_size: yield start, start + part_size start += part_size yield start, size
Este generador devuelve rangos para descargar. Un punto importante es elegir part_size que sea un múltiplo de 1024 para mantener proporciones por megas, aunque parece que cualquier número sirve. No funciona correctamente con part_size = 1, por lo que prefiero 10 MB por parte.
async def download ( url, headers, save_path ):
async with aiohttp.ClientSession(headers=headers) as session: async with session.get(url) as request: file = await aiofiles. open (save_path, 'wb' ) await file.write( await request.content.read())
Una de las funciones principales es la descarga de archivos. Funciona de forma asíncrona. Aquí necesitamos archivos asincrónicos para acelerar las escrituras en disco al no bloquear las operaciones de entrada y salida.
async def process ( url ):
filename = os.path.basename(urlparse(url).path) tmp_dir = TemporaryDirectory(prefix=filename, dir =os.path.abspath( '.' )) size = await get_content_length(url) tasks = [] file_parts = [] for number, sizes in enumerate (parts_generator(size)): part_file_name = os.path.join(tmp_dir.name, f' {filename} .part {number} ' ) file_parts.append(part_file_name) tasks.append(download(URL, { 'Range' : f'bytes= {sizes[ 0 ]} - {sizes[ 1 ]} ' }, part_file_name)) await asyncio.gather(*tasks) with open (filename, 'wb' ) as wfd: for f in file_parts: with open (f, 'rb' ) as fd: shutil.copyfileobj(fd, wfd)
La función más básica obtiene el nombre de archivo de la URL, lo convierte en un archivo .part numerado, crea un directorio temporal debajo del archivo original, todas las partes se descargan en él. await asyncio.gather(*tareas) le permite ejecutar todas las corrutinas recopiladas al mismo tiempo, lo que acelera significativamente la descarga. Después de eso, el método shutil.copyfileobj ya síncrono concatena todos los archivos en un solo archivo.
async def main ():
if len (sys.argv) <= 1 : print ( 'Add URLS' ) exit( 1 ) urls = sys.argv[ 1 :] await asyncio.gather(*[process(url) for url in urls])
La función principal recibe una lista de URL desde la línea de comandos y, utilizando el ya conocido asyncio.gather , comienza a descargar muchos archivos al mismo tiempo.
Punto de referencia:
En uno de los recursos que encontré, se llevó a cabo una evaluación comparativa sobre la descarga de una imagen de Gentoo Linux desde un sitio de una universidad ( servidor lento) .
Descarga la distribución de DietPi (servidor rápido):
Como puede ver, el resultado alcanza casi 3x de aceleración. En algunos archivos, el resultado alcanzó 20-30 veces.
Posibles mejoras:
En conclusión, puedo decir que la carga asíncrona es la salida, pero desafortunadamente no es una panacea en lo que respecta a la descarga de archivos.
import asyncio import os.path import shutil import aiofiles import aiohttp from tempfile import TemporaryDirectory import sys from urllib.parse import urlparse async def get_content_length ( url ):
async with aiohttp.ClientSession() as session: async with session.head(url) as request: return request.content_length def parts_generator ( size, start= 0 , part_size= 10 * 1024 ** 2 ):
while size - start > part_size: yield start, start + part_size start += part_size yield start, size async def download ( url, headers, save_path ):
async with aiohttp.ClientSession(headers=headers) as session: async with session.get(url) as request: file = await aiofiles. open (save_path, 'wb' ) await file.write( await request.content.read()) async def process ( url ):
filename = os.path.basename(urlparse(url).path) tmp_dir = TemporaryDirectory(prefix=filename, dir =os.path.abspath( '.' )) size = await get_content_length(url) tasks = [] file_parts = [] for number, sizes in enumerate (parts_generator(size)): part_file_name = os.path.join(tmp_dir.name, f' {filename} .part {number} ' ) file_parts.append(part_file_name) tasks.append(download(url, { 'Range' : f'bytes= {sizes[ 0 ]} - {sizes[ 1 ]} ' }, part_file_name)) await asyncio.gather(*tasks) with open (filename, 'wb' ) as wfd: for f in file_parts: with open (f, 'rb' ) as fd: shutil.copyfileobj(fd, wfd) async def main ():
if len (sys.argv) <= 1 : print ( 'Add URLS' ) exit( 1 ) urls = sys.argv[ 1 :] await asyncio.gather(*[process(url) for url in urls]) if __name__ == '__main__' : import time start_code = time.monotonic() loop = asyncio.get_event_loop() loop.run_until_complete(main()) print ( f' {time.monotonic() - start_code} seconds!' )