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

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

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

Python-скрипт, который принимает публичный URL поиска DeviantArt, получает отрендеренный HTML через Crawling API и извлекает структурированную запись для каждого произведения в сетке результатов. В качестве примера используем поиск по ключевому слову и извлекаем из каждой карточки результата следующие поля:

  • Title заголовок произведения, например "Owl #6".
  • Artist имя пользователя автора на DeviantArt, считанное из ссылки на deviation.
  • Image URL источник миниатюры, отображаемой в сетке результатов.
  • Page URL ссылка на страницу отдельного deviation.
  • Favourites публичное количество добавлений в избранное, если карточка раскрывает его.
  • Views публичное количество просмотров, если карточка раскрывает его.

Почему обычный запрос не работает на DeviantArt

Если отправить запрос к URL поиска DeviantArt с помощью обычного HTTP-клиента, вы получите ответ со статусом 200, но без каких-либо данных произведений в теле. Этому мешают два фактора. Во-первых, DeviantArt строит сетку результатов в браузере с помощью JavaScript: исходный HTML является оболочкой, которая заполняется только после выполнения скриптов страницы. Во-вторых, платформа быстро обнаруживает автоматизированный трафик: IP-адреса дата-центров и паттерны запросов, непохожие на реальный браузер, проверяются или блокируются ещё до получения отрендеренных миниатюр.

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

Зачем нужен JS-токен

Crawlbase предлагает два типа токенов. Обычный токен получает статический HTML; JavaScript (JS) токен сначала рендерит страницу в реальном браузере. Сетка поиска DeviantArt отображается на стороне клиента, поэтому здесь нужен JS-токен. Использование обычного токена вернёт ту же пустую оболочку, что и обычный запрос, и в ней не будет ничего для парсинга. Вы можете начать с до 20 000 бесплатных запросов без необходимости вводить данные кредитной карты.

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

Перед написанием кода необходимо выполнить несколько условий. Это не займёт много времени.

Базовые знания Python. Вы должны уметь писать и запускать Python-скрипты, а также устанавливать пакеты через pip. Если BeautifulSoup для вас нов, наше руководство по использованию BeautifulSoup в Python охватывает основы парсинга, которые предполагает этот туториал.

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

Аккаунт Crawlbase и JS-токен. Зарегистрируйтесь, откройте панель управления и скопируйте JavaScript (JS) токен со страницы документации аккаунта. Относитесь к токену как к паролю: он аутентифицирует ваши запросы, поэтому не добавляйте его в систему контроля версий.

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

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

bash
python --version

python -m venv deviantart_env
source deviantart_env/bin/activate

pip install crawlbase beautifulsoup4 requests

В Windows активируйте окружение командой deviantart_env\Scripts\activate вместо строки source. Три зависимости выполняют основную работу: crawlbase является официальным клиентом для Crawling API, beautifulsoup4 парсит возвращаемый HTML, позволяя извлекать отдельные поля по CSS-селектору, а requests потоково загружает файлы изображений на диск на последнем шаге.

Шаг 1: Получение отрендеренной страницы поиска

Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его JS-токеном и запросите URL поиска, построенный на основе ключевого слова. Путь поиска DeviantArt выглядит как /search?q=KEYWORD, поэтому запрос "fantasy" даёт сетку публичных результатов. Проверка статуса перед парсингом позволяет выявлять ошибки явно, а не незаметно.

python
from crawlbase import CrawlingAPI

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})

base_url = "https://www.deviantart.com"

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("latin1")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    keyword = "fantasy"
    search_url = f"{base_url}/search?q={keyword}"
    html = crawl(search_url)
    print(html[:500] if html else "No HTML returned")

Две опции ожидания важны для клиентски-рендеренного ресурса. ajax_wait ждёт завершения загрузки асинхронного контента, а page_wait задерживает фиксированное количество миллисекунд после загрузки, чтобы поздно рендеримые миниатюры появились до захвата страницы. Пять секунд являются разумной отправной точкой; увеличьте значение, если сетка возвращается пустой. Тело декодируется как latin1, чтобы необработанные байты проходили без искажений. Запустите скрипт командой python scraper.py, и вы должны увидеть реальную разметку произведений, а не пустую оболочку обычного запроса. Это подтверждает работу рендеринга до написания первого селектора.

Crawlbase DeviantArt Scraper

Сетка поиска DeviantArt требует отрендеренной страницы за доверенным IP в одном вызове. Crawling API принимает JS-токен, запускает страницу в реальном браузере, ротирует резидентные IP на стороне сервера и возвращает вам готовый HTML, избавляя от необходимости самостоятельно управлять флотом headless-браузеров и пулом прокси. Начните с публичной страницы поиска на бесплатном тарифе.

Шаг 2: Парсинг карточек произведений с BeautifulSoup

