Вход Регистрация

Парсинг

Скрапинг сложных JavaScript-сайтов без API: хитрости работы с динамическим контентом

Узнайте, как собирать данные с JavaScript-сайтов, когда нет API. Приемы и инструменты для скрапинга и работы с динамическим контентом через Python.

Команда Froxy 13 ноя 2025 8 мин
Скрапинг сложных JavaScript-сайтов без API: хитрости работы с динамическим контентом

Сегодня всё больше крупных сайтов уходят от статичной 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 веб-сайтов с примерами кода

Ниже набор практических методов и техник для парсинга JavaScript-сайтов: SPA/SSR-гибриды, сайты на React, Vue, Angular и пр.

Headless-браузеры (Playwright, Selenium)

Самый надёжный вариант, когда веб-страница с динамическим контентом не имеет явного API — задействовать рендеринг в реальных браузерах. Для «общения» скрипта парсинга с браузерами используются специальные библиотеки — веб-драйверы. С недавних пор у Google Chrome появился встроенный API-интерфейс, работающий по протоколу CDP (Chrome DevTool Protocol). Но и для него подходят те же веб-драйверы: Playwright и Selenium (если говорить о языке программирования Python), для скрапинга вебсайтов на JavaScript подойдёт Puppeteer.

Схема работы максимально проста:

  1. Скрипт с помощью специальных команд запускает новый экземпляр «безголового» или реального пользовательского браузера (если нужны «человеческие» цифровые отпечатки).
  2. Открывает целевую страницу и ждёт её прогрузки, например, пока не будет отрисован нужный элемент.
  3. Затем браузер передаёт скрипту результирующий HTML-код, который можно разобрать на составляющие. К слову, во многих веб-драйверах есть поддержка своего синтаксиса для поиска элементов на странице (без сторонних библиотек синтаксических анализаторов).
  4. Полученные структурированные данные можно сохранить в нужном вам виде и формате, передать для обработки и нормализации в другие модули парсера или отправить во внешние сервисы.

Простейший 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")  # замените на нужный сайт
Резидентные прокси

Идеальные прокси-серверы для доступа к ценным данным со всего мира.

Выбрать прокси $1.99, 100Mb

Перехват сетевых запросов (XHR, fetch)

Если у сайта нет публичного API, это не значит, что его нет совсем. «Тяжёлые» JavaScript-сайты, особенно созданные на React, Vue, Angular и т.п., сами подтягивают динамический контент по сети — через XHR или fetch.

Просто API может быть сложным в обнаружении, намеренно защищённым (обфусцированным, со специальными токенами авторизации) или скрытым.

Как искать внутренний API JavaScript-сайтов:

  1. Запустите инструменты разработчика браузера (DevTools) → вкладка «Сеть» (Network) → фильтр Fetch/XHR. Вас интересуют все запросы — изучите их заголовки и тело ответа.
  2. Содержимое задействованных скриптов и 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-адресами.

Получить триал $1.99, 100Mb

Комбинирование библиотек для парсинга (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%. По-прежнему нужны качественные прокси, надёжное и высокопроизводительное оборудование (объём вычислений возрастает кратно), а также правильно спроектированная архитектура парсера, способная противостоять антибот-системам и адаптироваться к изменениям в структуре динамического содержимого.

Получайте уведомления о новых функциях и обновлениях Froxy

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

Статьи по Теме