Télécharger un gros fichier en python avec des requêtes

402

Requests est une bibliothèque vraiment sympa. Je voudrais l'utiliser pour télécharger de gros fichiers (> 1 Go). Le problème est qu'il n'est pas possible de conserver le fichier entier en mémoire, j'ai besoin de le lire en morceaux. Et c'est un problème avec le code suivant

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 

Pour une raison quelconque, cela ne fonctionne pas de cette façon. Il charge toujours la réponse en mémoire avant de l'enregistrer dans un fichier.

MISE À JOUR

Si vous avez besoin d'un petit client (Python 2.x /3.x) qui peut télécharger de gros fichiers depuis FTP, vous pouvez le trouver ici . Il prend en charge le multithreading et les reconnexions (il surveille les connexions) et ajuste les paramètres de socket pour la tâche de téléchargement.

Roman Podlinov
la source

Réponses:

653

Avec le code de streaming suivant, l'utilisation de la mémoire Python est limitée quelle que soit la taille du fichier téléchargé:

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

Notez que le nombre d'octets retournés en utilisant iter_contentn'est pas exactement le chunk_size; il devrait être un nombre aléatoire qui est souvent beaucoup plus grand et devrait être différent à chaque itération.

Voir https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow et https://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content pour plus d'informations référence.

Roman Podlinov
la source
9
@Shuman Comme je vois, vous avez résolu le problème en passant de http: // à https: // ( github.com/kennethreitz/requests/issues/2043 ). Pouvez-vous mettre à jour ou supprimer vos commentaires, car les gens peuvent penser qu'il y a des problèmes avec le code pour les fichiers plus grands 1024 Mo
Roman Podlinov
8
le chunk_sizeest crucial. par défaut, c'est 1 (1 octet). cela signifie que pour 1 Mo, cela fera 1 million d'itérations. docs.python-requests.org/en/latest/api/…
Eduard Gamonal
4
f.flush()semble inutile. Qu'essayez-vous d'accomplir en l'utilisant? (votre utilisation de la mémoire ne sera pas de 1,5 Go si vous la laissez tomber). f.write(b'')(si iter_content()peut renvoyer une chaîne vide) doit être inoffensif et if chunkpeut donc également être supprimé.
jfs
11
@RomanPodlinov: f.flush()ne vide pas les données sur le disque physique. Il transfère les données vers l'OS. Habituellement, c'est suffisant, sauf en cas de panne de courant. f.flush()rend le code plus lent ici sans raison. Le vidage se produit lorsque le tampon de fichier correspondant (à l'intérieur de l'application) est plein. Si vous avez besoin d'écritures plus fréquentes; passez le paramètre buf.size à open().
jfs
9
N'oubliez pas de fermer la connexion avecr.close()
0xcaff
274

C'est beaucoup plus facile si vous utilisez Response.rawet 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

Cela diffuse le fichier sur le disque sans utiliser de mémoire excessive et le code est simple.

John Zwinck
la source
10
Notez que vous devrez peut-être ajuster lors de la diffusion en continu des réponses compressées par problème 2155.
ChrisP
32
CECI devrait être la bonne réponse! La réponse acceptée vous permet d'atteindre 2-3 Mo / s. L'utilisation de copyfileobj vous permet d'atteindre ~ 40 Mo / s. Téléchargements Curl (mêmes machines, même URL, etc.) avec ~ 50-55 Mo / s.
visoft
24
Pour vous assurer que la connexion Requests est libérée, vous pouvez utiliser un deuxième withbloc (imbriqué) pour effectuer la requête:with requests.get(url, stream=True) as r:
Christian Long
7
@ChristianLong: C'est vrai, mais seulement très récemment, car la fonctionnalité à prendre en charge with requests.get()n'a été fusionnée que le 07/06/2017! Votre suggestion est raisonnable pour les personnes qui ont des demandes 2.18.0 ou ultérieures. Ref: github.com/requests/requests/issues/4136
John Zwinck
4
@EricCousineau Vous pouvez corriger ce comportement en remplaçant la readméthode:response.raw.read = functools.partial(response.raw.read, decode_content=True)
Nuno André
54

Pas exactement ce qu'OP demandait, mais ... c'est ridiculement facile de le faire avec 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)

Ou de cette façon, si vous souhaitez l'enregistrer dans un fichier temporaire:

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)

J'ai regardé le processus:

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

Et j'ai vu le fichier augmenter, mais l'utilisation de la mémoire est restée à 17 Mo. Suis-je en train de manquer quelque chose?

x-yuri
la source
2
Pour Python 2.x, utilisezfrom urllib import urlretrieve
Vadim Kotov
Cela se traduit par une vitesse de téléchargement lente ...
citynorman
@citynorman Pouvez-vous élaborer? Par rapport à quelle solution? Pourquoi?
x-yuri
@ x-yuri vs la solution shutil.copyfileobjavec le plus de votes, voir mes commentaires et ceux là
citynorman
42

Votre taille de bloc peut être trop grande, avez-vous essayé de supprimer cela - peut-être 1024 octets à la fois? (vous pouvez également utiliser withpour ranger la syntaxe)

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 

Soit dit en passant, comment déduisez-vous que la réponse a été chargée en mémoire?

Il semble que python n'est pas le rinçage des données dans un fichier, d'autres questions SO vous pouvez essayer f.flush()et os.fsync()forcer la écriture de fichiers et de la mémoire 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
la source
1
J'utilise System Monitor dans Kubuntu. Cela me montre que la mémoire du processus python augmente (jusqu'à 1,5 Go à partir de 25 Ko).
Roman Podlinov
Ce ballonnement de mémoire est nul, peut f.flush(); os.fsync()- être pourrait-il forcer l'écriture sur une mémoire libre.
danodonovan
2
c'estos.fsync(f.fileno())
sebdelsol
29
Vous devez utiliser stream = True dans l'appel requests.get (). C'est ce qui cause le gonflement de la mémoire.
Hut8
1
faute de frappe mineure: vous manquez deux points (':') aprèsdef DownloadFile(url)
Aubrey