Получив отрендеренный HTML, загрузите его в BeautifulSoup и извлеките каждый результат по его селектору. DeviantArt оборачивает каждую миниатюру в ссылку на deviation, поэтому выбирайте все такие ссылки сразу, затем считывайте те же поля с каждой из них. Нажмите правой кнопкой мыши на миниатюру в браузере, выберите "Inspect" (Просмотр кода), и вы увидите структуру, подобную приведённой ниже.

html
<a data-hook="deviation_link"
   href="https://www.deviantart.com/siobhan-o-wisp/art/Owl-6-925596734"
   aria-label="Owl #6 by Siobhan-o-wisp, visual art">
  <div data-testid="thumb" typeof="ImageObject">
    <img alt="Owl #6" src="src_url_here" property="contentUrl" />
  </div>
</a>

Это даёт всё необходимое из одной карточки. Якорь содержит URL страницы deviation в href, а заголовок и художника в aria-label; вложенный img содержит миниатюру в src. CSS-селектор для миниатюры, a[data-hook="deviation_link"] img[property="contentUrl"], а заголовок произведения доступен через атрибут alt изображения. Ниже приведён парсер.

python
from bs4 import BeautifulSoup

def artist_from_url(page_url):
    # DeviantArt page URLs look like /<username>/art/<slug>
    parts = page_url.split("/")
    return parts[3] if len(parts) > 3 else None

def count_for(link, label):
    el = link.select_one(f'span[aria-label*="{label}"]')
    return el.get_text(strip=True) if el else None

def parse_artworks(html):
    soup = BeautifulSoup(html, "html.parser")
    links = soup.select('a[data-hook="deviation_link"]')
    artworks = []

    for link in links:
        img = link.select_one('img[property="contentUrl"]')
        if not img:
            continue
        page_url = link.get("href")
        artworks.append({
            "title": img.get("alt"),
            "artist": artist_from_url(page_url),
            "image_url": img.get("src"),
            "page_url": page_url,
            "favourites": count_for(link, "Favourites"),
            "views": count_for(link, "Views"),
        })

    return artworks

Заголовок берётся из атрибута alt изображения, художник из сегмента имени пользователя в URL deviation, а миниатюра из src. Счётчики избранного и просмотров находятся в небольших помеченных тегах span; count_for выполняет запрос по тексту aria-label и возвращает None, когда карточка не раскрывает это число, поэтому отсутствующий счётчик никогда не прерывает цикл. Не каждый результат публикует свою статистику в сетке, поэтому оба поля являются необязательными.

Селекторы меняются

Имена классов и внутренние атрибуты DeviantArt изменяются без предупреждения. Стабильными якорями здесь являются data-hook="deviation_link" и property="contentUrl", которые соответствуют разметке публичной схемы, но рассматривайте каждый селектор как начальный шаблон, а не как неизменный контракт. Когда поле возвращается как None для каждой карточки, заново проинспектируйте живую миниатюру в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание нормально для любого рабочего парсера.

Шаг 3: Сборка парсера

Теперь соедините запрос и парсинг в один работающий скрипт, добавьте пагинацию для получения большего количества результатов, чем первая сетка, и запишите результат в JSON. DeviantArt пагинирует поиск параметром &page=, поэтому небольшой цикл по номерам страниц собирает более широкий набор результатов. Добавляйте паузы между запросами, чтобы не перегружать сайт в плотном цикле.

python
import json
import time
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})
base_url = "https://www.deviantart.com"

def crawl(page_url):
    options = {"ajax_wait": "true", "page_wait": 5000}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("latin1")
    print(f"Request failed: {response['status_code']}")
    return None

def artist_from_url(page_url):
    parts = page_url.split("/")
    return parts[3] if len(parts) > 3 else None

def count_for(link, label):
    el = link.select_one(f'span[aria-label*="{label}"]')
    return el.get_text(strip=True) if el else None

def parse_artworks(html):
    soup = BeautifulSoup(html, "html.parser")
    links = soup.select('a[data-hook="deviation_link"]')
    artworks = []
    for link in links:
        img = link.select_one('img[property="contentUrl"]')
        if not img:
            continue
        page_url = link.get("href")
        artworks.append({
            "title": img.get("alt"),
            "artist": artist_from_url(page_url),
            "image_url": img.get("src"),
            "page_url": page_url,
            "favourites": count_for(link, "Favourites"),
            "views": count_for(link, "Views"),
        })
    return artworks

def main():
    keyword = "fantasy"
    total_pages = 2
    results = []
    for page in range(1, total_pages + 1):
        url = f"{base_url}/search?q={keyword}&page={page}"
        html = crawl(url)
        if html:
            results.extend(parse_artworks(html))
        time.sleep(3)

    with open("deviantart_data.json", "w") as f:
        json.dump(results, f, indent=2)
    print(f"Saved {len(results)} artworks")

