Сегодня всё больше крупных сайтов уходят от статичной HTML-структуры и рендерят контент на стороне клиента с помощью JavaScript-фреймворков. Для бизнеса и владельцев сайтов это отличное решение, так как можно существенно разгрузить свои серверы (хостинг) и переложить задачи по работе с контентом на более производительные CDN-сети.
С другой стороны, исследователям данных это создаёт серьёзную проблему: привычные инструменты парсинга «видят» пустую страницу или лишь обёртку JavaScript-скриптов без нужной информации.
Ниже разберём отдельно тяжёлые JavaScript вебсайты без API и конкретные практические техники по скрапингу динамического контента.
Почему JavaScript сайты сложно парсить без API
Главная сложность заключается в том, что сайты с большим объёмом JavaScript-кода не отдают результирующие страницы в виде HTML. Точнее, HTML-код отдаётся, но вместо основного содержимого в нём ссылки на JavaScript-скрипты. Они загружаются и выполняются непосредственно в браузере, и уже в процессе своего выполнения подтягивают нужную информацию, а также отвечают за формирование окончательной (итоговой) версии HTML-страницы.
Получается, что для получения данных нужен полноценный браузер или специальная среда, способная скачать и отрендерить все JavaScript-скрипты (в браузерах за это отвечает специальный движок).
Если пытаться парсить такой сайт «в лоб», посредством HTTP-протокола, например, через requests или через аналогичные Python-библиотеки, то парсер увидит пустые блоки или минимальную разметку без содержимого. Проблему на 100% решило бы наличие открытого API-интерфейса, благодаря которому данные можно запрашивать и получать в готовом структурированном виде, например, в JSON ли XML-формате. Но многие сайты, наоборот, дополнительно усложняют парсинг и намеренно исключают работу по API.
Распространённые проблемы при парсинге динамического контента
Как итог вышесказанного, парсинг вебсайтов на JavaScript сопряжён со следующими проблемами:
- Задержки рендеринга — данные могут подгружаться через несколько секунд или по определённым действиям пользователя.
- Сильная антибот-защита — динамические веб-страницы и сайты часто дополнительно проверяют поведение пользователя и «человечность» цифровых отпечатков (cookies, user-agent и заголовки, выполнение JS).
- Увеличенное потребление ресурсов. Без полноценных браузеров или без их аналогов, headless- и антидетект-браузеров, парсинг невозможен. А они в свою очередь потребляют много оперативной памяти и требуют большого объёма вычислений для рендеринга JavaScript.
- Сильное усложнение DOM-структуры. В некоторых случаях сайты могут выполнять специальные скрипты, которые намеренно «уникализируют» классы, идентификаторы и атрибуты HTML-элементов. Так как на однотипных страницах невозможно распознать повторяющиеся паттерны, приходится прибегать к использованию ИИ при парсинге и в том числе технологий компьютерного зрения, например, для распознавания скриншотов.
- Необходимость использования качественных прокси. Многие механизмы защиты завязаны на анализ истории IP и его типа, а также на соотнесении расположения IP-адреса с тем, что можно определить по косвенным признакам. Больше всего доверия вызывают резидентные и мобильные прокси. Без прокси работать будет проблематично, так как автоматические запросы быстро выявляются и блокируются. Заменить свой реальный IP на новый легче всего именно через прокси.
Методы и техники парсинга JavaScript веб-сайтов с примерами кода

