Изображения являются одними из самых тяжёлых и полезных файлов в интернете: фотографии товаров для каталога, графики для исследовательского набора данных, ресурсы для конвейера машинного обучения. Когда вам нужно больше, чем горстка файлов, щелчок «Сохранить изображение как» перестаёт быть вариантом, а короткий скрипт Python справляется с задачей за секунды, а не за целый день.

В этом руководстве рассматриваются шесть практических способов загрузки изображений с помощью Python: от одного файла с помощью requests до извлечения всех изображений со страницы с BeautifulSoup, потоковой загрузки больших файлов по частям, организации сохранённых файлов и доступа к защищённым источникам через Crawlbase Crawling API. Каждый фрагмент кода реален и запускается как написан, поэтому вы можете скопировать любой из них и адаптировать под свои цели.

Что вы создадите

К концу у вас будет небольшой набор функций, охватывающих типичные случаи, плюс короткий скрипт, объединяющий их.

  • Single download. Получить одно изображение по URL и записать его на диск с помощью requests.
  • Standard-library download. Сделать то же самое без сторонних пакетов, используя urllib.request.
  • Page scrape. Найти каждый тег <img> на странице с помощью BeautifulSoup и загрузить каждый источник.
  • Chunked streaming. Сохранять большие файлы по частям, чтобы использование памяти оставалось постоянным.
  • Naming and folders. Формировать безопасные имена файлов и сортировать загрузки по директориям.
  • Protected sources. Получать изображения за рендерингом или защитой от ботов через Crawling API.

Почему простая загрузка не работает на некоторых сайтах

Загрузка изображения в простейшем виде, это один HTTP GET. Это отлично работает для статических файлов, доступных по предсказуемому URL. Проблемы начинаются на реальных сайтах. Некоторые страницы создают свою сетку изображений с помощью JavaScript после загрузки исходного HTML, поэтому обычный запрос возвращает разметку без тегов изображений. Другие находятся за защитой от ботов, которая проверяет или блокирует запросы с IP-адресов дата-центров или всего, что не выглядит как настоящий браузер, и вы получаете 403 или HTML-страницу ошибки вместо JPEG.

Для простых файлов пяти методов ниже достаточно. Для сложных случаев последний метод маршрутизирует запрос через слой рендеринга и пул надёжных IP-адресов, чтобы файл вернулся нетронутым. Сначала мы рассматриваем простой путь, поскольку большинство загрузок никогда не требуют большего.

Предварительные требования

Для следования руководству вам не нужно многого.

Python версии 3.8 или новее. Проверьте свою версию командой python --version. Если у вас её нет, установите с python.org.

Базовые знания Python. Вы должны уметь запускать скрипты и устанавливать пакеты с помощью pip. Достаточно знания функций, циклов и оператора with.

Аккаунт Crawlbase (только для последнего метода). Первые пять методов используют только requests, urllib и BeautifulSoup. Для метода с защищёнными источниками вам понадобится бесплатный аккаунт Crawlbase и его API-токен.

Настройка проекта

Создайте виртуальное окружение, чтобы зависимости проекта оставались изолированными, а затем установите две сторонние библиотеки, используемые в этом руководстве.

bash
python --version

python -m venv image_env
source image_env/bin/activate

pip install requests beautifulsoup4

В Windows активируйте окружение командой image_env\Scripts\activate вместо строки с source. requests, это HTTP-клиент, получающий каждый файл, а beautifulsoup4 парсит HTML страницы, позволяя находить теги изображений. urllib, os и hashlib входят в стандартную библиотеку и не требуют установки.

Способ 1: Загрузка одного изображения с requests

Самый распространённый случай, это одно изображение по заданному URL. Отправьте GET, убедитесь, что ответ пришёл как изображение, и запишите байты в файл в бинарном режиме. Проверка кода состояния и типа контента перед записью предотвращает сохранение HTML-страницы ошибки под именем .jpg.

python
import requests

url = "https://www.python.org/static/img/python-logo.png"
headers = {"User-Agent": "Mozilla/5.0 (image downloader)"}

response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200 and "image" in response.headers.get("Content-Type", ""):
    with open("python-logo.png", "wb") as f:
        f.write(response.content)
    print("Saved python-logo.png")
