Pobierz duży plik w Pythonie z żądaniami

Requests to naprawdę fajna biblioteka. Chciałbym go użyć do pobierania dużych plików (>1GB). Problem w tym, że nie jest możliwe utrzymanie całego pliku w pamięci; muszę go odczytać w kawałkach. I 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 nie działa to w ten sposób: nadal ładuje odpowiedź do pamięci, zanim zostanie zapisana do pliku.

UPDATE

Jeśli potrzebujesz małego klienta (Python 2.x / 3.x) które można pobrać Duże pliki z FTP, znajdziesz je tutaj . Obsługuje wielowątkowość i ponowne połączenia (monitoruje połączenia), a także dostraja paramy gniazd do zadania pobierania.

Author: JoeyC, 2013-05-22

6 answers

W poniższym kodzie strumieniowym użycie pamięci Pythona jest ograniczone niezależnie od rozmiaru 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

Należy zauważyć, że liczba bajtów zwróconych za pomocą iter_content nie jest dokładnie chunk_size; oczekuje się, że będzie to liczba losowa, która jest często znacznie większa i oczekuje się, że będzie inna w każdej iteracji.

Zobacz body-content-workflow i odpowiedź.iter_content dla dalszych informacji.

 725
Author: Roman Podlinov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2021-01-14 19:02:52

Jest znacznie łatwiejsze, jeśli używasz Response.raw oraz 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

Spowoduje to Przesyłanie pliku na dysk bez użycia nadmiernej pamięci, a kod jest prosty.

 338
Author: John Zwinck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-03-24 12:31:43

Nie do końca o co prosiła OP, ale... śmiesznie łatwo to zrobić z 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'

I widziałem, że plik rośnie, ale zużycie pamięci pozostało na 17 MB. Coś przeoczyłem?

 61
Author: x-yuri,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-15 09:14:18

Twój rozmiar kawałka może być zbyt duży, próbowałeś to upuścić-może 1024 bajty na raz? (możesz też użyć with do 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?

Brzmi to tak, jakby python nie spłukiwał danych do pliku, z innych więc pytania można spróbować f.flush() i os.fsync() wymusić zapis pliku i wolną pamięć;

    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())
 43
Author: danodonovan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:30

W oparciu o najbardziej upvoted komentarz Romana powyżej, oto moja realizacja, W tym mechanizm" download as "I" 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 ''
 5
Author: Ben Moskovitch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-07-05 17:15:42

Zamiast tego użyj wget modułu Pythona. Oto fragment

import wget
wget.download(url)
 2
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-10-19 04:09:45