Вы пишете несколько строк Python, указываете requests на страницу товара или страницу результатов поиска, передаёте ответ в BeautifulSoup и получаете почти ничего. Заголовок есть, структура есть, но нужные данные отсутствуют. Это самая распространённая стена, на которую люди наталкиваются при попытке парсить JavaScript-страницы с Python: страница рендерит контент в браузере после получения исходного HTML, поэтому обычный HTTP-запрос видит только пустую оболочку.

В этом руководстве объясняется, почему это происходит, рассматриваются три реальных способа получить отрендеренные данные (headless-браузеры, лежащий в основе JSON API и API рендеринга), и показан чистый, рабочий пример, загружающий готовую страницу через Crawling API и парсящий её с BeautifulSoup. К концу вы будете знать, какой подход подходит для какой задачи и как не допустить блокировки.

Почему requests плюс BeautifulSoup возвращают пустую оболочку

Чтобы увидеть проблему, а не просто прочитать о ней, загрузите страницу с клиентским рендерингом простым способом и посмотрите, что возвращается.

python
import requests
from bs4 import BeautifulSoup

url = "https://example-shop.com/search?q=smartwatch"
html = requests.get(url).text
soup = BeautifulSoup(html, "html.parser")

products = soup.select("[data-product-title]")
print(f"found {len(products)} products")
# found 0 products

Код статуса 200, HTML-документ с полным содержимым и ноль товаров. Причина, жизненный цикл страницы. Сервер отправляет лёгкую HTML-оболочку: несколько точек монтирования div, несколько тегов <script>, возможно, индикатор загрузки. Только когда эти скрипты выполняются, браузер вызывает API, получает данные о товарах как JSON и строит DOM-узлы, которые их содержат. Библиотека requests не запускает JavaScript. Она загружает оболочку и останавливается, поэтому узлы с товарами никогда не существуют для BeautifulSoup.

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

Как быстро определить

Щёлкните страницу правой кнопкой мыши и выберите «Просмотр кода страницы», чтобы увидеть исходный HTML, отправленный сервером, именно это получает requests. Затем откройте инструменты разработчика и посмотрите на панель Elements, которая отображает живой DOM после выполнения скриптов. Если нужные данные появляются в Elements, но не в исходном коде, страница рендерится на клиенте и обычный запрос не сработает.

Подход 1: управление реальным браузером через Selenium или Playwright

Самое прямолинейное решение, использовать инструмент, который действительно запускает браузер. Selenium и Playwright оба запускают Chromium (headless или видимый), загружают URL, ждут завершения выполнения скриптов и позволяют читать отрендеренный DOM. Поскольку настоящий движок браузера выполняет JavaScript, данные, отсутствовавшие при обычном запросе, теперь присутствуют.

Минимальный пример на Playwright выглядит следующим образом:

python
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://example-shop.com/search?q=smartwatch")
    page.wait_for_selector("[data-product-title]")
    html = page.content()
    browser.close()

soup = BeautifulSoup(html, "html.parser")
titles = [t.get_text(strip=True) for t in soup.select("[data-product-title]")]
print(titles)

Ключевая строка, wait_for_selector. Вместо того чтобы угадывать с фиксированным sleep, вы говорите браузеру ждать, пока нужный элемент действительно не появится, что одновременно быстрее и надёжнее. Selenium предлагает ту же идею через хелперы WebDriverWait и expected-conditions.

Этот подход работает, и он является правильным инструментом, когда нужно кликать, прокручивать, заполнять формы или проходить многостраничные потоки. Но он несёт реальные затраты. Каждый экземпляр браузера потребляет сотни мегабайт RAM и полное ядро CPU, поэтому запуск многих параллельно обходится дорого. Настройка сложна: нужно управлять бинарными файлами браузера, версиями драйверов и хрупкой цепочкой зависимостей. И рендеринг сам по себе не делает вас невидимым. Headless-браузер с IP датацентра и стандартным отпечатком автоматизации выявляется и блокируется серьёзными системами защиты от ботов так же быстро, как и обычный запрос. Рендеринг решает проблему JavaScript; он ничего не делает с проблемой обнаружения. Для более детального сравнения движков смотрите руководство выбор headless-браузера для веб-скрапинга и это руководство по парсингу динамического контента с Selenium и BeautifulSoup.

Подход 2: пропуск браузера и прямой вызов лежащего в основе API

Вот идея, которую большинство руководств упускают. Когда страница с клиентским рендерингом строит себя, она почти всегда загружает данные из бэкенд-эндпоинта JSON. Если найти этот эндпоинт, можно вызвать его напрямую и полностью пропустить рендеринг, получив чистый структурированный JSON без каких-либо браузеров.

Чтобы найти его, откройте инструменты разработчика, перейдите на вкладку Network, отфильтруйте по Fetch/XHR и перезагрузите страницу. Вы ищете запрос, ответ которого содержит нужные данные, обычно URL с /api/, /graphql или насыщенным запросами путём. Найдя его, воспроизведите его в Python.

