474 votes

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

Demandes est une très belle bibliothèque. J'aimerais l'utiliser pour télécharger de gros fichiers (>1GB). Le problème est qu'il n'est pas possible de garder tout le fichier en mémoire ; je dois le lire par 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.

UPDATE

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

767voto

Roman Podlinov Points 1294

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_content n'est pas exactement le chunk_size On s'attend à ce qu'il s'agisse d'un nombre aléatoire qui est souvent beaucoup plus grand, et qui est censé être différent à chaque itération.

Voir body-content-workflow y Réponse.iter_content pour de plus amples informations.

0 votes

Shuman Ce code télécharge avec succès des fichiers d'une taille supérieure à 1,5 Go. Pouvez-vous télécharger le fichier via n'importe quel navigateur avec succès ?

0 votes

Oui, dans firefox, si je télécharge manuellement, le fichier .zip de 1,5 Go est enregistré avec succès.

9 votes

@Shuman Comme je le vois, vous avez résolu le problème en passant de http:// à https:// ( github.com/kennethreitz/requests/issues/2043 ). Pouvez-vous s'il vous plaît mettre à jour ou supprimer vos commentaires parce que les gens peuvent penser qu'il ya des problèmes avec le code pour les fichiers plus grands 1024Mb

372voto

John Zwinck Points 43636

C'est beaucoup plus facile si vous utilisez Response.raw y 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 permet de transférer le fichier sur le disque sans utiliser trop de mémoire, et le code est simple.

16 votes

Notez que vous devrez peut-être ajuster lorsque streaming des réponses gzippées selon le numéro 2155.

1 votes

Avez-vous testé ce code pour le téléchargement de gros fichiers >1gb ?

1 votes

Oui, je l'ai fait. La plupart des fichiers étaient > 1GB. Le code téléchargeait un tas de fichiers vidéo sur une base quotidienne.

67voto

x-yuri Points 616

Ce n'est pas exactement ce que l'OP demandait, mais... c'est ridiculement facile de faire ça 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 voulez 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 observé le processus :

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

Et j'ai vu le fichier grandir, mais l'utilisation de la mémoire est restée à 17 Mo. Est-ce que quelque chose m'échappe ?

2 votes

Pour Python 2.x, utilisez from urllib import urlretrieve

42voto

danodonovan Points 5268

La taille de vos morceaux est peut-être trop importante, avez-vous essayé de la réduire - peut-être 1024 octets à la fois ? (vous pouvez également utiliser with pour mettre de l'ordre dans 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 

Au fait, comment déduisez-vous que la réponse a été chargée en mémoire ?

Il semble que python ne vide pas les données dans le fichier, à partir d'autres sources. Questions de fond vous pouvez essayer f.flush() y os.fsync() pour forcer l'écriture du fichier et libérer de la mémoire ;

    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())

1 votes

J'utilise System Monitor dans Kubuntu. Il me montre que la mémoire du processus python augmente (jusqu'à 1,5 Go contre 25 Ko).

0 votes

Ce bloat de mémoire craint, peut-être f.flush(); os.fsync() pourrait forcer une écriture et une libération de la mémoire.

2 votes

C'est os.fsync(f.fileno())

6voto

Ben Moskovitch Points 96

Basé sur le commentaire le plus voté de Roman ci-dessus, voici mon implémentation, incluant les mécanismes "download as" et "retries" :

def download(url: str, file_path='', attempts=2):
    """Downloads a URL content into a file (with large file support by streaming)

    :param url: URL to download
    :param file_path: Local file name to contain the data downloaded
    :param attempts: Number of attempts
    :return: New file path. Empty string if the download failed
    """
    if not file_path:
        file_path = os.path.realpath(os.path.basename(url))
    logger.info(f'Downloading {url} content to {file_path}')
    url_sections = urlparse(url)
    if not url_sections.scheme:
        logger.debug('The given url is missing a scheme. Adding http scheme')
        url = f'http://{url}'
        logger.debug(f'New url: {url}')
    for attempt in range(1, attempts+1):
        try:
            if attempt > 1:
                time.sleep(10)  # 10 seconds wait time between downloads
            with requests.get(url, stream=True) as response:
                response.raise_for_status()
                with open(file_path, 'wb') as out_file:
                    for chunk in response.iter_content(chunk_size=1024*1024):  # 1MB chunks
                        out_file.write(chunk)
                logger.info('Download finished successfully')
                return file_path
        except Exception as ex:
            logger.error(f'Attempt #{attempt} failed with error: {ex}')
    return ''

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X