Descargar archivos grandes en python con solicitudes

402

Requests es una biblioteca realmente bonita. Me gustaría usarlo para descargar archivos grandes (> 1 GB). El problema es que no es posible mantener todo el archivo en la memoria. Necesito leerlo en fragmentos. Y este es un problema con el siguiente código

import requests

def DownloadFile(url)
    local_filename = url.split('/')[-1]
    r = requests.get(url)
    f = open(local_filename, 'wb')
    for chunk in r.iter_content(chunk_size=512 * 1024): 
        if chunk: # filter out keep-alive new chunks
            f.write(chunk)
    f.close()
    return 

Por alguna razón no funciona de esta manera. Todavía carga la respuesta en la memoria antes de guardarla en un archivo.

ACTUALIZAR

Si necesita un cliente pequeño (Python 2.x /3.x) que puede descargar archivos grandes desde FTP, puede encontrarlo aquí . Admite subprocesos múltiples y reconexiones (supervisa las conexiones) y también ajusta los parámetros de socket para la tarea de descarga.

Roman Podlinov
fuente

Respuestas:

653

Con el siguiente código de transmisión, el uso de la memoria Python está restringido independientemente del tamaño del archivo descargado:

def download_file(url):
    local_filename = url.split('/')[-1]
    # NOTE the stream=True parameter below
    with requests.get(url, stream=True) as r:
        r.raise_for_status()
        with open(local_filename, 'wb') as f:
            for chunk in r.iter_content(chunk_size=8192): 
                # If you have chunk encoded response uncomment if
                # and set chunk_size parameter to None.
                #if chunk: 
                f.write(chunk)
    return local_filename

Tenga en cuenta que el número de bytes devueltos usando iter_contentno es exactamente el chunk_size; se espera que sea un número aleatorio que a menudo es mucho más grande, y se espera que sea diferente en cada iteración.

Ver https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow y https://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content para más información referencia.

Roman Podlinov
fuente
99
@Shuman Como veo, resolvió el problema al cambiar de http: // a https: // ( github.com/kennethreitz/requests/issues/2043 ). ¿Puede actualizar o eliminar sus comentarios porque la gente puede pensar que hay problemas con el código para archivos más grandes de 1024Mb
Roman Podlinov
8
El chunk_sizees crucial. por defecto es 1 (1 byte). eso significa que por 1 MB hará 1 millón de iteraciones. docs.python-requests.org/en/latest/api/…
Eduard Gamonal
44
f.flush()Parece innecesario ¿Qué estás tratando de lograr al usarlo? (su uso de memoria no será 1.5gb si lo deja caer). f.write(b'')(si iter_content()puede devolver una cadena vacía) debe ser inofensivo y, por if chunklo tanto, también podría descartarse.
jfs
11
@RomanPodlinov: f.flush()no descarga datos al disco físico. Transfiere los datos al sistema operativo. Por lo general, es suficiente a menos que haya una falla de energía. f.flush()hace que el código sea más lento aquí sin ninguna razón. El vaciado ocurre cuando el búfer de archivo correspondiente (dentro de la aplicación) está lleno. Si necesita escrituras más frecuentes; pasar el parámetro buf.size a open().
jfs
99
No olvide cerrar la conexión conr.close()
0xcaff
274

Es mucho más fácil si usa Response.rawy shutil.copyfileobj():

import requests
import shutil

def download_file(url):
    local_filename = url.split('/')[-1]
    with requests.get(url, stream=True) as r:
        with open(local_filename, 'wb') as f:
            shutil.copyfileobj(r.raw, f)

    return local_filename

Esto transmite el archivo al disco sin usar memoria excesiva, y el código es simple.

John Zwinck
fuente
10
Tenga en cuenta que es posible que deba realizar ajustes cuando transmita respuestas comprimidas por correo
electrónico
32
¡ESTA debería ser la respuesta correcta! La respuesta aceptada te lleva hasta 2-3 MB / s. El uso de copyfileobj te lleva a ~ 40MB / s. Descargas de curl (mismas máquinas, misma url, etc.) con ~ 50-55 MB / s.
visoft
24
Para asegurarse de que se libere la conexión de Solicitudes, puede usar un segundo withbloque (anidado) para realizar la solicitud:with requests.get(url, stream=True) as r:
Christian Long el
77
@ChristianLong: Eso es cierto, pero solo muy recientemente, ya que la función de soporte with requests.get()solo se fusionó el 2017-06-07. Su sugerencia es razonable para las personas que tienen Solicitudes 2.18.0 o posterior. Ref: github.com/requests/requests/issues/4136
John Zwinck
54

No es exactamente lo que OP preguntaba, pero ... es ridículamente fácil hacer eso con urllib:

from urllib.request import urlretrieve
url = 'http://mirror.pnl.gov/releases/16.04.2/ubuntu-16.04.2-desktop-amd64.iso'
dst = 'ubuntu-16.04.2-desktop-amd64.iso'
urlretrieve(url, dst)

O de esta manera, si desea guardarlo en un archivo temporal:

from urllib.request import urlopen
from shutil import copyfileobj
from tempfile import NamedTemporaryFile
url = 'http://mirror.pnl.gov/releases/16.04.2/ubuntu-16.04.2-desktop-amd64.iso'
with urlopen(url) as fsrc, NamedTemporaryFile(delete=False) as fdst:
    copyfileobj(fsrc, fdst)

Vi el proceso:

watch 'ps -p 18647 -o pid,ppid,pmem,rsz,vsz,comm,args; ls -al *.iso'

Y vi crecer el archivo, pero el uso de memoria se mantuvo en 17 MB. ¿Me estoy perdiendo de algo?

x-yuri
fuente
2
Para Python 2.x, usefrom urllib import urlretrieve
Vadim Kotov el
Esto resulta en una velocidad de descarga lenta ...
citynorman
@citynorman ¿Puedes dar más detalles? En comparación con qué solución? ¿Por qué?
x-yuri
@ x-yuri vs la solución shutil.copyfileobjcon más votos, vea mis comentarios y los de otros allí
citynorman
42

El tamaño de tu fragmento podría ser demasiado grande, ¿has intentado soltarlo, quizás 1024 bytes a la vez? (también, podría usar withpara ordenar la sintaxis)

def DownloadFile(url):
    local_filename = url.split('/')[-1]
    r = requests.get(url)
    with open(local_filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024): 
            if chunk: # filter out keep-alive new chunks
                f.write(chunk)
    return 

Por cierto, ¿cómo deduce que la respuesta se ha cargado en la memoria?

Parece que Python no está vaciando los datos al archivo, de otras preguntas SO que podría intentar f.flush()y os.fsync()forzar la escritura del archivo y la memoria libre;

    with open(local_filename, 'wb') as f:
        for chunk in r.iter_content(chunk_size=1024): 
            if chunk: # filter out keep-alive new chunks
                f.write(chunk)
                f.flush()
                os.fsync(f.fileno())
danodonovan
fuente
1
Yo uso System Monitor en Kubuntu. Me muestra que la memoria del proceso de Python aumenta (hasta 1,5 gb desde 25 kb).
Roman Podlinov
Esa hinchazón de memoria apesta, tal vez f.flush(); os.fsync()podría forzar una escritura libre de memoria.
danodonovan
2
esos.fsync(f.fileno())
sebdelsol
29
Debe usar stream = True en la llamada request.get (). Eso es lo que está causando la hinchazón de la memoria.
Choza8
1
error tipográfico menor: se pierde un colon (':') despuésdef DownloadFile(url)
Aubrey