else:
    print(f"Skipped: {response.status_code} {response.headers.get('Content-Type')}")

Здесь важны три детали. Файл открывается с флагом "wb" (запись в бинарном режиме), поскольку данные изображения являются байтами, а не текстом, и запись в текстовом режиме повредит файл. Проверка Content-Type подтверждает, что сервер действительно вернул изображение, а не страницу ошибки со статусом 200. Параметр timeout предотвращает зависание скрипта на зависшем сервере. Запустите это, и вы должны увидеть Saved python-logo.png с настоящим PNG рядом со скриптом. Это рабочая загрузка.

Crawlbase Crawling API

Приведённая выше одиночная загрузка работает, когда файл доступен по обычному URL. Как только страница скрывает свои изображения за JavaScript или блокирует запросы из дата-центров, этот GET возвращает страницу ошибки вместо байтов. Crawling API отрисовывает страницу в настоящем браузере и ротирует резидентские IP-адреса на стороне сервера, затем возвращает готовый ответ, поэтому вы не строите собственный флот headless-браузеров и пул прокси. Попробуйте его на бесплатном тарифе перед созданием этой инфраструктуры самостоятельно.

Способ 2: Загрузка с urllib из стандартной библиотеки

Если вы предпочитаете не добавлять зависимость, стандартная библиотека справится с той же задачей. urllib.request поставляется с Python, поэтому такой подход не требует ничего устанавливать. Установка User-Agent через объект Request помогает с серверами, отклоняющими стандартный агент urllib.

python
import urllib.request

url = "https://www.python.org/static/img/python-logo.png"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (image downloader)"})

with urllib.request.urlopen(req, timeout=10) as resp, open("logo_urllib.png", "wb") as f:
    f.write(resp.read())

print("Saved logo_urllib.png")

Оператор with открывает соединение и выходной файл вместе и аккуратно закрывает их по завершении блока, даже если в процессе записи возникает ошибка. Существует также однострочный вариант: urllib.request.urlretrieve(url, "logo.png"), удобный для быстрых скриптов, но не дающий контроля над заголовками или обработкой ошибок, поэтому приведённая выше явная форма безопаснее по умолчанию. Оба варианта, requests и urllib, записывают одни и те же байты на диск; у requests просто более удобный API, поэтому большая часть остального руководства использует именно его.

Способ 3: Загрузка всех изображений страницы с BeautifulSoup

Загрузить один файл легко. Реальная работа, это автоматически извлечь все изображения со страницы. Паттерн состоит из двух шагов: получить HTML страницы, затем разобрать его с BeautifulSoup для сбора src каждого тега <img> и наконец пройтись по этим URL, повторно используя логику одиночной загрузки из способа 1.

python
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin

page_url = "https://en.wikipedia.org/wiki/Python_(programming_language)"
headers = {"User-Agent": "Mozilla/5.0 (image downloader)"}

page = requests.get(page_url, headers=headers, timeout=10)
soup = BeautifulSoup(page.text, "html.parser")

image_urls = []
for img in soup.select("img"):
    src = img.get("src")
    if src:
        image_urls.append(urljoin(page_url, src))

print(f"Found {len(image_urls)} images")

Здесь стоит остановиться на двух моментах. Селектор img захватывает каждый тег изображения на странице, а чтение img.get("src") вместо img["src"] возвращает None для любого тега без атрибута, а не выбрасывает ошибку. Другой ключевой момент, это urljoin: источники изображений часто являются относительными путями вроде /images/photo.jpg, и объединение каждого из них с URL страницы превращает его в полный, загружаемый адрес. Для более подробного руководства по выбору элементов таким образом см. как использовать BeautifulSoup в Python.

Имея список абсолютных URL, цикл загрузки повторно использует паттерн бинарной записи из способа 1 и даёт каждому файлу собственное имя.

python
import os

os.makedirs("downloads", exist_ok=True)

for i, img_url in enumerate(image_urls):
    try:
        r = requests.get(img_url, headers=headers, timeout=10)
        if r.status_code == 200 and "image" in r.headers.get("Content-Type", ""):
            path = os.path.join("downloads", f"image_{i}.jpg")
            with open(path, "wb") as f:
                f.write(r.content)
    except requests.RequestException as e:
        print(f"Failed {img_url}: {e}")

