Проблема JavaScript-рендеренного контента
Современные веб-сайты всё чаще используют JavaScript для рендеринга контента. Одностраничные приложения (SPA) на React, Vue или Angular загружают минимальную HTML-оболочку, а затем получают и рендерят данные на стороне клиента. Когда вы делаете простой HTTP-запрос к таким сайтам, вы получаете пустую или неполную страницу, потому что контент существует только после выполнения JavaScript.
Скрапинг JavaScript-тяжёлых сайтов требует headless-браузеров — настоящих браузерных движков, работающих без видимого окна, способных выполнять JavaScript, рендерить DOM и взаимодействовать с элементами страницы. В сочетании с прокси headless-браузеры открывают доступ к данным даже самых динамичных сайтов.
Это руководство — часть нашего Полного руководства по прокси для веб-скрапинга. Об избежании обнаружения при использовании headless-браузеров — Как антибот-системы обнаруживают прокси.
Когда нужен headless-браузер?
| Сценарий | Простой HTTP | Headless-браузер |
|---|---|---|
| Статические 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.






