Вы пишете несколько строк Python, указываете requests на страницу товара или страницу результатов поиска, передаёте ответ в BeautifulSoup и получаете почти ничего. Заголовок есть, структура есть, но нужные данные отсутствуют. Это самая распространённая стена, на которую люди наталкиваются при попытке парсить JavaScript-страницы с Python: страница рендерит контент в браузере после получения исходного HTML, поэтому обычный HTTP-запрос видит только пустую оболочку.
В этом руководстве объясняется, почему это происходит, рассматриваются три реальных способа получить отрендеренные данные (headless-браузеры, лежащий в основе JSON API и API рендеринга), и показан чистый, рабочий пример, загружающий готовую страницу через Crawling API и парсящий её с BeautifulSoup. К концу вы будете знать, какой подход подходит для какой задачи и как не допустить блокировки.
Почему requests плюс BeautifulSoup возвращают пустую оболочку
Чтобы увидеть проблему, а не просто прочитать о ней, загрузите страницу с клиентским рендерингом простым способом и посмотрите, что возвращается.
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 выглядит следующим образом:
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.
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, который вы уже знаете.
Crawlbase предлагает два типа токенов. Обычный токен загружает статический HTML; JavaScript (JS) токен сначала рендерит страницу в реальном браузере. Для цели с клиентским рендерингом нужен JS-токен, иначе вы получите обратно ту же пустую оболочку, что и при обычном запросе, и парсить там будет нечего.
Установите официальный клиент и BeautifulSoup, затем загрузите отрендеренную страницу.
python -m venv scraper_env source scraper_env/bin/activate pip install crawlbase beautifulsoup4
В Windows активируйте окружение командой scraper_env\Scripts\activate вместо строки с source. Теперь загрузите страницу с JS-токеном и двумя параметрами ожидания, важными для контента с клиентским рендерингом.
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 символах, а не оболочку, которую возвращает обычный запрос. Это подтверждает, что рендеринг работает, прежде чем вы напишете единственный селектор.
Рендеринг JavaScript-страницы за доверенным IP в одном вызове, это именно то, для чего создан Crawling API. Передайте JS-токен, он запускает страницу в реальном браузере, ротирует резидентные IP на стороне сервера и возвращает готовый HTML, поэтому вам не нужно самостоятельно управлять парком headless-браузеров и пулом прокси. Попробуйте на реальной странице в рамках бесплатного тарифа.
Парсинг отрендеренного HTML с BeautifulSoup
Когда crawl возвращает отрендеренный HTML, шаг парсинга является обычным BeautifulSoup, поскольку JavaScript уже выполнен на стороне сервера и узлы с данными присутствуют. Оберните доступ к полям в небольшой хелпер, чтобы один отсутствующий элемент не прервал выполнение.
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 или базу данных.
[ { "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 запросов бесплатно, без карты.
