Как скрапить сайты с JavaScript

Скрапинг JavaScript-рендеренного контента с headless-браузерами и прокси. Руководства по настройке Puppeteer, Playwright и chromedp с оптимизацией и перехватом API.

Как скрапить сайты с JavaScript

Проблема JavaScript-рендеренного контента

Современные веб-сайты всё чаще используют JavaScript для рендеринга контента. Одностраничные приложения (SPA) на React, Vue или Angular загружают минимальную HTML-оболочку, а затем получают и рендерят данные на стороне клиента. Когда вы делаете простой HTTP-запрос к таким сайтам, вы получаете пустую или неполную страницу, потому что контент существует только после выполнения JavaScript.

Скрапинг JavaScript-тяжёлых сайтов требует headless-браузеров — настоящих браузерных движков, работающих без видимого окна, способных выполнять JavaScript, рендерить DOM и взаимодействовать с элементами страницы. В сочетании с прокси headless-браузеры открывают доступ к данным даже самых динамичных сайтов.

Это руководство — часть нашего Полного руководства по прокси для веб-скрапинга. Об избежании обнаружения при использовании headless-браузеров — Как антибот-системы обнаруживают прокси.

Когда нужен headless-браузер?

СценарийПростой HTTPHeadless-браузер
Статические HTML-страницыРаботает отличноИзлишне
Серверный рендеринг с APIРаботает (обращайтесь к API напрямую)Не нужен
SPA (React, Vue, Angular)Получает пустую оболочкуОбязателен
Бесконечная прокрутка / ленивая загрузкаНе может вызватьОбязателен
Контент за авторизацией + JSЗатруднительноРекомендуется
Страницы с JS-проверками ботовНе проходит детекциюОбязателен
Всегда проверяйте наличие API или серверного рендеринга прежде чем использовать headless-браузер. Многие «JavaScript-тяжёлые» сайты имеют API-эндпоинты, возвращающие чистый JSON — гораздо быстрее и дешевле для скрапинга.

Puppeteer + прокси (Node.js)

Puppeteer управляет Chrome/Chromium программно. Это наиболее зрелый инструмент headless-браузера для Node.js.

Базовая настройка с ProxyHat

const puppeteer = require('puppeteer');
async function scrapeWithPuppeteer(url) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: [
      '--proxy-server=http://gate.proxyhat.com:8080',
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-dev-shm-usage',
    ],
  });
  const page = await browser.newPage();
  // Authenticate with proxy
  await page.authenticate({
    username: 'USERNAME',
    password: 'PASSWORD',
  });
  // Set realistic viewport and user agent
  await page.setViewport({ width: 1920, height: 1080 });
  await page.setUserAgent(
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
    '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  );
  try {
    await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
    // Wait for specific content to render
    await page.waitForSelector('.product-list', { timeout: 10000 });
    const content = await page.content();
    const data = await page.evaluate(() => {
      return Array.from(document.querySelectorAll('.product-item')).map(el => ({
        name: el.querySelector('.product-name')?.textContent?.trim(),
        price: el.querySelector('.product-price')?.textContent?.trim(),
        url: el.querySelector('a')?.href,
      }));
    });
    return { html: content, data };
  } finally {
    await browser.close();
  }
}
// Usage
const result = await scrapeWithPuppeteer('https://example.com/products');
console.log(`Found ${result.data.length} products`);

Оптимизированный многостраничный скрапинг

