Scarica file di grandi dimensioni in Python con richieste


402

Requests è davvero una bella libreria. Vorrei usarlo per scaricare file di grandi dimensioni (> 1 GB). Il problema è che non è possibile mantenere l'intero file in memoria. Ho bisogno di leggerlo a pezzi. E questo è un problema con il seguente codice

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 

Per qualche motivo non funziona in questo modo. Carica ancora la risposta in memoria prima di salvarla in un file.

AGGIORNARE

Se hai bisogno di un piccolo client (Python 2.x /3.x) in grado di scaricare file di grandi dimensioni da FTP, puoi trovarlo qui . Supporta il multithreading e le riconnessioni (monitora le connessioni) e ottimizza i parametri socket per l'attività di download.

Risposte:


653

Con il seguente codice di streaming, l'utilizzo della memoria di Python è limitato indipendentemente dalle dimensioni del file scaricato:

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

Si noti che il numero di byte restituiti utilizzando iter_contentnon è esattamente il chunk_size; si prevede che sia un numero casuale che è spesso molto più grande e che dovrebbe essere diverso in ogni iterazione.

Vedi https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow e https://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content per ulteriori riferimento.


9
@Shuman Come vedo, hai risolto il problema passando da http: // a https: // ( github.com/kennethreitz/requests/issues/2043 ). Puoi aggiornare o eliminare i tuoi commenti perché le persone potrebbero pensare che ci siano problemi con il codice per file di dimensioni superiori a 1024 Mb
Roman Podlinov

8
il chunk_sizeè fondamentale. per impostazione predefinita è 1 (1 byte). ciò significa che per 1 MB eseguirà 1 milione di iterazioni. docs.python-requests.org/en/latest/api/…
Eduard Gamonal

4
f.flush()sembra inutile. Cosa stai cercando di realizzare usando? (l'utilizzo della memoria non sarà di 1,5 GB se lo lasci cadere). f.write(b'')(se iter_content()può restituire una stringa vuota) dovrebbe essere innocuo e quindi if chunkpotrebbe anche essere eliminato.
jfs,

11
@RomanPodlinov: f.flush()non scarica i dati sul disco fisico. Trasferisce i dati sul sistema operativo. Di solito, è sufficiente a meno che non si verifichi un'interruzione di corrente. f.flush()rende il codice più lento qui senza motivo. Il flush si verifica quando il buffer del file corrispondente (all'interno dell'app) è pieno. Se hai bisogno di scritture più frequenti; passare il parametro buf.size a open().
jfs,

9
Non dimenticare di chiudere la connessione conr.close()
0xcaff il

274

È molto più semplice se si utilizza Response.rawe 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

Questo esegue lo streaming del file su disco senza utilizzare memoria eccessiva e il codice è semplice.


10
Tieni presente che potresti dover modificare le impostazioni per lo streaming delle risposte gzip per numero 2155.
ChrisP

32
QUESTA dovrebbe essere la risposta corretta! La risposta accettata ti porta fino a 2-3 MB / s. L'uso di copyfileobj ti porta a ~ 40 MB / s. Download di arricciature (stesse macchine, stesso url, ecc.) Con ~ 50-55 MB / s.
visoft,

24
Per assicurarsi che venga rilasciata la connessione Richieste, è possibile utilizzare un secondo withblocco (nidificato) per effettuare la richiesta:with requests.get(url, stream=True) as r:
Christian Long,

7
@ChristianLong: È vero, ma solo di recente, poiché la funzione da supportare è with requests.get()stata unita solo il 07/06/2017! Il tuo suggerimento è ragionevole per le persone che hanno Richieste 2.18.0 o successive. Rif: github.com/requests/requests/issues/4136
John Zwinck


54

Non esattamente quello che stava chiedendo OP, ma ... è ridicolmente facile farlo 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 in questo modo, se si desidera salvarlo in un file temporaneo:

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)

Ho visto il processo:

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

E ho visto crescere il file, ma l'utilizzo della memoria è rimasto a 17 MB. Mi sto perdendo qualcosa?


2
Per Python 2.x, usafrom urllib import urlretrieve
Vadim Kotov

Ciò si traduce in una bassa velocità di download ...
citynorman

@citynorman Puoi elaborare? Rispetto a quale soluzione? Perché?
x-yuri

@ x-yuri vs la soluzione shutil.copyfileobjcon il maggior numero di voti, vedi i miei e altri commenti lì
citynorman,

42

La dimensione del tuo blocco potrebbe essere troppo grande, hai provato a farlo cadere - forse 1024 byte alla volta? (inoltre, è possibile utilizzare withper riordinare la sintassi)

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 

Per inciso, come stai deducendo che la risposta è stata caricata in memoria?

Sembra come se python non è vampate di calore i dati in un file, da altre domande in modo si potrebbe provare f.flush()e os.fsync()per forzare il file di scrittura e memoria libera;

    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
Uso System Monitor in Kubuntu. Mi mostra che la memoria di processo di Python aumenta (fino a 1,5 GB da 25 KB).
Roman Podlinov,

Quel gonfiore di memoria fa schifo, forse f.flush(); os.fsync()potrebbe costringere una scrittura a liberare memoria.
danodonovan,

2
èos.fsync(f.fileno())
sebdelsol il

29
È necessario utilizzare stream = True nella chiamata request.get (). Questo è ciò che sta causando il gonfiore della memoria.
Capanna

1
piccolo errore di battitura: ti manca un colon (':') dopodef DownloadFile(url)
Aubrey
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.