OpenSea является одной из крупнейших NFT-площадок, и каждая страница коллекции или листинга содержит именно те структурированные данные, которые нужны трекеру цен на NFT, исследовательскому блокноту или дашборду редкости: название объекта, коллекция, к которой он относится, текущая цена листинга в ETH, последняя продажа, id токена и изображение. Сложность в том, что OpenSea является тяжёлым JavaScript-приложением на React, поэтому обычный HTTP-запрос возвращает почти пустой каркас вместо интересующих вас объектов.

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

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

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

  • Название объекта уникальное имя отдельного NFT, например «Courtyard #1024».
  • Коллекция коллекция, к которой принадлежит объект.
  • Цена (ETH) текущая цена листинга, отображаемая на карточке.
  • Последняя продажа цена предыдущей продажи объекта, если карточка её показывает.
  • Id токена уникальный идентификатор в блокчейне, полезный для отслеживания токена на разных платформах.
  • URL изображения источник миниатюры объекта.
  • URL объекта ссылка на страницу с подробной информацией об отдельном NFT.

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

При запросе URL коллекции OpenSea с помощью обычного HTTP-клиента вы получаете ответ со статусом 200 и почти без данных NFT в теле. Два фактора работают против вас. Во-первых, OpenSea является клиентским React-приложением, формирующим сетку объектов в браузере, поэтому исходный HTML является каркасом, который заполняется только после выполнения скриптов страницы и загрузки данных площадки по сети. Во-вторых, OpenSea быстро выявляет автоматизированный трафик: IP-адреса дата-центров и паттерны запросов, не характерные для реального браузера, блокируются ещё до достижения отрендеренной сетки.

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

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

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

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

Перед написанием кода нужно подготовить несколько вещей. Ни одна из них не займёт много времени.

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

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

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

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

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

bash
python --version

python -m venv opensea_env
source opensea_env/bin/activate

pip install crawlbase beautifulsoup4

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

Шаг 1: Загрузка отрендеренной страницы коллекции

Начните с получения готовой страницы. Импортируйте класс CrawlingAPI, инициализируйте его с помощью JS-токена и запросите URL коллекции. Проверка статуса перед разбором даёт возможность явно увидеть ошибки, а не упустить их.