if __name__ == "__main__":
    main()

Каждая страница имеет ту же структуру карточки, поэтому уже написанный парсер работает для всех без изменений. Пауза time.sleep(3) между запросами предотвращает перегрузку сайта при многостраничном запуске. Увеличивайте total_pages только настолько, насколько вам реально нужно, и следите за кодами статуса в процессе работы.

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

Запустите полный скрипт командой python scraper.py, и вы получите чистую структурированную запись для каждого произведения, готовую для записи в JSON, CSV или базу данных.

json
[
  {
    "title": "Owl #6",
    "artist": "siobhan-o-wisp",
    "image_url": "https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/.../owl_6.jpg",
    "page_url": "https://www.deviantart.com/siobhan-o-wisp/art/Owl-6-925596734",
    "favourites": "412",
    "views": "3.1K"
  },
  {
    "title": "Magic Forest",
    "artist": "postapodcast",
    "image_url": "https://images-wixmp-ed30a86b8c4ca887773594c2.wixmp.com/f/.../magic_forest.png",
    "page_url": "https://www.deviantart.com/postapodcast/art/Magic-Forest",
    "favourites": null,
    "views": null
  }
]

URL изображений указывают на CDN DeviantArt (хост wixmp.com), а избранное и просмотры возвращаются как null на карточках, не раскрывающих свою статистику, что ожидаемо. Имея этот список, можно индексировать метаданные, изучать их или перейти к загрузке файлов миниатюр.

Шаг 4: Загрузка файлов изображений

Записи содержат URL миниатюр, а не байты изображений. Чтобы сохранить файлы локально, потоково загрузите каждый URL на диск с помощью requests. Загрузка чанками поддерживает постоянный расход памяти даже для более крупных изображений, а raise_for_status превращает неудачную загрузку в перехваченную ошибку, а не незаметно пустой файл.

python
import os
import requests

def download_image(url, save_path):
    try:
        response = requests.get(url, stream=True, timeout=30)
        response.raise_for_status()
        with open(save_path, "wb") as file:
            for chunk in response.iter_content(chunk_size=8192):
                file.write(chunk)
        print(f"Saved {save_path}")
    except requests.exceptions.RequestException as e:
        print(f"Error downloading {url}: {e}")

def download_all(artworks, folder="downloaded_images"):
    os.makedirs(folder, exist_ok=True)
    for i, art in enumerate(artworks):
        url = art.get("image_url")
        if not url:
            continue
        artist = art.get("artist") or "unknown"
        path = os.path.join(folder, f"{artist}_{i}.jpg")
        download_image(url, path)

Вызовите download_all(results) после парсинга, и каждая миниатюра окажется в папке downloaded_images с именем, состоящим из имени пользователя художника и индекса. Именование по художнику сохраняет атрибуцию вместе с файлом, что здесь особенно важно, поскольку каждое из этих изображений принадлежит создавшему его человеку. Прочитайте следующий раздел перед сохранением чего-либо, что вы намерены использовать не только для беглого просмотра.

Как оставаться незаблокированным

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

  • Регулируйте скорость запросов. Отправка запросов на страницы поиска в плотном цикле, самый быстрый способ получить ограничение. Пауза time.sleep в цикле существует именно для этого; сохраняйте её.
  • Используйте ротацию. Пул резидентных IP распределяет запросы по множеству реальных пользовательских адресов, чтобы ни один из них не превышал лимит. Crawling API выполняет это за вас; если вы используете собственный стек, это ключевая часть, которую нужно реализовать правильно.
  • Следите за кодами статуса. Запуск, который начинает возвращать проверки или ошибки, сигнализирует о том, что текущая скорость или уровень IP больше недостаточны. Воспринимайте это как сигнал для снижения активности, а не как шум, который можно игнорировать.

Подробный сценарий работы описан в статье как парсить сайты, не получая блокировок. Если ваша цель отображается на стороне клиента, как DeviantArt, наше руководство по краулингу JavaScript-сайтов объясняет, почему рендеринг важен. Если вы предпочитаете направлять собственный трафик через ротирующийся пул вместо управляемого API, Smart AI Proxy (также называемый AI Proxy) обеспечивает ту же ротацию резидентных IP как прокси-эндпоинт для подключения.

Законен ли парсинг DeviantArt?

Это наиболее важная часть на творческой платформе, поэтому не просматривайте её бегло. Техническая часть этого руководства является более простой половиной; вопрос о правах является сложной половиной. Допустимость парсинга DeviantArt зависит от условий использования DeviantArt, вашей юрисдикции и того, что вы делаете с данными. Условия DeviantArt ограничивают автоматизированный доступ, поэтому парсинг может противоречить этим условиям независимо от аккуратности используемых инструментов. Прочитайте Условия использования DeviantArt и его robots.txt, и рассматривайте оба документа как границу того, что вы собираете.