const puppeteer = require('puppeteer');
class PuppeteerScraper {
  constructor(concurrency = 3) {
    this.concurrency = concurrency;
    this.browser = null;
  }
  async init() {
    this.browser = await puppeteer.launch({
      headless: 'new',
      args: [
        '--proxy-server=http://gate.proxyhat.com:8080',
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-gpu',
        '--disable-extensions',
      ],
    });
  }
  async scrapePage(url) {
    const page = await this.browser.newPage();
    await page.authenticate({ username: 'USERNAME', password: 'PASSWORD' });
    await page.setViewport({ width: 1920, height: 1080 });
    // Block unnecessary resources to speed up loading
    await page.setRequestInterception(true);
    page.on('request', (req) => {
      const type = req.resourceType();
      if (['image', 'stylesheet', 'font', 'media'].includes(type)) {
        req.abort();
      } else {
        req.continue();
      }
    });
    try {
      await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
      const content = await page.content();
      return { url, status: 'success', html: content };
    } catch (err) {
      return { url, status: 'error', error: err.message };
    } finally {
      await page.close();
    }
  }
  async scrapeMany(urls) {
    const results = [];
    for (let i = 0; i < urls.length; i += this.concurrency) {
      const batch = urls.slice(i, i + this.concurrency);
      const batchResults = await Promise.all(
        batch.map(url => this.scrapePage(url))
      );
      results.push(...batchResults);
      console.log(`Progress: ${results.length}/${urls.length}`);
    }
    return results;
  }
  async close() {
    if (this.browser) await this.browser.close();
  }
}
// Usage
const scraper = new PuppeteerScraper(3);
await scraper.init();
const results = await scraper.scrapeMany(urls);
await scraper.close();

Playwright + прокси (Python)

Playwright — более новая альтернатива, поддерживающая Chromium, Firefox и WebKit. Его Python API чистый и хорошо подходит для скрапинга.

Базовая настройка

from playwright.sync_api import sync_playwright
def scrape_with_playwright(url: str) -> dict:
    """Scrape a JavaScript-heavy page using Playwright with ProxyHat proxy."""
    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            proxy={
                "server": "http://gate.proxyhat.com:8080",
                "username": "USERNAME",
                "password": "PASSWORD",
            }
        )
        context = browser.new_context(
            viewport={"width": 1920, "height": 1080},
            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 = context.new_page()
        try:
            page.goto(url, wait_until="networkidle", timeout=60000)
            # Wait for dynamic content
            page.wait_for_selector(".product-list", timeout=10000)
            # Extract data using page.evaluate
            products = page.evaluate("""() => {
                return Array.from(document.querySelectorAll('.product-item')).map(el => ({
                    name: el.querySelector('.product-name')?.textContent?.trim(),
                    price: el.querySelector('.product-price')?.textContent?.trim(),
                    url: el.querySelector('a')?.href,
                }));
            }""")
            return {"url": url, "products": products, "html": page.content()}
        finally:
            browser.close()

Асинхронный Playwright для параллельного скрапинга

import asyncio
from playwright.async_api import async_playwright
async def scrape_batch(urls: list[str], concurrency: int = 3) -> list[dict]:
    """Scrape multiple JS-heavy pages in parallel using Playwright."""
    results = []
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=True,
            proxy={
                "server": "http://gate.proxyhat.com:8080",
                "username": "USERNAME",
                "password": "PASSWORD",
            }
        )
        semaphore = asyncio.Semaphore(concurrency)
        async def scrape_one(url: str) -> dict:
            async with semaphore:
                context = await browser.new_context(
                    viewport={"width": 1920, "height": 1080},
                )
                page = await context.new_page()
                # Block heavy resources
                await page.route("**/*.{png,jpg,jpeg,gif,svg,css,woff,woff2}",
                                 lambda route: route.abort())
                try:
                    await page.goto(url, wait_until="networkidle", timeout=30000)
                    html = await page.content()
                    return {"url": url, "status": "success", "html": html}
                except Exception as e:
                    return {"url": url, "status": "error", "error": str(e)}
                finally:
                    await context.close()
        tasks = [scrape_one(url) for url in urls]
        results = await asyncio.gather(*tasks)
        await browser.close()
    return results
# Usage
urls = [f"https://example.com/product/{i}" for i in range(50)]
results = asyncio.run(scrape_batch(urls, concurrency=5))

Go: chromedp с прокси