Блок try/except вокруг каждого запроса является обязательным, как только вы загружаете много файлов. Из десятков изображений одно рано или поздно истечёт по тайм-ауту, вернёт цикл перенаправления или исчезнет, и перехват requests.RequestException позволяет циклу пропустить плохой файл и продолжить работу, а не аварийно завершиться на тридцатом файле. Вызов os.makedirs(..., exist_ok=True) создаёт выходную папку один раз и ничего не делает, если она уже существует.

Способ 4: Потоковая загрузка больших файлов по частям

Чтение response.content загружает весь файл в память перед записью. Для логотипа это нормально, но расточительно для фотографии высокого разрешения или многомегабайтного ресурса, и может исчерпать память при последовательной загрузке многих больших файлов. Потоковая передача ответа и запись фиксированными по размеру частями поддерживает постоянное использование памяти независимо от размера файла.

python
def download_stream(url, path, chunk_size=8192):
    with requests.get(url, headers=headers, stream=True, timeout=30) as r:
        r.raise_for_status()
        with open(path, "wb") as f:
            for chunk in r.iter_content(chunk_size=chunk_size):
                f.write(chunk)
    return path

download_stream(
    "https://upload.wikimedia.org/wikipedia/commons/c/c3/Python-logo-notext.svg",
    "downloads/large_logo.svg",
)

Передача stream=True указывает requests не загружать тело заранее; вместо этого iter_content извлекает его порциями по chunk_size байт и записывает каждую порцию прямо на диск. Пиковое использование памяти остаётся примерно равным размеру одной порции, здесь восемь килобайт, независимо от размера файла. raise_for_status() превращает ответ 4xx или 5xx в исключение, чтобы вы не сохраняли тело ошибки молча, а внешний блок with на запросе обеспечивает освобождение соединения после записи файла.

Способ 5: Именование и организация файлов

Сохранять всё как image_0.jpg, image_1.jpg работает, но при этом теряются исходные имена файлов и расширения, что важно при смешивании PNG, JPEG и SVG. Небольшой вспомогательный метод формирует чистое имя из URL и использует хеш содержимого как запасной вариант, когда в URL нет подходящего имени, что гарантирует уникальность и предотвращает перезапись двух файлов с одинаковыми именами.

python
import os
import hashlib
from urllib.parse import urlparse

def filename_for(url, content, folder="downloads"):
    name = os.path.basename(urlparse(url).path)
    if not name or "." not in name:
        digest = hashlib.md5(content).hexdigest()[:12]
        name = f"{digest}.jpg"
    return os.path.join(folder, name)

# Example: turn a messy URL into a tidy path
r = requests.get(image_urls[0], headers=headers, timeout=10)
print(filename_for(image_urls[0], r.content))

urlparse(...).path убирает строки запроса и фрагменты из URL, а os.path.basename берёт только последний сегмент, поэтому .../photo.jpg?size=large становится photo.jpg. Когда в URL нет настоящего имени файла, MD5-хеш байтов файла даёт короткое, стабильное, устойчивое к коллизиям имя. Тот же хеш является простейшим способом обнаружения дубликатов: два одинаковых изображения дают одинаковый дайджест, поэтому вы можете пропустить файл, который уже сохранили, ещё до его записи.

Sorting into subfolders

Если вы загружаете с нескольких страниц или категорий одновременно, передавайте разный folder для каждого источника, чтобы файлы попадали в отдельные директории. В сочетании с os.makedirs(folder, exist_ok=True) это позволяет организовать большой запуск на диске и легко найти нужное изображение позже без сканирования одной огромной папки.

Способ 6: Загрузка с защищённых источников через Crawling API

Пять перечисленных выше методов охватывают любое изображение, доступное с помощью обычного запроса. Некоторые сайты не столь отзывчивы. Страница может создавать сетку изображений с помощью JavaScript, поэтому запрос не возвращает тегов <img>, или хост изображений может блокировать IP-адреса дата-центров и возвращать 403 вместо файла. Вы можете решить обе проблемы самостоятельно, запустив headless-браузер и поддерживая пул ротируемых резидентских прокси, но создание и поддержка этого занимают большую часть инженерных усилий и не имеют ничего общего с нужными вам изображениями.

