Baixe arquivos grandes em python com solicitações

401

Solicitações é uma biblioteca muito boa. Gostaria de usá-lo para baixar arquivos grandes (> 1 GB). O problema é que não é possível manter o arquivo inteiro na memória, preciso lê-lo em pedaços. E este é um problema com o seguinte 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 alguma razão, não funciona dessa maneira. Ele ainda carrega a resposta na memória antes de salvá-la em um arquivo.

ATUALIZAR

Se você precisar de um cliente pequeno (Python 2.x /3.x) que possa baixar grandes arquivos do FTP, pode encontrá-lo aqui . Ele suporta multithreading e reconecta (ele monitora conexões) e também ajusta parâmetros de soquete para a tarefa de download.

Roman Podlinov
fonte

Respostas:

653

Com o seguinte código de streaming, o uso da memória Python é restrito, independentemente do tamanho do arquivo baixado:

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

Observe que o número de bytes retornados usando iter_contentnão é exatamente o chunk_size; espera-se que seja um número aleatório que geralmente é muito maior e que seja diferente em cada iteração.

Consulte https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow e https://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content para obter mais informações referência.

Roman Podlinov
fonte
9
@Shuman Pelo que vejo, você resolveu o problema quando mudou de http: // para https: // ( github.com/kennethreitz/requests/issues/2043 ). Você pode agradar a atualização ou excluir seus comentários, porque as pessoas podem pensar que há problemas com o código para os ficheiros maiores 1024MB
Roman Podlinov
8
o chunk_sizeé crucial. por padrão, é 1 (1 byte). isso significa que, para 1 MB, ele fará 1 milhão de iterações. docs.python-requests.org/en/latest/api/…
Eduard Gamonal
4
f.flush()parece desnecessário. O que você está tentando realizar usando-o? (seu uso de memória não será de 1,5 GB se você o soltar). f.write(b'')(se iter_content()pode retornar uma string vazia) deve ser inofensiva e, portanto, também if chunkpode ser descartada.
jfs
11
@RomanPodlinov: f.flush()não libera dados para o disco físico. Ele transfere os dados para o sistema operacional. Geralmente, é suficiente, a menos que haja uma falha de energia. f.flush()torna o código mais lento aqui sem motivo. A liberação ocorre quando o buffer do arquivo correspondente (aplicativo interno) está cheio. Se você precisar de gravações mais frequentes; passe o parâmetro buf.size para open().
jfs
9
Não se esqueça de fechar a conexão comr.close()
0xcaff 25/11
273

É muito mais fácil se você usar 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

Isso transmite o arquivo para o disco sem usar memória excessiva, e o código é simples.

John Zwinck
fonte
10
Note que você pode precisar de ajustar quando streaming de respostas gzipped por edição 2155.
ChrisP
32
ESTA deve ser a resposta correta! A resposta aceita leva de 2 a 3 MB / s. Usando copyfileobj você chega a ~ 40MB / s. Transferências de curl (mesmas máquinas, mesmo URL, etc) com ~ 50-55 MB / s.
Visoft
24
Para garantir que a conexão Solicitações seja liberada, você pode usar um segundo withbloco (aninhado) para fazer a solicitação:with requests.get(url, stream=True) as r:
Christian Long
7
@ChristianLong: Isso é verdade, mas apenas muito recentemente, já que o recurso de suporte with requests.get()foi mesclado em 07/06/2017! Sua sugestão é razoável para pessoas que têm solicitações 2.18.0 ou posterior. Re
John Zwinck
4
@EricCousineau Você pode corrigir esse comportamento, substituindo o readmétodo:response.raw.read = functools.partial(response.raw.read, decode_content=True)
Nuno André
54

Não é exatamente o que o OP estava pedindo, mas ... é ridiculamente fácil fazer isso com 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 assim, se você deseja salvá-lo em um arquivo temporário:

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)

Eu assisti o processo:

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

E vi o arquivo crescendo, mas o uso de memória ficou em 17 MB. Estou esquecendo de algo?

x-yuri
fonte
2
Para Python 2.x, usefrom urllib import urlretrieve
Vadim Kotov
Isso resulta em uma velocidade de download lenta ...
citynorman
@citynorman Você pode elaborar? Comparado com qual solução? Por quê?
x-yuri
@ x-yuri vs a solução shutil.copyfileobjcom mais votos, veja meus e outros comentários lá
citynorman
42

O tamanho do seu pedaço pode ser muito grande. Você já tentou descartá-lo - talvez 1024 bytes por vez? (também, você pode usar withpara arrumar a sintaxe)

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 

Aliás, como você deduz que a resposta foi carregada na memória?

Soa como se python não está esvaziando os dados de arquivo, de outros SO perguntas que você poderia tentar f.flush()e os.fsync()forçar a gravação de arquivo e memória livre;

    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
fonte
11
Eu uso o Monitor do sistema no Kubuntu. Isso me mostra que a memória do processo python aumenta (até 1.5gb a partir de 25kb).
Roman Podlinov
Esse inchaço da memória é péssimo, talvez f.flush(); os.fsync()possa forçar a gravação de uma memória livre.
danodonovan
2
éos.fsync(f.fileno())
sebdelsol
29
Você precisa usar stream = True na chamada orders.get (). É isso que está causando o inchaço da memória.
Hut8
11
erro de digitação menor: você perde dois pontos (':') apósdef DownloadFile(url)
Aubrey