python
import requests

api = "https://example-shop.com/api/search"
params = {"q": "smartwatch", "page": 1}
headers = {"Accept": "application/json"}

data = requests.get(api, params=params, headers=headers).json()
for item in data["results"]:
    print(item["title"], item["price"])

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

Загвоздка в том, что это не всегда работает. Эндпоинт может требовать подписанного токена, cookie сессии или специального набора заголовков, динамически генерируемых страницей. Он может быть защищён тем же слоем защиты от ботов, что и сама страница. И он может измениться без предупреждения, поскольку внутренний API не имеет обязательств по стабильности. Когда API доступен, воспользуйтесь им. Когда он заблокирован, вам снова нужна отрендеренная страница, что приводит к третьему подходу.

Подход 3: рендеринг через Crawling API и парсинг результата

Два предыдущих подхода решают половину проблемы каждый. Headless-браузер рендерит, но не скрывает вас. Прямой вызов API чистый, но часто заблокированный. Обычно нужны оба сразу: реальный браузер, выполняющий JavaScript страницы, расположенный за IP, который сайт воспринимает как настоящего посетителя, возвращающий готовый HTML в одном вызове, чтобы ваш Python оставался простым.

Именно это делает Crawling API. Вы отправляете ему URL с JavaScript-токеном, он загружает страницу в реальном браузере на своей стороне, ротирует через резидентные IP на стороне сервера и возвращает полностью отрендеренный HTML. Вы никогда не запускаете парк браузеров и не обслуживаете пул прокси; вы делаете один HTTP-запрос и парсите ответ с тем же BeautifulSoup, который вы уже знаете.

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

Crawlbase предлагает два типа токенов. Обычный токен загружает статический HTML; JavaScript (JS) токен сначала рендерит страницу в реальном браузере. Для цели с клиентским рендерингом нужен JS-токен, иначе вы получите обратно ту же пустую оболочку, что и при обычном запросе, и парсить там будет нечего.

Установите официальный клиент и BeautifulSoup, затем загрузите отрендеренную страницу.

bash
python -m venv scraper_env
source scraper_env/bin/activate

pip install crawlbase beautifulsoup4

В Windows активируйте окружение командой scraper_env\Scripts\activate вместо строки с source. Теперь загрузите страницу с JS-токеном и двумя параметрами ожидания, важными для контента с клиентским рендерингом.

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://example-shop.com/search?q=smartwatch"
    html = crawl(page_url)
    print(html[:500] if html else "No HTML returned")

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

Crawlbase Crawling API

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

Парсинг отрендеренного HTML с BeautifulSoup

Когда crawl возвращает отрендеренный HTML, шаг парсинга является обычным BeautifulSoup, поскольку JavaScript уже выполнен на стороне сервера и узлы с данными присутствуют. Оберните доступ к полям в небольшой хелпер, чтобы один отсутствующий элемент не прервал выполнение.

python
import json
from crawlbase import CrawlingAPI
from bs4 import BeautifulSoup

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")
    return None

def parse_products(html):
    soup = BeautifulSoup(html, "html.parser")
    items = []
    for card in soup.select("div.product-card"):
        title = card.select_one("[data-product-title]")
        price = card.select_one("span.price")
        items.append({
            "title": title.get_text(strip=True) if title else None,
            "price": price.get_text(strip=True) if price else None,
        })
    return items

def main():
    url = "https://example-shop.com/search?q=smartwatch"
    html = crawl(url)
    if not html:
        return
    products = parse_products(html)
    print(json.dumps(products, indent=2))

if __name__ == "__main__":
    main()

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

json
[
  {
    "title": "Aero Fit Smartwatch 2",
    "price": "$129.00"
  },
  {
    "title": "Pulse Sport Band Pro",
    "price": "$89.99"
  }
]
Селекторы устаревают

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

Распространённые ошибки при парсинге JavaScript-страниц

Несколько проблем объясняют большинство неудачных запусков против целей с клиентским рендерингом. Знание их заранее экономит много времени на отладку.

  • Захват слишком рано. Наиболее частая ошибка, парсинг до того, как контент существует. Предпочтительнее ждать конкретного селектора или, при использовании Crawling API, опираться на ajax_wait и щедрый page_wait, а не на слепую фиксированную задержку.
  • Контент за взаимодействием. Некоторые данные появляются только после прокрутки, клика на вкладку или нажатия «Загрузить ещё». Прямой запрос или единственный рендер не вызовут этого. Здесь браузер с пошаговым управлением или рендеринг с инструкцией прокрутки оправдывают свою стоимость.
  • Ленивозагружаемые и пагинированные списки. Страницы с бесконечной прокруткой загружаются блоками при прокрутке. Либо управляйте прокруткой в браузере, либо, что лучше, найдите пагинированный API за ней и запрашивайте каждую страницу напрямую.
  • Блокировка несмотря на рендеринг. Рендеринг не является скрытностью. IP датацентра или очевидный отпечаток автоматизации всё равно блокируется. Ротация резидентных IP, это то, что реально обеспечивает работоспособность парсера в масштабе.