Самый важный факт здесь: каждое произведение на DeviantArt защищено авторским правом художника, который его создал. Собрать публичный URL миниатюры или заголовок, это одно; то, что вы затем делаете с этим изображением, другое, и закон различает эти случаи. Не распространяйте, не публикуйте повторно, не перепродавайте и не обучайте модели на загруженных произведениях без явного разрешения создателя. Ограничивайте свою работу индексированием метаданных и сохранением миниатюр для личных, исследовательских или внутренних учебных целей, и сохраняйте имя художника, чтобы атрибуция никогда не терялась. Распаршенное изображение не является лицензированным изображением, и "оно было публичным" не является лицензией.

Это руководство намеренно ограничено публичными страницами галереи и поиска, поскольку именно это позволяет работе оставаться в рамках допустимого. Оно не охватывает ничего за логином, контент с ограничением по возрасту, данные аккаунта или личные данные, а также любые попытки обойти аутентификацию или ограничения контента. Только публичные, незащищённые метаданные произведений. Если ваш проект требует большего, санкционированный путь, официальный DeviantArt OAuth API, который предоставляет доступ к deviation и метаданным на условиях, которые платформа действительно предоставляет. Для чего угодно, кроме личного использования, особенно коммерческого, официальный API плюс разрешение художника являются правильным путём, а не более хитроумный парсер.

Итоги

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

  • DeviantArt отображается на стороне клиента. Обычный запрос возвращает пустую оболочку, поэтому необходимо отрендерить сетку поиска перед её парсингом.
  • Нужны рендеринг и доверенный IP одновременно. Crawling API с JS-токеном делает оба в одном вызове; ajax_wait и page_wait контролируют время ожидания контента.
  • BeautifulSoup выполняет извлечение. Выберите каждый a[data-hook="deviation_link"], затем считайте title, artist, image URL, page URL и публичные счётчики избранного или просмотров с каждого.
  • Загружайте потоково. Используйте requests с stream=True для записи миниатюр на диск чанками с именами по художнику, чтобы атрибуция сохранялась.
  • Произведения защищены авторским правом. Ограничивайтесь публичными, незащищёнными страницами, никогда не распространяйте, не перепродавайте и не обучайте модели на загруженных изображениях без разрешения, и используйте официальный DeviantArt OAuth API для чего-либо, кроме личного или исследовательского использования.

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

Почему обычный запрос не возвращает произведения с DeviantArt?

Потому что DeviantArt строит свою сетку поиска на стороне клиента с помощью JavaScript. Исходный HTML является оболочкой, которая заполняется только после выполнения скриптов страницы в браузере, поэтому необработанный HTTP-запрос возвращает статус 200 с пустой сеткой. Чтобы получить реальные данные, необходимо сначала отрендерить страницу, что и обеспечивает JS-токен Crawling API.

Нужен ли обычный токен или JS-токен для DeviantArt?

JS-токен. Обычный токен получает статический HTML, который на странице поиска DeviantArt является той же пустой оболочкой, что и обычный запрос. JS-токен рендерит страницу в реальном браузере перед возвратом HTML, поэтому миниатюры и их метаданные присутствуют при парсинге BeautifulSoup.

Как получить больше, чем первую страницу результатов?

DeviantArt пагинирует поиск параметром &page=. Переберите в цикле нужные номера страниц, создайте URL вида /search?q=fantasy&page=2 для каждой, запросите её через Crawling API и запустите тот же парсер. Добавляйте небольшую паузу между запросами, чтобы многостраничный запуск не перегружал сайт.

Мои селекторы возвращают None для каждой карточки. Что изменилось?

Почти наверняка разметка DeviantArt. Имена классов и внутренние атрибуты изменяются без предупреждения, поэтому селекторы, работавшие в прошлом месяце, могут перестать работать. Якоря схемы data-hook="deviation_link" и property="contentUrl" наиболее устойчивы, но заново проинспектируйте живую миниатюру в инструментах разработчика браузера и обновите селекторы, когда поле возвращается пустым.

Можно ли повторно использовать или продавать изображения, загруженные с DeviantArt?

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

Существует ли официальный API DeviantArt, который лучше использовать?

Да. DeviantArt предлагает OAuth API, который предоставляет доступ к deviation и их метаданным на условиях, которые платформа предоставляет напрямую. Для чего угодно, кроме случайного личного использования, и особенно для коммерческих проектов, официальный DeviantArt OAuth API является санкционированным путём, который позволяет оставаться в рамках условий использования и авторского права.

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

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

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

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