Pobierz duży plik w pythonie z żądaniami

398

Requests to naprawdę fajna biblioteka. Chciałbym go użyć do pobierania dużych plików (> 1 GB). Problem polega na tym, że nie można zachować całego pliku w pamięci. Potrzebuję go odczytać w kawałkach. Jest to problem z następującym kodem

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 

Z jakiegoś powodu to nie działa w ten sposób. Nadal ładuje odpowiedź do pamięci przed zapisaniem jej w pliku.

AKTUALIZACJA

Jeśli potrzebujesz małego klienta (Python 2.x / 3..x), który może pobierać duże pliki z FTP, możesz go znaleźć tutaj . Obsługuje wielowątkowość i ponowne łączenie (monitoruje połączenia), a także dostraja parametry gniazd dla zadania pobierania.

Roman Podlinov
źródło

Odpowiedzi:

650

W przypadku następującego kodu przesyłania strumieniowego użycie pamięci w języku Python jest ograniczone bez względu na rozmiar pobranego pliku:

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

Zauważ, że liczba bajtów zwróconych przy użyciu iter_contentnie jest dokładnie taka chunk_size; oczekuje się, że będzie to liczba losowa, która jest często znacznie większa i oczekuje się, że będzie różna w każdej iteracji.

Zobacz https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow i https://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content w celu uzyskania dalszych informacji odniesienie.

Roman Podlinov
źródło
9
@Shuman Jak widzę, rozwiązałeś problem po przejściu z http: // na https: // ( github.com/kennethreitz/requests/issues/2043 ). Czy możesz zaktualizować lub usunąć swoje komentarze, ponieważ ludzie mogą myśleć, że występują problemy z kodem dla plików większych 1024Mb
Roman Podlinov
8
to chunk_sizejest kluczowe. domyślnie jest to 1 (1 bajt). oznacza to, że za 1 MB wykona 1 milion iteracji. docs.python-requests.org/en/latest/api/…
Eduard Gamonal
4
f.flush()wydaje się niepotrzebne. Co próbujesz osiągnąć, używając go? (użycie pamięci nie wyniesie 1,5 GB, jeśli ją upuścisz). f.write(b'')(jeśli iter_content()może zwrócić pusty ciąg) powinien być nieszkodliwy i dlatego if chunkmoże zostać upuszczony.
jfs
11
@RomanPodlinov: f.flush()nie opróżnia danych na dysk fizyczny. Przesyła dane do systemu operacyjnego. Zwykle wystarczy, chyba że nastąpi awaria zasilania. f.flush()spowalnia kod tutaj bez powodu. Opróżnianie ma miejsce, gdy odpowiedni bufor pliku (w aplikacji) jest pełny. Jeśli potrzebujesz częstszych zapisów; przekazać parametr buf.size do open().
jfs
9
Nie zapomnij zamknąć połączenia zr.close()
0xcaff,
271

Jest to o wiele łatwiejsze, jeśli używasz Response.rawi 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

To przesyła plik na dysk bez użycia nadmiernej pamięci, a kod jest prosty.

John Zwinck
źródło
10
Należy pamiętać, że konieczne może być dostosowanie podczas strumieniowego zgzipowanego odpowiedzi na numerze 2155.
Chrisp
32
TO powinna być poprawna odpowiedź! Akceptowane odpowiedź dostaje się do 2-3MB / s. Korzystanie z copyfileobj pozwala uzyskać ~ 40 MB / s. Zwijaj pliki do pobrania (te same maszyny, ten sam adres URL itp.) Przy ~ 50-55 MB / s.
visoft,
24
Aby upewnić się, że połączenie Żądania zostanie zwolnione, możesz użyć drugiego (zagnieżdżonego) withbloku, aby wysłać żądanie:with requests.get(url, stream=True) as r:
Christian Long
7
@ChristianLong: To prawda, ale dopiero niedawno, ponieważ funkcja do obsługi with requests.get()została połączona dopiero w dniu 2017-06-07! Twoja sugestia jest uzasadniona dla osób, które mają Wnioski 2.18.0 lub nowsze. Ref: github.com/requests/requests/issues/4136
John Zwinck
54

Nie do końca to, o co prosi OP, ale ... jest to absurdalnie łatwe, aby to zrobić za pomocą 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)

Lub w ten sposób, jeśli chcesz zapisać go do pliku tymczasowego:

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)

Obejrzałem proces:

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

Widziałem, jak plik się powiększa, ale zużycie pamięci pozostało na poziomie 17 MB. Czy coś brakuje?

x-yuri
źródło
2
W przypadku Python 2.x użyjfrom urllib import urlretrieve
Vadim Kotov
Powoduje to spowolnienie pobierania ...
citynorman
@citynorman Czy potrafisz opracować? W porównaniu z jakim rozwiązaniem? Dlaczego?
x-yuri
@ x-yuri vs rozwiązanie shutil.copyfileobjz największą liczbą głosów, zobacz moje i inne komentarze tam
citynorman
41

Twój rozmiar fragmentu może być zbyt duży, czy próbowałeś to upuścić - może 1024 bajty na raz? (możesz też użyć withdo uporządkowania składni)

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 

Nawiasem mówiąc, jak wnioskujesz, że odpowiedź została załadowana do pamięci?

To brzmi tak, jakby pyton nie jest przepłukanie danych do pliku, od innych pytań, dlatego można spróbować f.flush()i os.fsync()aby wymusić zapis pliku i wolnej pamięci;

    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
źródło
1
Używam Monitora systemu w Kubuntu. Pokazuje mi, że pamięć procesowa Pythona wzrasta (do 1,5 GB z 25 KB).
Roman Podlinov
Ten wzdęcie pamięci jest do bani, może f.flush(); os.fsync()może wymusić zapisanie pamięci wolnej.
danodonovan
2
jestos.fsync(f.fileno())
sebdelsol,
29
Musisz użyć stream = True w wywołaniu requests.get (). To właśnie powoduje wzdęcie pamięci.
Hut8
1
drobna literówka: brakuje Ci dwukropka („:”) podef DownloadFile(url)
Aubrey,