Ниже набор практических методов и техник для парсинга JavaScript-сайтов: SPA/SSR-гибриды, сайты на React, Vue, Angular и пр.
Headless-браузеры (Playwright, Selenium)
Самый надёжный вариант, когда веб-страница с динамическим контентом не имеет явного API — задействовать рендеринг в реальных браузерах. Для «общения» скрипта парсинга с браузерами используются специальные библиотеки — веб-драйверы. С недавних пор у Google Chrome появился встроенный API-интерфейс, работающий по протоколу CDP (Chrome DevTool Protocol). Но и для него подходят те же веб-драйверы: Playwright и Selenium (если говорить о языке программирования Python), для скрапинга вебсайтов на JavaScript подойдёт Puppeteer.
Схема работы максимально проста:
- Скрипт с помощью специальных команд запускает новый экземпляр «безголового» или реального пользовательского браузера (если нужны «человеческие» цифровые отпечатки).
- Открывает целевую страницу и ждёт её прогрузки, например, пока не будет отрисован нужный элемент.
- Затем браузер передаёт скрипту результирующий HTML-код, который можно разобрать на составляющие. К слову, во многих веб-драйверах есть поддержка своего синтаксиса для поиска элементов на странице (без сторонних библиотек синтаксических анализаторов).
- Полученные структурированные данные можно сохранить в нужном вам виде и формате, передать для обработки и нормализации в другие модули парсера или отправить во внешние сервисы.
Простейший Python-скрипт веб-скрапинга JavaScript-сайта с использованием Playwright (не забудьте установить библиотеку и браузер командами «pip install playwright», «playwright install»):
from playwright.sync_api import sync_playwright
# Основная функция парсинга, принимает в качестве аргумента целевую страницу
def run(url):
with sync_playwright() as p:
# Запускаем экземпляр браузера в headless-режиме
browser = p.chromium.launch(headless=True)
# Открываем новую вкладку
page = browser.new_page()
# Переходим на указанный адрес
page.goto(url)
# Ждём появления карточек с товарами
page.wait_for_selector(".product-card, .catalog-item")
# Используем локаторы для поиска
cards = page.locator(".product-card, .catalog-item")
# Считаем количество найденных элементов
count = cards.count()
# Печатаем данные в консоль
print(f"Elements found: {count}")
# Обработка массива элементов
for i in range(count):
title = cards.nth(i).locator(".title, .product-title, h3").inner_text(timeout=1000) if cards.nth(i).locator(".title, .product-title, h3").count() else "-"
price = cards.nth(i).locator(".price, .product-price").inner_text(timeout=1000) if cards.nth(i).locator(".price, .product-price").count() else "-"
# Выводим в консоль название товара и цену
print(f"{i+1}. {title} — {price}")
# Закрываем браузер
browser.close()
if __name__ == "__main__":
run("https://example.com") # замените на нужный сайт
Резидентные прокси
Идеальные прокси-серверы для доступа к ценным данным со всего мира.
Перехват сетевых запросов (XHR, fetch)
Если у сайта нет публичного API, это не значит, что его нет совсем. «Тяжёлые» JavaScript-сайты, особенно созданные на React, Vue, Angular и т.п., сами подтягивают динамический контент по сети — через XHR или fetch.
Просто API может быть сложным в обнаружении, намеренно защищённым (обфусцированным, со специальными токенами авторизации) или скрытым.
Как искать внутренний API JavaScript-сайтов:
- Запустите инструменты разработчика браузера (DevTools) → вкладка «Сеть» (Network) → фильтр Fetch/XHR. Вас интересуют все запросы — изучите их заголовки и тело ответа.
- Содержимое задействованных скриптов и JSON-структур можно найти, кликнув на конкретном элементе, во вкладке «Предварительный просмотр» (Preview).
Когда вы найдёте нужные точки получения данных, останется написать свой JS-скрипт для получения динамического содержимого. Он может выглядеть примерно так:
// устанавливаем и подключаем модуль fetch — npm i node-fetch@2
const fetch = require('node-fetch');
// Основная функция
async function callApi() {
// Делаем свой вызов API
const url = 'https://example.com/api/products?page=1';
// Отправляем запрос через fetch
const res = await fetch(url, {
headers: {
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 ...'
// передайте актуальные заголовки: Authorization, X-CSRF-Token и т.д.
}
});
// Наполняем переменную полученными данными
const data = await res.json();
// Выводим количество найденных элементов в консоль
console.log(data.items.length);
}
callApi();
Полезные лайфхаки при веб-скрапинге динамического контента

Динамические сайты на JavaScript могут отдавать содержимое не сразу, а в ответ на действия пользователя. Они могут изучать поведение клиента, показывать капчу, усложнять DOM-структуру и т.п. Ниже разберём наиболее частые ситуации и методы работы с ними.
Обработка бесконечной прокрутки и динамической пагинации
Вот так может выглядеть пример Python-скрипта, который будет отрабатывать заданное количество прокруток с фиксированным таймаутом между ними:
def run(url, max_scroll=5, pause=2):
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url)
# сколько прокруток сделать
for i in range(max_scroll):
# скроллим в самый низ
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
time.sleep(pause) # ждём подгрузку контента
print(f"Scroll {i+1}/{max_scroll} done")
# Далее код вашего парсера
Если хотите максимальной надёжности, можно заменить фиксированные ожидания на случайные. А ещё перед входом в цикл скролла, можно дожидаться проверки появления на странице определённого элемента, чтобы убедиться, что вам показывается нормальная страница, а не заглушка с капчей.
Если вместо бесконечного скролла страница подгружает контент по клику, например, на кнопке «Показать ещё», то скрипт может выглядеть так:
# Определяем лимит «кликов»
def run(url, max_clicks=10):
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url)
# Стартовое значение переменной со счётчиком кликов
clicks = 0
# Пока значение счётчика меньше максимума…
while clicks < max_clicks:
try:
# ищем кнопку
button = page.locator("button:has-text('Show more'), .load-more, .show-more")
if not button.count():
print("The 'Show more' button has not been found — we are stopping.")
break
# кликаем по кнопке
button.first.click()
page.wait_for_timeout(2000) # ждём загрузку новых данных
# Увеличиваем счётчик на единицу
clicks += 1
print(f"Click on the button {clicks}/{max_clicks}")
except Exception as e:
print(f"Error when clicking: {e}")
break
# Далее собираем данные после всех кликов
Интерактивное взаимодействие / симуляция пользователя

