Как скачать файл по ссылке в Python с прогресс-баром
Пошагово: скачиваем файл по HTTP частями, показываем прогресс через tqdm, обрабатываем обрыв соединения. Работает на Python 3.9+ и requests 2.x.

К концу статьи у вас будет скрипт, который скачивает файл по ссылке, пишет его на диск частями и показывает полосу прогресса с процентами и скоростью. Занимает это минут пятнадцать вместе с установкой библиотек. Разберём базовый вариант без зависимостей, удобный вариант с tqdm и обработку случая, когда сервер не сообщает размер файла.
Что нужно до начала
- Python 3.9 или новее (проверьте командой
python --version); - доступ в интернет и права на запись в папку, куда сохраняете файл;
- две библиотеки:
requestsдля загрузки иtqdmдля прогресс-бара.
Установите их одной командой:
pip install requests tqdm
После установки проверьте, что импорт работает — команда ниже не должна выдать ошибку:
python -c "import requests, tqdm; print(requests.__version__, tqdm.__version__)"
В консоли появятся две версии, например 2.32.3 4.66.5. Если вместо этого ModuleNotFoundError — библиотека не установилась, вернитесь к pip install.
Почему нельзя просто прочитать ответ целиком
Соблазн написать open(path, 'wb').write(requests.get(url).content) велик, и для мелкого файла это сработает. Но .content загружает весь ответ в оперативную память. Файл на 3 ГБ — это 3 ГБ в памяти до того, как хоть один байт попадёт на диск. Поэтому качаем потоком, кусками по несколько килобайт, и сразу пишем их в файл.
Ключ ко всему — stream=True
Параметр stream=True у requests.get говорит: не скачивай тело ответа сразу, дай мне читать его порциями через iter_content. Каждая порция — это bytes, который мы дописываем в открытый файл.
Шаг 1. Базовое скачивание без прогресса
Сначала убедимся, что файл вообще качается и корректно пишется на диск.
import requests
def download(url: str, dest: str) -> None:
# stream=True — тело ответа не грузится в память целиком
with requests.get(url, stream=True, timeout=30) as response:
response.raise_for_status() # бросит исключение при HTTP 4xx/5xx
with open(dest, "wb") as file:
# читаем и пишем блоками по 8 КБ
for chunk in response.iter_content(chunk_size=8192):
file.write(chunk)
if __name__ == "__main__":
download(
"https://speed.hetzner.de/100MB.bin",
"test.bin",
)
print("Готово")
Запустите. Файл test.bin должен появиться в текущей папке, а в консоли — строка Готово. Пока никакого прогресса, зато видно, что цепочка работает.
Шаг 2. Добавляем прогресс-бар через tqdm
Чтобы показать процент, нужно знать полный размер файла. Сервер обычно кладёт его в заголовок Content-Length. Читаем это число, отдаём в tqdm как total, и на каждый записанный блок двигаем полосу на длину этого блока.
import requests
from tqdm import tqdm
def download(url: str, dest: str) -> None:
with requests.get(url, stream=True, timeout=30) as response:
response.raise_for_status()
# размер файла в байтах; 0, если сервер не сообщил
total = int(response.headers.get("Content-Length", 0))
chunk_size = 8192
bar = tqdm(
total=total,
unit="B", # единица — байты
unit_scale=True, # 104857600 покажет как 100MB
desc=dest,
)
with open(dest, "wb") as file, bar:
for chunk in response.iter_content(chunk_size=chunk_size):
written = file.write(chunk)
bar.update(written) # двигаем полосу на реально записанное
if __name__ == "__main__":
download(
"https://speed.hetzner.de/100MB.bin",
"test.bin",
)
Теперь при запуске в консоли появится живая полоса вида test.bin: 42%|████ | 42.0M/100M [00:03<00:04, 13.2MB/s] — с процентами, скачанным объёмом, оставшимся временем и скоростью. Это всё tqdm считает сам из total и вызовов update.
Прогресс двигайте на число байт, которое реально вернулfile.write(), а не наlen(chunk). Разницы почти нет, но так вы считаете записанное, а не прочитанное — если запись частичная, полоса не соврёт.
Почему unit_scale важен
Без unit_scale=True вы увидите голые байты: 104857600/104857600. Мало кто в уме делит на 1048576. Флаг переводит числа в K, M, G автоматически.
Шаг 3. Что делать, когда размер неизвестен
Некоторые серверы не отдают Content-Length — например, при Transfer-Encoding: chunked или у динамически генерируемых файлов. Тогда total будет 0, и tqdm покажет неопределённый индикатор: он не знает, сколько осталось.
Здесь ничего специально писать не нужно — код из шага 2 уже это переживёт. При total=0 tqdm выводит счётчик скачанного объёма и скорость без процентов. Проверьте сами: замените ссылку на источник без Content-Length и посмотрите, как поменяется вид полосы.
Про сжатие: подводный камень с процентами
Если сервер отдаёт ответ с Content-Encoding: gzip, requests по умолчанию распаковывает его на лету при чтении через iter_content. При этом Content-Length в заголовке — размер сжатых данных, а на диск ляжет больше. Полоса тогда «перескочит» за 100%. Для типичных бинарных файлов (архивы, образы, видео) сжатия нет и проблемы не возникает. Если работаете с текстовыми ответами, где сжатие вероятно, — либо не показывайте проценты, либо считайте прогресс по прочитанным сырым байтам через response.raw. Это отдельная тема, здесь на ней не останавливаемся.
Если не получилось
requests.exceptions.ConnectTimeout/ReadTimeout— сервер не ответил за 30 секунд. Увеличьтеtimeoutили проверьте сеть. Для больших файлов задавайтеtimeoutкак кортеж:(10, 60)— 10 секунд на соединение, 60 на чтение блока.requests.exceptions.HTTPError: 404 Client Error— ссылка битая или файл удалён. Это результатraise_for_status(). Откройте URL в браузере и убедитесь, что файл существует.PermissionError: [Errno 13]— нет прав на запись в папку или файл открыт другой программой. Смените путьdestили закройте программу, держащую файл.- Полоса застряла и не двигается — сервер отдаёт данные очень медленно либо соединение подвисло. По истечении
timeoutчтения вы получитеReadTimeout. - Полоса ушла за 100% — см. раздел про gzip выше: сервер сжал ответ, а
Content-Lengthуказан для сжатого размера.
Устойчивость к обрыву: докачка
Базовый скрипт при обрыве начинает с нуля. Если файлы большие, добавьте докачку через заголовок Range: узнаёте размер уже скачанного, открываете файл в режиме ab (дописывание) и просите сервер отдать остаток.
import os
import requests
from tqdm import tqdm
def download_resumable(url: str, dest: str) -> None:
# сколько уже скачано, если файл существует
already = os.path.getsize(dest) if os.path.exists(dest) else 0
headers = {"Range": f"bytes={already}-"} if already else {}
with requests.get(url, headers=headers, stream=True, timeout=30) as response:
# 206 — сервер поддерживает докачку, 200 — отдаёт файл целиком
response.raise_for_status()
remaining = int(response.headers.get("Content-Length", 0))
total = already + remaining
mode = "ab" if already and response.status_code == 206 else "wb"
if mode == "wb":
already = 0 # докачка не поддержана — качаем заново
bar = tqdm(total=total, initial=already, unit="B",
unit_scale=True, desc=dest)
with open(dest, mode) as file, bar:
for chunk in response.iter_content(chunk_size=8192):
bar.update(file.write(chunk))
Проверить докачку просто: прервите скачивание на середине через Ctrl+C, запустите скрипт снова. Полоса стартует не с нуля, а с уже скачанного объёма — при условии, что сервер вернул статус 206.
Таблица: какой вариант когда брать
| Вариант | Когда подходит | Чем платите |
|---|---|---|
requests.get().content | Файл до нескольких МБ, разовый скрипт | Весь файл в памяти, нет прогресса |
| Поток + tqdm (шаг 2) | Обычная задача: файлы любого размера, нужен прогресс | Чуть больше кода, зависимость от tqdm |
| Докачка через Range | Большие файлы, нестабильная сеть | Сложнее логика, нужна поддержка Range на сервере |
Что дальше
Скрипт готов к работе. Если будете качать много файлов подряд, посмотрите в сторону requests.Session — она переиспользует TCP-соединение и ускоряет серию загрузок. Для параллельного скачивания нескольких файлов с общей полосой прогресса у tqdm есть поддержка нескольких баров, а сами загрузки удобно раскидать по concurrent.futures.ThreadPoolExecutor. Если задача — качать асинхронно сотнями, это уже httpx или aiohttp, где тот же принцип потокового чтения, но через async for.
Дефектный акт по фото: Осмотр по фотографиям: классификация дефектов, ссылки на нормы, требования к устранению. Готовый акт в Word.
Спросить за 80 ₽Источники: Requests: Streaming Requests / Advanced Usage, tqdm — документация и примеры
Частые вопросы
А если у меня requests другой версии?
Полоса прогресса не появляется, хотя файл качается. Почему?
Можно ли обойтись без tqdm и не ставить лишнюю зависимость?
Прогресс перескакивает за 100%. Что не так?
Как безопасно скачивать в многопоточном скрипте?
Что делать, если сервер обрывает соединение на середине большого файла?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.