python
from crawlbase import CrawlingAPI

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

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("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

if __name__ == "__main__":
    page_url = "https://opensea.io/collection/courtyard-nft"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

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

Crawlbase Crawling API

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

Шаг 2: Разбор карточек объектов с помощью BeautifulSoup

Получив отрендеренный HTML, загрузите его в BeautifulSoup и извлеките каждый NFT по его селектору. OpenSea отображает коллекцию в виде сетки повторяющихся карточек объектов, поэтому вы один раз выбираете все карточки, а затем читаете одинаковые поля из каждой. Проверьте актуальные атрибуты в инструментах разработчика браузера на живой странице; приведённые ниже селекторы соответствуют макету на момент написания руководства.

python
from bs4 import BeautifulSoup

BASE = "https://opensea.io"

def text_of(card, selector):
    el = card.select_one(selector)
    return el.get_text(strip=True) if el else None

def token_id_from(href):
    # OpenSea item URLs end with the token id, e.g. /assets/.../1024
    return href.rstrip("/").split("/")[-1] if href else None

def parse_collection(html):
    soup = BeautifulSoup(html, "html.parser")
    cards = soup.select('article.AssetSearchList--asset')
    items = []

    for card in cards:
        link = card.select_one("a.Asset--anchor")
        href = link["href"] if link else None
        img = card.select_one("img")
        items.append({
            "name": text_of(card, 'span[data-testid="ItemCardFooter-name"]'),
            "price_eth": text_of(card, 'div[data-testid="ItemCardPrice"] span[data-id="TextBody"]'),
            "last_sale": text_of(card, 'div[data-testid="ItemCardPrice-secondary"]'),
            "token_id": token_id_from(href),
            "image_url": img["src"] if img else None,
            "item_url": BASE + href if href else None,
        })

    return items

Вспомогательная функция text_of одновременно выполняет две полезные задачи: запрашивает отдельный элемент внутри карточки и возвращает None при его отсутствии, избегая вызова .get_text() для None. Это делает извлечение устойчивым, когда одно поле отсутствует на конкретной карточке, что является обычным явлением, поскольку не каждый NFT показывает цену последней продажи или текущего листинга. Название объекта берётся из тега span с data-testid="ItemCardFooter-name", цена листинга из вложенного тега span с data-id="TextBody" внутри ItemCardPrice, а ссылка из анкора Asset--anchor. Id токена является последним сегментом пути этой ссылки, а полный URL объекта формируется из базового хоста и относительного href.

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

Имена классов и атрибуты data-testid в OpenSea меняются без предупреждения. Относитесь к приведённым выше селекторам как к начальному шаблону, а не как к постоянному договору. Когда поле возвращает None для каждой карточки, повторно проверьте живой объект в инструментах разработчика браузера и обновите селектор. Периодическое обслуживание селекторов является нормой для любого промышленного скрапера, а не признаком неисправности.

Шаг 3: Обработка пагинации с бесконечной прокруткой

Страница коллекции не загружает все объекты сразу. OpenSea использует бесконечную прокрутку, поэтому дополнительные NFT появляются только при прокрутке сетки вниз. Вместо того чтобы реверс-инжинирить вызовы пагинации, вы позволяете Crawling API прокрутить страницу за вас с помощью параметров scroll и scroll_interval. API прокручивает страницу в течение указанного количества секунд, что загружает дополнительные карточки в тот же HTML, который вы затем разбираете.

python
def crawl_with_scroll(page_url):
    options = {
        "ajax_wait": "true",
        "scroll": "true",
        "scroll_interval": "20",  # scroll for 20 seconds, max 60
    }
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

Установка параметра scroll в true указывает API прокрутить отрендеренную страницу, а scroll_interval управляет продолжительностью прокрутки, до 60 секунд. Более длительная прокрутка загружает больше объектов, но тратит больше времени на каждый запрос, поэтому выбирайте значение исходя из того, насколько глубоко в коллекцию нужно погружаться. Обратите внимание, что при использовании прокрутки параметр page_wait не нужен, так как прокрутка уже удерживает страницу открытой достаточно долго для рендеринга новых карточек.

Шаг 4: Сборка всего вместе

Теперь соедините загрузку с прокруткой и парсер в один работающий скрипт. Загрузите отрендеренный HTML, передайте его парсеру и запишите результат в JSON, чтобы впоследствии использовать его в любом месте.

python
import json
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

api = CrawlingAPI({"token": "YOUR_CRAWLBASE_JS_TOKEN"})
BASE = "https://opensea.io"

def crawl(page_url):
    options = {"ajax_wait": "true", "scroll": "true", "scroll_interval": "20"}
    response = api.get(page_url, options)
    if response["status_code"] == 200:
        return response["body"].decode("utf-8")
    print(f"Request failed: {response['status_code']}")
    return None

def text_of(card, selector):
    el = card.select_one(selector)
    return el.get_text(strip=True) if el else None

def token_id_from(href):
    return href.rstrip("/").split("/")[-1] if href else None

def parse_collection(html):
    soup = BeautifulSoup(html, "html.parser")
    cards = soup.select('article.AssetSearchList--asset')
    items = []

    for card in cards:
        link = card.select_one("a.Asset--anchor")
        href = link["href"] if link else None
        img = card.select_one("img")
        items.append({
            "name": text_of(card, 'span[data-testid="ItemCardFooter-name"]'),
            "price_eth": text_of(card, 'div[data-testid="ItemCardPrice"] span[data-id="TextBody"]'),
            "last_sale": text_of(card, 'div[data-testid="ItemCardPrice-secondary"]'),
            "token_id": token_id_from(href),
            "image_url": img["src"] if img else None,
            "item_url": BASE + href if href else None,
        })

    return items

def main():
    page_url = "https://opensea.io/collection/courtyard-nft"
    html = crawl(page_url)
    if not html:
        return
    items = parse_collection(html)
    with open("opensea_data.json", "w") as f:
        json.dump(items, f, indent=2)
    print(f"Saved {len(items)} items")

if __name__ == "__main__":
    main()

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

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

json
[
  {
    "name": "Courtyard #1024",
    "price_eth": "0.018 ETH",
    "last_sale": "Last sale: 0.015 ETH",
    "token_id": "1024",
    "image_url": "https://i.seadn.io/s/raw/files/abc123.png",
    "item_url": "https://opensea.io/assets/matic/0x251be3.../1024"
  },
  {
    "name": "Courtyard #2087",
    "price_eth": "0.021 ETH",
    "last_sale": null,
    "token_id": "2087",
    "image_url": "https://i.seadn.io/s/raw/files/def456.png",
    "item_url": "https://opensea.io/assets/matic/0x251be3.../2087"
  }
]

Во второй записи поле последней продажи равно null, что является нормальной работой вспомогательной функции: не каждый объект продавался раньше, поэтому это поле просто отсутствует, а не вызывает сбой.

Парсинг страниц с подробной информацией об NFT

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

python
def parse_nft_detail(html, url):
    soup = BeautifulSoup(html, "html.parser")
    rank = soup.select_one('[data-testid="rarity-rank"]')
    return {
        "name": text_of(soup, "h1.item--title"),
        "collection": text_of(soup, "a.item--collection-detail"),
        "price_eth": text_of(soup, "div.Price--amount"),
        "rarity_rank": rank.get_text(strip=True) if rank else None,
        "token_id": token_id_from(url),
        "item_url": url,
    }

Здесь повторно используются те же вспомогательные функции text_of и token_id_from из скрапера коллекции, поэтому загрузка подробностей представляет собой просто другой набор селекторов поверх того же цикла загрузки с последующим разбором. Название объекта берётся из заголовка item--title, цена листинга из Price--amount, а ранг редкости из test id rarity-rank, если коллекция его публикует. Там, где коллекция не имеет ранжирования редкости, это поле остаётся равным None, что является правильным поведением, а не ошибкой.

Масштабирование и защита от блокировок

Одна коллекция является демонстрацией; реальная задача охватывает несколько коллекций. Структура остаётся той же: ведите список URL коллекций, загружайте каждую через Crawling API с включённой прокруткой, разбирайте с помощью той же функции и собирайте строки. Поскольку все страницы коллекций имеют одинаковую структуру карточек, уже написанный парсер работает для всех них без изменений. Даже при отработанном рендеринге OpenSea следит за трафиком, характерным для скраперов, поэтому несколько привычек помогут сохранить работоспособность запуска и применимы к любой сложной цели.

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

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

Законно ли парсить OpenSea?

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

Несколько правил, которых стоит придерживаться. Собирайте только публичные данные NFT: название объекта, коллекция, цена листинга, последняя продажа, id токена, изображение и ссылка, которые может видеть любой пользователь на странице коллекции без аккаунта. Соблюдайте заявленные ожидания OpenSea по частоте запросов и держите объём запросов достаточно низким, чтобы не создавать нагрузку на серверы. Метаданные токена хранятся в блокчейне, поэтому для многих сценариев чтение блокчейна напрямую является более чистым и однозначным решением. Не распространяйте произведения искусства или медиафайлы, связанные с NFT, как свои собственные; URL изображения можно ссылаться, а базовые медиафайлы принадлежат создателю.

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

Итоги

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

  • OpenSea является клиентским React-приложением. Обычный запрос возвращает пустой каркас, поэтому страницу необходимо отрендерить до разбора.
  • Используйте JS-токен. Crawling API с JavaScript-токеном рендерит страницу за доверенным IP в одном вызове; параметры ajax_wait и page_wait управляют временем ожидания контента.
  • Прокрутка обрабатывает пагинацию. OpenSea загружает объекты при бесконечной прокрутке, поэтому передавайте параметры scroll и scroll_interval вместо реверс-инжиниринга пагинации.
  • BeautifulSoup выполняет извлечение. Выберите все карточки объектов, затем читайте из каждой название, цену в ETH, последнюю продажу, id токена, изображение и ссылку, ожидая смещения селекторов.
  • Оставайтесь в рамках публичных данных, а для промышленного использования предпочитайте официальный API. Соблюдайте условия использования OpenSea и файл robots.txt, ограничивайтесь публичными данными NFT и используйте официальный API для коммерческих или высокообъёмных задач.

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

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

Потому что OpenSea является клиентским React-приложением, формирующим сетку объектов в браузере. Исходный HTML является каркасом, который заполняется только после выполнения скриптов страницы и загрузки данных площадки по сети, поэтому обычный HTTP-запрос возвращает статус 200 с пустой сеткой. Для получения реальных данных необходимо сначала отрендерить страницу, что и делает JS-токен Crawling API.

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

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

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

OpenSea загружает объекты при бесконечной прокрутке, поэтому остальные появляются только при прокрутке сетки вниз. Передайте параметр scroll со значением true и значение scroll_interval в секундах в Crawling API, и он прокрутит отрендеренную страницу перед захватом HTML, чтобы при разборе было доступно больше карточек. Более длительная прокрутка загружает больше объектов ценой более медленного запроса.

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

Почти наверняка разметка OpenSea. Имена классов и атрибуты data-testid меняются без предупреждения, поэтому работавшие в прошлом месяце селекторы могут сломаться. Повторно проверьте живой объект в инструментах разработчика браузера и обновите селекторы. Периодическое обслуживание является нормой для любого промышленного скрапера.

Что лучше: парсить OpenSea или использовать официальный API?

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

Можно ли парсить данные аккаунтов или персональные данные с OpenSea?

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

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

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

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

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