Выбор подхода

Не существует единственного правильного инструмента, только правильный инструмент для текущей задачи.

В первую очередь пробуйте прямой вызов API. Если вкладка Network выявляет открытый JSON-эндпоинт, это самый чистый и быстрый путь без каких-либо накладных расходов на рендеринг. Всегда проверяйте перед тем, как делать что-то более тяжёлое.

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

Используйте API рендеринга, когда нужен готовый HTML в масштабе без блокировки. Когда задача, «надёжно загрузить много JavaScript-страниц и парсить их», Crawling API устраняет два наиболее сложных аспекта, запуск браузеров и ротацию IP, и оставляет вам один HTTP-вызов плюс BeautifulSoup. Если вы предпочитаете направлять собственный браузерный трафик через ротируемый пул, Smart AI Proxy (также называемый AI Proxy) предоставляет резидентную ротацию в качестве прокси-эндпоинта. Для более широкого обзора этих шаблонов смотрите руководство как выполнять краулинг JavaScript-сайтов.

Итоги

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

  • Обычные запросы видят только оболочку. requests не запускает JavaScript, поэтому данные с клиентским рендерингом отсутствуют в загружаемом HTML.
  • Существуют три реальных решения. Управление реальным браузером, прямой вызов лежащего в основе JSON API или рендеринг через API, возвращающий готовый HTML.
  • Сначала проверьте наличие открытого API. Прямой JSON-эндпоинт является самым быстрым и чистым путём, когда он доступен, без затрат на рендеринг.
  • Рендеринг не является скрытностью. Headless-браузер на IP датацентра всё равно блокируется; ротация резидентных IP, это то, что обеспечивает работоспособность парсера.
  • Crawling API объединяет оба компонента. JS-токен рендерит страницу за доверенным IP в одном вызове; ajax_wait и page_wait управляют временем ожидания до парсинга результата BeautifulSoup.

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

Почему requests не возвращает данные на JavaScript-странице?

Потому что requests загружает только HTML, отправленный сервером, и никогда не выполняет JavaScript. Страница с клиентским рендерингом поставляет тонкую оболочку, а затем строит реальный контент в браузере, вызывая API после загрузки. Поскольку этот шаг никогда не происходит при обычном запросе, узлы с данными не существуют при разборе ответа BeautifulSoup, поэтому ваши селекторы ничего не находят.

Нужен ли headless-браузер для парсинга JavaScript-страниц с Python?

Нет. Headless-браузер является одним из вариантов, но часто самым тяжёлым. Перед запуском Selenium или Playwright откройте вкладку Network и поищите JSON-эндпоинт, который вызывает страница. Если он доступен, прямой вызов через requests быстрее и чище. Используйте браузер или API рендеринга только тогда, когда нет доступного открытого эндпоинта или данные требуют взаимодействия.

В чём разница между ajax_wait и page_wait?

ajax_wait говорит Crawling API ждать, пока асинхронные (XHR/fetch) запросы страницы не установятся, перед захватом HTML, именно это заполняет данные с клиентским рендерингом. page_wait добавляет фиксированную задержку в миллисекундах после загрузки, давая поздно рендерящимся элементам дополнительное время на появление. Используйте оба для целей с клиентским рендерингом и увеличивайте page_wait, если поля возвращаются пустыми.

Почему мой headless-браузер всё равно блокируется?

Потому что рендеринг и скрытность, это отдельные проблемы. Запуск реального браузера решает проблему выполнения JavaScript, но запрос всё равно поступает с узнаваемого IP и с отпечатком автоматизации. Системы защиты от ботов блокируют IP датацентров и стандартные headless-подписи независимо от рендеринга. Ротация резидентных IP, которую обеспечивают Crawling API и Smart AI Proxy, решает проблему блокировки.

Можно ли использовать BeautifulSoup с Crawling API?

Да, и это и есть предполагаемый рабочий процесс. Crawling API возвращает полностью отрендеренный HTML, поэтому вы парсите его с BeautifulSoup точно так же, как любую статическую страницу. Разница в том, что JavaScript уже выполнен на стороне сервера, поэтому узлы с данными, на которые нацелены ваши селекторы, присутствуют в получаемом HTML.

Как парсить JavaScript-страницы, которые загружают больше контента при прокрутке?

Страницы с бесконечной прокруткой загружаются блоками при прокрутке пользователем, поэтому один запрос или рендер захватывает только первую партию. Есть два варианта: управлять прокруткой в Selenium или Playwright, ожидая каждой партии, или найти пагинированный API, который вызывает прокрутка, на вкладке Network и запрашивать каждую страницу напрямую. Прямой путь через API обычно быстрее и надёжнее, когда эндпоинт доступен.

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

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

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

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