package main
import (
    "context"
    "fmt"
    "log"
    "time"
    "github.com/chromedp/chromedp"
)
func scrapeJSPage(targetURL string) (string, error) {
    // Configure proxy
    opts := append(chromedp.DefaultExecAllocatorOptions[:],
        chromedp.ProxyServer("http://gate.proxyhat.com:8080"),
        chromedp.Flag("headless", true),
        chromedp.Flag("disable-gpu", true),
        chromedp.Flag("no-sandbox", true),
        chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "+
            "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"),
    )
    allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
    defer cancel()
    ctx, cancel := chromedp.NewContext(allocCtx)
    defer cancel()
    ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
    defer cancel()
    var htmlContent string
    err := chromedp.Run(ctx,
        chromedp.Navigate(targetURL),
        chromedp.WaitVisible(".product-list", chromedp.ByQuery),
        chromedp.OuterHTML("html", &htmlContent),
    )
    if err != nil {
        return "", fmt.Errorf("scrape failed: %w", err)
    }
    return htmlContent, nil
}
func main() {
    html, err := scrapeJSPage("https://example.com/products")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Got %d bytes of rendered HTML\n", len(html))
}

Стратегии оптимизации производительности

Headless-браузеры в 10-50 раз медленнее простых HTTP-запросов. Вот стратегии минимизации разрыва в производительности:

1. Блокируйте ненужные ресурсы

Изображения, CSS, шрифты и медиафайлы не нужны для извлечения данных. Их блокировка кардинально ускоряет загрузку страниц:

# Playwright resource blocking
async def fast_scrape(page, url):
    # Block images, CSS, fonts, media
    await page.route("**/*.{png,jpg,jpeg,gif,svg,css,woff,woff2,mp4,webm}",
                     lambda route: route.abort())
    # Also block tracking scripts
    await page.route("**/*google-analytics*", lambda route: route.abort())
    await page.route("**/*facebook*", lambda route: route.abort())
    await page.goto(url, wait_until="domcontentloaded")  # Faster than networkidle
    return await page.content()

2. Используйте правильную стратегию ожидания

СтратегияСкоростьНадёжностьПрименение
domcontentloadedБыстроМожет пропустить async-данныеСтраницы с inline-данными
loadСреднеХорошаяБольшинство страниц
networkidleМедленноНаивысшаяТяжёлые SPA, бесконечная прокрутка
Конкретный селекторВарьируетсяНаивысшаяКогда знаете целевой элемент

3. Переиспользуйте экземпляры браузера

Запуск браузера занимает 1-3 секунды. Для пакетного скрапинга запустите один раз и создавайте новые страницы/контексты для каждого URL:

from playwright.sync_api import sync_playwright
class BrowserPool:
    """Reusable browser pool for efficient headless scraping."""
    def __init__(self, pool_size: int = 3):
        self.pool_size = pool_size
        self.playwright = None
        self.browsers = []
    def start(self):
        self.playwright = sync_playwright().start()
        for _ in range(self.pool_size):
            browser = self.playwright.chromium.launch(
                headless=True,
                proxy={
                    "server": "http://gate.proxyhat.com:8080",
                    "username": "USERNAME",
                    "password": "PASSWORD",
                }
            )
            self.browsers.append(browser)
    def get_browser(self, index: int):
        return self.browsers[index % self.pool_size]
    def stop(self):
        for browser in self.browsers:
            browser.close()
        self.playwright.stop()
# Usage
pool = BrowserPool(pool_size=3)
pool.start()
for i, url in enumerate(urls):
    browser = pool.get_browser(i)
    context = browser.new_context()
    page = context.new_page()
    page.goto(url, wait_until="networkidle")
    html = page.content()
    context.close()
pool.stop()

4. Перехватывайте API-вызовы вместо парсинга DOM

Многие SPA получают данные через API. Перехватывайте эти вызовы напрямую — вы получаете чистый JSON без парсинга HTML:

const puppeteer = require('puppeteer');
async function interceptAPIData(url) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--proxy-server=http://gate.proxyhat.com:8080'],
  });
  const page = await browser.newPage();
  await page.authenticate({ username: 'USERNAME', password: 'PASSWORD' });
  const apiResponses = [];
  // Intercept XHR/fetch responses
  page.on('response', async (response) => {
    const url = response.url();
    if (url.includes('/api/') || url.includes('/graphql')) {
      try {
        const json = await response.json();
        apiResponses.push({ url, data: json });
      } catch {
        // Not JSON, skip
      }
    }
  });
  await page.goto(url, { waitUntil: 'networkidle2' });
  await browser.close();
  return apiResponses;
}
// Get clean API data instead of scraping DOM
const data = await interceptAPIData('https://example.com/products');
console.log(`Intercepted ${data.length} API calls`);

Сравнение headless-браузера и HTTP

МетрикаПростой HTTP + проксиHeadless-браузер + прокси
Скорость на страницу0.5-2 секунды3-15 секунд
Память на экземпляр~50 МБ200-500 МБ
Нагрузка на CPUМинимальнаяЗначительная
Трафик на страницу50-200 КБ2-10 МБ (с ресурсами)
Рендеринг JavaScriptНетПолный
Обход антибот-защитыОграниченныйЛучше (реальный браузер)
Параллельных страниц100+3-10 на машину

Лучшие практики

  • Всегда пробуйте HTTP сначала. Проверяйте API-эндпоинты, серверный рендеринг или JSON в HTML перед использованием headless-браузера.
  • Блокируйте ненужные ресурсы. Изображения, CSS и шрифты добавляют время загрузки без предоставления данных.
  • Используйте конкретные селекторы для ожидания. networkidle безопасен, но медлителен. Ждите конкретный нужный элемент.
  • Переиспользуйте экземпляры браузера. Запускайте один раз, создавайте новые контексты для каждой страницы.
  • Перехватывайте API-вызовы. Многие SPA загружают данные через API — перехватывайте JSON напрямую.
  • Ограничивайте конкурентность. Headless-браузеры требуют много памяти. 3-5 параллельных страниц на ГБ RAM — хорошее правило.
  • Используйте резидентные прокси. Резидентные прокси ProxyHat обеспечивают наивысший уровень доверия, снижая обнаружение при работе headless-браузеров.

Об обработке CAPTCHA, с которыми сталкиваются headless-браузеры — Обработка CAPTCHA при скрапинге. О масштабировании скрапинга headless-браузерами — Масштабирование инфраструктуры скрапинга.

Начните с Python SDK, Node SDK или Go SDK для интеграции прокси и изучите ProxyHat для веб-скрапинга.

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

Всегда ли нужен headless-браузер для JavaScript-сайтов?

Нет. Многие JavaScript-тяжёлые сайты загружают данные из API-эндпоинтов. Проверьте вкладку Network в браузере на XHR/fetch-запросы — если данные приходят из API, вызывайте его напрямую простыми HTTP-запросами через прокси, что гораздо быстрее.

Puppeteer или Playwright — что лучше для скрапинга?

Playwright рекомендуется для новых проектов. Он поддерживает несколько браузерных движков (Chromium, Firefox, WebKit), имеет лучшее автоожидание, нативную поддержку async в Python и встроенную конфигурацию прокси. Puppeteer более зрелый и имеет большую экосистему в мире Node.js.

Сколько страниц headless-браузера можно запустить параллельно?

Каждая страница потребляет 200-500 МБ оперативной памяти. На машине с 8 ГБ RAM реалистично 3-10 параллельных страниц. Используйте блокировку ресурсов (изображения, CSS) для снижения потребления памяти. Для большей конкурентности распределяйте по нескольким машинам с очередной архитектурой.

Зачем использовать прокси с headless-браузерами?

Даже с настоящим браузером повторные запросы с одного IP блокируются. Прокси ротируют IP, чтобы каждая загрузка страницы выглядела как от другого пользователя. Резидентные прокси ProxyHat обеспечивают наивысший уровень доверия, минимизируя блокировки и CAPTCHA.

Готовы начать?

Доступ к более чем 50 млн резидентных IP в 148+ странах с AI-фильтрацией.

Смотреть ценыРезидентные прокси
← Вернуться в Блог