Crawling API объединяет рендеринг и ротацию IP в один запрос. Вы устанавливаете официальный клиент, затем маршрутизируете через него получение страницы; возвращаемый HTML полностью отрисован, поэтому парсинг BeautifulSoup из способа 3 работает без изменений.

bash
pip install crawlbase
python
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup
from urllib.parse import urljoin

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})
page_url = "https://example.com/gallery"

result = api.get(page_url, {"ajax_wait": "true", "page_wait": 5000})
html = result["body"].decode("utf-8") if result["status_code"] == 200 else None

# Same parsing as Tip 3, now against rendered HTML
soup = BeautifulSoup(html, "html.parser")
image_urls = [urljoin(page_url, img["src"]) for img in soup.select("img[src]")]
print(f"Found {len(image_urls)} images on the rendered page")

Два параметра ожидания важны для клиентски-рендеримой галереи. ajax_wait указывает API ждать завершения загрузки асинхронного контента, а page_wait выдерживает фиксированное количество миллисекунд после загрузки, чтобы запоздавшие изображения появились до захвата. Используйте JavaScript-токен для сайтов, формирующих сетку в браузере; для статических хостов изображений обычный токен быстрее. После заполнения image_urls передайте его напрямую во вспомогательный метод потоковой загрузки download_stream из способа 4. Если вам нужна только ротация IP, а не рендеринг, например когда HTML страницы нормальный, но хост изображений вас блокирует, Smart AI Proxy маршрутизирует обычный трафик requests через тот же надёжный пул с однострочным изменением настроек прокси.

Как выглядит результат

После скрапинга страницы небольшой манифест сохранённых файлов полезен для аудита и пропуска дубликатов при следующем запуске. Запись одной записи на файл даёт вам структурированный журнал рядом с самими изображениями.

json
[
  {
    "source_url": "https://example.com/img/photo-01.jpg",
    "saved_as": "downloads/photo-01.jpg",
    "content_type": "image/jpeg",
    "bytes": 184213
  },
  {
    "source_url": "https://example.com/img/diagram.svg",
    "saved_as": "downloads/diagram.svg",
    "content_type": "image/svg+xml",
    "bytes": 9042
  }
]

Каждая запись связывает исходный URL с локальным файлом, указывает тип контента (чтобы знать реальный формат) и записывает размер в байтах. Сохранение source_url позволяет последующему запуску проверить, что у вас уже есть, прежде чем загружать повторно.

Масштабирование на множество изображений

Для нескольких десятков файлов последовательный цикл из способа 3 подходит. При загрузке тысяч файлов скачивание по одному является узким местом, поскольку большую часть времени занимает ожидание сети, а не процессор. Пул потоков позволяет нескольким загрузкам выполняться одновременно, оставаясь при этом намного проще, чем полноценный асинхронный код.

python
from concurrent.futures import ThreadPoolExecutor

def save_one(url):
    try:
        r = requests.get(url, headers=headers, timeout=15)
        if r.status_code == 200 and "image" in r.headers.get("Content-Type", ""):
            path = filename_for(url, r.content)
            with open(path, "wb") as f:
                f.write(r.content)
            return path
    except requests.RequestException:
        return None

with ThreadPoolExecutor(max_workers=8) as pool:
    saved = list(pool.map(save_one, image_urls))

print(f"Saved {len([p for p in saved if p])} of {len(image_urls)} images")

Восемь воркеров, это разумная отправная точка; слишком высокое значение грозит перегрузкой хоста, что одновременно невежливо и быстро ведёт к ограничению частоты запросов. Каждый вызов save_one самодостаточен и поглощает собственные ошибки, так что одна ошибка никогда не топит всю партию. Если вы загружаете с защищённого источника в таком объёме, маршрутизируйте save_one через Crawling API или Smart AI Proxy, чтобы ротируемые IP поглощали нагрузку вместо вашего единственного адреса. Подробнее о поддержании работоспособности больших запусков см. в статье как скрапить сайты, не попадая в блок.

Ответственная загрузка изображений

Изображения не являются свободно доступными данными; они представляют собой результат творческого труда, и почти каждое изображение в интернете защищено авторским правом того, кто его создал. Возможность загрузить файл не означает права его использовать. Прежде чем направлять скрипт на сайт, прочитайте его условия использования и проверьте его robots.txt, поддерживайте объём запросов достаточно низким, чтобы не нагружать сервер, и загружайте только то, на использование чего у вас есть право. Фотографии за логином, личные изображения и контент, на который у вас нет лицензии, являются недоступными.

