paint-brush
Cómo acelerar las descargas de archivos con Pythonpor@exactor
14,520 lecturas
14,520 lecturas

Cómo acelerar las descargas de archivos con Python

por Maksim Kuznetsov6m2022/02/07
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

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, sobre todo cuando se necesita descargar un archivo de gran tamaño (a partir de 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), tardará 512 segundos.

Company Mentioned

Mention Thumbnail
featured image - Cómo acelerar las descargas de archivos con Python
Maksim Kuznetsov HackerNoon profile picture

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:

  • Sabes de antemano que tu ancho de banda es superior a la velocidad de descarga
  • Incluso las páginas grandes del sitio se cargan rápidamente (la primera señal de una velocidad artificialmente baja)
  • No estás usando un proxy lento o VPN
  • Buen ping al sitio

¿Para qué sirven estas restricciones?

  • optimización del backend y devolución de archivos estáticos
  • Protección DDoS

¿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) .

  • asíncrono: 164.682 segundos
  • sincronización: 453.545 segundos

Descarga la distribución de DietPi (servidor rápido):

  • asíncrono: 17.106 segundos mejor tiempo, 20.056 segundos peor tiempo
  • sincronización: 15.897 segundos mejor tiempo, 25.832 peor tiempo

Como puede ver, el resultado alcanza casi 3x de aceleración. En algunos archivos, el resultado alcanzó 20-30 veces.

Posibles mejoras:

  • Descarga más segura. Si hay un error, reinicie la descarga.
  • Optimización de memoria. Uno de los problemas es un aumento de 2x en el consumo de espacio de almacenamiento. (cuando se descargan todas las partes, se copian en un nuevo archivo, pero el directorio aún no se ha eliminado). Se soluciona fácilmente eliminando el archivo inmediatamente después de copiar el contenido de la parte.
  • Algunos servidores realizan un seguimiento de la cantidad de conexiones y pueden arruinar dicha carga, lo que requiere pausar o aumentar considerablemente el tamaño de la parte.
  • Adición de una barra de progreso.

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!' )