Некоторые сайты изучают не только цифровые отпечатки браузерных профилей, но и «человечность» при взаимодействии с динамическим содержимым. На случай всестороннего тестирования headless-браузеры умеют имитировать действия пользователей. Этим вполне можно воспользоваться для задач веб-скрапинга JavaScript-сайтов. Пример типовых действий пользователя при обращении к поисковой форме (на Python):
from playwright.sync_api import sync_playwright
def run():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False) # опцию отключаем намеренно, чтобы видеть процесс вживую
page = browser.new_page()
# открываем сайт
page.goto("https://example.com") # Замените целевой URL на свой.
# вводим запрос в строку поиска
page.fill("input[name='q']", "smartphone") # Замените запрос на свой
# жмём Enter (имитация клавиши)
page.press("input[name='q']", "Enter")
# ждём результатов
page.wait_for_selector(".search-results") # Можно заменить на таймаут
# кликаем на первый товар
page.locator(".search-results .item").first.click()
# ждём карточку товара
page.wait_for_selector(".product-card")
print("Product card has been successfully opened!")
browser.close()
if __name__ == "__main__":
run()
Обход антибот-защиты и капчи
Самый первый рубеж обороны — работа через прокси. Если работать с Froxy, то ротацию и подбор выходных адресов достаточно настроить в личном кабинете. К скрипту парсинга динамического содержимого прокси подключается всего один раз.
Пример элементарной реализации:
from playwright.async_api import async_playwright
import asyncio
async def main():
# Данные прокси
proxy_server = "http://proxy.example.com:8080" # ваш прокси сервер
proxy_username = "your_username" # логин
proxy_password = "your_password" # пароль
# Запуск headless-браузера с прокси
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=False,
proxy={
'server': proxy_server,
'username': proxy_username,
'password': proxy_password
}
)
# Открываем страницу
page = await browser.new_page()
# Проверяем IP через тестовый сайт
await page.goto("https://httpbin.org/ip")
# Получаем результат
content = await page.content()
print("IP proxy:", content)
# Закрываем браузер
await browser.close()
# Запуск скрипта
asyncio.run(main())
В Playwright можно отловить появление капчи так же, как и любой другой элемент страницы — через локатор или CSS-селектор. После этого мы можем просто остановить выполнение основного скрипта и дать оператору вручную решить задачу в открытом браузере. Более правильный путь — автоматизация парсинга динамического содержимого за счёт профильных сервисов для решения капчи. Особое внимание — капче от Cloudflare, она может показываться ещё до загрузки сайта.
Вот простейший пример на Python с ручным решением:
from playwright.sync_api import sync_playwright
def run():
with sync_playwright() as p:
browser = p.chromium.launch(headless=False) # отключаем headless-режим, чтобы видеть капчу
page = browser.new_page()
# Загружаем целевой сайт
page.goto("https://example.com") # Можете указать свой адрес
# Проверяем наличие блока с капчей (например, reCAPTCHA iframe)
try:
page.wait_for_selector("iframe[title='reCAPTCHA']", timeout=5000)
print("The captcha has been detected! Solve it manually...")
# Ожидание, пока пользователь решит капчу и появится новый контент
page.wait_for_selector("#content-loaded", timeout=0)
# timeout=0 означает бесконечное ожидание
print("The captcha is solved, we continue to work.")
except:
print("The captcha did not appear, we continue to work.")
# Дальнейший парсинг или действия
browser.close()
if __name__ == "__main__":
run()
Так как некоторые системы защиты детально изучают браузерный профиль, логично замаскировать отдельные параметры при запуске своего экземпляра браузера. Например:
from playwright.async_api import async_playwright
import asyncio
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
# Список аргументов можно расширить
args=[
'--no-sandbox',
'--disable-blink-features=AutomationControlled',
'--disable-dev-shm-usage',
'--disable-features=VizDisplayCompositor',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
]
)
page = await browser.new_page()
# Скрываем атрибуты headless-браузера с помощью скрипта запуска браузера
await page.add_init_script("""
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
delete navigator.__proto__.webdriver;
""")
# Обращаемся к целевой странице
await page.goto('https://site.com/')
# Запуск асинхронной функции
asyncio.run(main())
Частично проблему обнаружения headless-браузеров решают стелс-библиотеки, такие как playwright_stealth, selenium_stealth, undetected-chromedriver, playwright_extra и т.п.
Мобильные прокси
Максимум гибкости и бесперебойная связь с мобильными IP-адресами.
Комбинирование библиотек для парсинга (BeautifulSoup, lxml) с отрендеренным HTML
Иногда возможностей встроенных механизмов headless-браузеров для поиска нужных элементов вёрстки недостаточно. Плюс кому-то может быть привычнее и понятнее синтаксис сторонних библиотек-анализаторов, например, BeautifulSoup, lxml и пр.
Никто не запрещает передать результирующий HTML-код на разбор в такие библиотеки. Пример комбинирования Selenium и BeautifulSoup:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
import time
# Настройка Selenium
def setup_driver():
chrome_options = Options()
chrome_options.add_argument("--headless") # Убрать для отладки
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
driver = webdriver.Chrome(options=chrome_options)
return driver
# Парсинг динамического контента
def parse_dynamic_content(url):
driver = setup_driver()
try:
# Загружаем страницу
driver.get(url)
# Ждем загрузки динамического контента
wait = WebDriverWait(driver, 10)
wait.until(EC.presence_of_element_located((By.CLASS_NAME, "dynamic-content")))
# Даём время на выполнение JavaScript
time.sleep(2)
# Получаем готовый HTML после выполнения JS
page_source = driver.page_source
# Передаем его в BeautifulSoup для парсинга
soup = BeautifulSoup(page_source, 'html.parser')
# Извлекаем данные
products = []
product_elements = soup.find_all('div', class_='product-item') # Здесь указан класс, идентифицирующий все карточки с товарами
# Далее каждая карточка разбирается на составляющие, это делается переборкой массива в цикле
for product in product_elements:
name = product.find('h3')
price = product.find('span', class_='price')
description = product.find('p', class_='description')
# На основе найденных данных наполняется уже другой массив
products.append({
'name': name.text.strip() if name else 'No name',
'price': price.text.strip() if price else 'No price',
'description': description.text.strip() if description else 'No description'
})
return products
finally:
driver.quit()
# Пример использования
if __name__ == "__main__":
url = "https://example.com/dynamic-products"
data = parse_dynamic_content(url)
for item in data:
print(f"Name: {item['name']}")
print(f"Price: {item['price']}")
print(f"Description: {item['description']}")
print("-" * 50)
Заключение и рекомендации

С динамическими сайтами работать значительно сложнее, чем с теми, которые отдают готовый HTML-код. Но это не значит, что веб-скрапинг JavaScript-сайтов невозможен. Достаточно подобрать правильные библиотеки и уделить внимание настройкам. В процессе парсинга появляется новый элемент — headless- или антидетект-браузер, который отвечает за рендеринг всех JS-скриптов. В таком браузере можно имитировать поведение пользователя, работать с куками и цифровыми отпечатками, взаимодействовать с нужными элементами страницы.
Но сам по себе браузер не решает проблемы работы с динамическим содержимым на 100%. По-прежнему нужны качественные прокси, надёжное и высокопроизводительное оборудование (объём вычислений возрастает кратно), а также правильно спроектированная архитектура парсера, способная противостоять антибот-системам и адаптироваться к изменениям в структуре динамического содержимого.