Та же осторожность применима к тому, что вы делаете после. Не распространяйте чужие изображения как свои и не используйте защищённые авторским правом медиаматериалы для обучения модели или набора данных без разрешения правообладателя. Когда сайт предлагает официальный API или путь лицензирования для своих медиаматериалов, используйте его: это самый чистый способ получить изображения, которыми вы можете пользоваться на законных основаниях, и обычно он поставляется с условиями, точно определяющими, что с ними можно делать.

Итоги

Ключевые выводы

  • Бинарный режим обязателен. Всегда открывайте выходной файл с флагом "wb" и проверяйте, что ответ является изображением, перед записью, чтобы никогда не сохранять страницу ошибки как JPEG.
  • Сначала найти, потом загрузить для целых страниц. Разберите HTML с BeautifulSoup для сбора каждого источника <img>, преобразуйте относительные пути в абсолютные URL с помощью urljoin, затем загружайте каждый URL в защищённом цикле.
  • Потоковая передача больших файлов по частям. Используйте stream=True и iter_content, чтобы пиковое использование памяти оставалось постоянным независимо от размера файла.
  • Именуйте и дедуплицируйте намеренно. Формируйте чистые имена файлов из URL, используйте хеш содержимого как запасной вариант и применяйте этот хеш для пропуска дубликатов.
  • Получайте доступ к защищённым источникам через API. Когда страница отрисовывает изображения в JavaScript или блокирует ваш IP, Crawling API возвращает отрисованный HTML, поэтому ваш существующий парсер и код загрузки продолжают работать.

Часто задаваемые вопросы

Как проще всего загрузить изображение в Python?

Отправьте GET с помощью requests, затем запишите response.content в файл, открытый в бинарном режиме ("wb"). Это три строки для одного файла. Сначала проверьте код состояния и заголовок Content-Type, чтобы сохранять только реальные данные изображения, а не HTML-страницу ошибки со статусом 200.

Использовать requests или urllib?

Оба загружают одни и те же байты. У requests более чистый API, более простая работа с заголовками и встроенная потоковая передача, поэтому большинство кода использует именно его. Обращайтесь к urllib.request, когда хотите обойтись без сторонних зависимостей, поскольку он поставляется с Python. Логика загрузки в остальном идентична.

Как загрузить все изображения с веб-страницы?

Получите HTML страницы, разберите его с BeautifulSoup и соберите src каждого тега <img>. Преобразуйте относительные пути в абсолютные URL с помощью urljoin, затем пройдитесь по списку и загрузите каждый файл по паттерну одиночного изображения. Оберните каждую загрузку в try/except, чтобы один неверный URL не остановил всю партию, как показано в способе 3.

Почему загруженный файл открывается как повреждённое изображение?

Обычно одна из двух причин. Либо файл был открыт в текстовом режиме вместо бинарного, что повреждает байты, поэтому убедитесь, что используете "wb". Либо сервер вернул HTML-страницу ошибки, а не изображение, вот почему важно проверять код состояния и Content-Type перед записью. На страницах с активным JavaScript изображение может вообще отсутствовать в исходном HTML, и тогда необходим этап рендеринга.

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

Отправляйте реалистичный User-Agent, контролируйте темп запросов с короткой задержкой и избегайте перегрузки одного хоста множеством параллельных потоков. При больших объёмах также нужны IP-адреса, воспринимаемые как адреса реальных пользователей, что одна машина обеспечить не может. Маршрутизация через ротируемые резидентские IP, через Crawling API или Smart AI Proxy, позволяет крупным запускам загрузки изображений не превышать лимиты частоты.

Как избежать двойной загрузки одного и того же изображения?

Вычисляйте хеш байтов каждого файла с помощью hashlib и храните уже просмотренные дайджесты в множестве. Перед записью нового файла проверьте, есть ли его хеш уже в множестве; если есть, пропустите его. Тот же дайджест служит надёжным, устойчивым к коллизиям именем файла, когда в URL нет подходящего имени.

Начать создавать

Обходите любой сайт в масштабе, без борьбы с инфраструктурой.

Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга