Скачать большой файл в Python с запросами

402

Requests - это действительно хорошая библиотека. Я хотел бы использовать его для загрузки больших файлов (> 1 ГБ). Проблема в том, что невозможно сохранить весь файл в памяти, мне нужно прочитать его порциями. И это проблема со следующим кодом

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 

По какой-то причине это не работает таким образом. Он по-прежнему загружает ответ в память, прежде чем сохранить его в файл.

ОБНОВИТЬ

Если вам нужен маленький клиент (Python 2.x /3.x), который может загружать большие файлы с FTP, вы можете найти его здесь . Он поддерживает многопоточность и повторное соединение (он контролирует соединения), а также настраивает параметры сокета для задачи загрузки.

Роман Подлинов
источник

Ответы:

653

При использовании следующего потокового кода использование памяти Python ограничено независимо от размера загружаемого файла:

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

Обратите внимание, что число возвращаемых байтов iter_contentне совсем chunk_size; ожидается, что это будет случайное число, которое часто намного больше, и ожидается, что оно будет различным на каждой итерации.

См. Https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow и https://requests.readthedocs.io/en/latest/api/#requests.Response.iter_content для получения дополнительной информации. ссылка.

Роман Подлинов
источник
9
@Shuman Как я вижу, вы решили проблему, когда переключились с http: // на https: // ( github.com/kennethreitz/requests/issues/2043 ). Не могли бы вы обновить или удалить свои комментарии, потому что люди могут подумать, что существуют проблемы с кодом для файлов размером более 1024 Мб
Роман Подлинов
8
chunk_sizeимеет решающее значение. по умолчанию это 1 (1 байт). это означает, что для 1 МБ это будет 1 миллион итераций. docs.python-requests.org/en/latest/api/…
Эдуард Гамонал
4
f.flush()кажется ненужным Что вы пытаетесь достичь с помощью этого? (использование вашей памяти не будет 1,5 ГБ, если вы уроните ее). f.write(b'')(if iter_content()может вернуть пустую строку) должен быть безвредным и, следовательно, if chunkможет быть также отброшен.
JFS
11
@RomanPodlinov: f.flush()не сбрасывает данные на физический диск. Он передает данные в ОС. Обычно этого достаточно, если не происходит сбоя питания. f.flush()делает код медленнее здесь без причины. Сброс происходит, когда соответствующий файловый буфер (внутри приложения) заполнен. Если вам нужны более частые записи; передать параметр buf.size в open().
Jfs
9
Не забудьте закрыть соединение сr.close()
0xcaff
274

Это намного проще, если вы используете Response.rawи 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

Это потоковое файл на диск без использования чрезмерной памяти, и код прост.

Джон Цвинк
источник
10
Обратите внимание, что вам может понадобиться настроить при потоковой передаче ответов в формате
gzip по
32
ЭТО должен быть правильный ответ! Общепринятый ответ получает вас до 2-3MB / с. Использование copyfileobj дает вам ~ 40 МБ / с. Загрузка Curl (те же машины, тот же URL и т. Д.) С ~ 50-55 МБ / с.
visoft
24
Чтобы убедиться, что соединение Requests освобождается, вы можете использовать второй (вложенный) withблок для выполнения запроса:with requests.get(url, stream=True) as r:
Christian Long
7
@ChristianLong: Это правда, но совсем недавно, так как функция поддержки with requests.get()была объединена только 2017-06-07! Ваше предложение разумно для людей, у которых есть запросы 2.18.0 или более поздние. Ссылка: github.com/requests/requests/issues/4136
Джон Цвинк,
4
@EricCousineau Вы можете исправить это поведение, заменив readметод:response.raw.read = functools.partial(response.raw.read, decode_content=True)
Нуно Андре
54

Не совсем то, о чем спрашивал OP, но ... это до смешного легко сделать с помощью 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)

Или так, если вы хотите сохранить его во временный файл:

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)

Я наблюдал за процессом:

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

И я увидел, что файл растет, но использование памяти осталось на 17 МБ. Я что-то пропустил?

х-юри
источник
2
Для Python 2.x используйтеfrom urllib import urlretrieve
Вадим Котов
Это приводит к низкой скорости загрузки ...
citynorman
@citynorman Можете ли вы уточнить? По сравнению с каким решением? Почему?
x-yuri
@ x-yuri против решения shutil.copyfileobjс наибольшим количеством голосов, см. мои и другие комментарии там
citynorman
42

Ваш размер куска может быть слишком большим, вы пытались сбросить его - может быть, 1024 байта за раз? (также вы можете использовать, withчтобы привести в порядок синтаксис)

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 

Кстати, как вы делаете вывод, что ответ был загружен в память?

Звучит так, как будто python не сбрасывает данные в файл, из других вопросов SO, которые вы могли бы попробовать f.flush()и os.fsync()заставить файл записать и освободить память;

    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
источник
1
Я использую системный монитор в Kubuntu. Это показывает, что память процесса Python увеличивается (до 1,5 ГБ с 25 КБ).
Роман Подлинов
Это раздувание памяти - отстой, возможно, f.flush(); os.fsync()может заставить запись освободить память.
Данодонован
2
этоos.fsync(f.fileno())
sebdelsol
29
Вам нужно использовать stream = True в вызове запросы. Это то, что вызывает раздувание памяти.
Hut8
1
мелкая опечатка: вы пропустили двоеточие (':') послеdef DownloadFile(url)
Обри