Завантажте великий файл у python із запитами


398

Запити - це справді приємна бібліотека. Я хотів би використовувати його для завантаження великих файлів (> 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, ви можете знайти його тут . Він підтримує багатопотокові та повторно підключаються (робить моніторні з'єднання), а також налаштовує параметри сокета для завдання завантаження.

Відповіді:


650

За допомогою наступного потокового коду використання пам'яті 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/isissue/2043 ). Чи можете ви оновити чи видалити свої коментарі, тому що люди можуть подумати, що є проблеми з кодом для файлів, більших 1024Mb
Роман Подлінов

8
chunk_sizeмає вирішальне значення. за замовчуванням це 1 (1 байт). це означає, що за 1 Мб вона зробить 1 мільйон ітерацій. docs.python-requests.org/en/latest/api/…
Едуард Гамонал

4
f.flush()здається непотрібним. Що ви намагаєтеся досягти, використовуючи його? (використання вашої пам’яті не буде 1,5 Гб, якщо ви її скинете). f.write(b'')(якщо iter_content()може повернути порожній рядок) має бути нешкідливим, а тому if chunkможе бути і кинутим.
jfs

11
@RomanPodlinov: f.flush()не передає дані на фізичний диск. Він передає дані в ОС. Зазвичай цього достатньо, якщо не відбувається відключення живлення. f.flush()робить код повільніше тут без жодної причини. Промивання відбувається, коли буфер відповідних файлів (всередині програми) заповнений. Якщо вам потрібні частіші записи; передати параметр buf.size в open().
jfs

9
Не забудьте r.close()
перервати

271

Це набагато простіше, якщо ви використовуєте 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
Зауважте, що вам може знадобитися коригування під час передавання потокових відповідей за випуск 2155.
ChrisP

32
ЦЕ повинна бути правильною відповіддю! Загальноприйнятий відповідь отримує вас до 2-3MB / с. За допомогою copyfileobj ви отримаєте ~ 40MB / s. Завантаження завитків (ті ж машини, однаковий URL тощо) з ~ 50-55 Мб / с.
visoft

24
Щоб переконатися, що з'єднання "Запити" звільнено, ви можете використовувати другий (вкладений) withблок, щоб зробити запит:with requests.get(url, stream=True) as r:
Christian Long

7
@ChristianLong: Це правда, але зовсім недавно, оскільки функція для підтримки with requests.get()була об’єднана лише 2017-06-07! Ваша пропозиція розумна для людей, які мають запити 2.18.0 або новіших версій. Ref: github.com/requests/requests/isissue/4136
Джон Звінк

4
@EricCousineau Ви можете зафіксувати цю поведінку, замінивши readметод:response.raw.read = functools.partial(response.raw.read, decode_content=True)
Nuno André

54

Не зовсім те, що просили ОП, але ... це смішно легко зробити з 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 vs рішення shutil.copyfileobjз найбільшою кількістю голосів, дивіться мої та інші коментарі там
citynorman

41

Ваш розмір може бути занадто великим, ви намагалися скинути це - можливо, 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 не перемикає дані у файл, з інших запитань ТА можна спробувати 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())

1
Я використовую System Monitor в Kubuntu. Це показує мені, що пам’ять процесів python збільшується (до 1,5 гб від 25 кбіт).
Роман Підлінов

Цей запам’ятовування пам’яті смокче, можливо, f.flush(); os.fsync()може змусити записувати пам'ять безкоштовно.
danodonovan

2
цеos.fsync(f.fileno())
sebdelsol

29
Вам потрібно використовувати stream = True у виклику request.get (). Ось що викликає роздуття пам’яті.
Хата8

1
другорядний помилок: ви пропускаєте двокрапку (':') післяdef DownloadFile(url)
Обрі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.