Безголовый браузер (headless browser), это настоящий браузерный движок, работающий без видимого окна: он загружает страницы, выполняет JavaScript, применяет CSS и строит тот же DOM, что Chrome или Firefox, но делает это в фоновом режиме под управлением вашего скрипта. Для парсинга сайтов с помощью безголового браузера это принципиально важно, поскольку значительная часть современного веба существует только после выполнения JavaScript. Обычный HTTP-запрос возвращает начальную HTML-оболочку, тогда как безголовый браузер отдаёт страницу такой, какой её видит реальный пользователь.

Это руководство представляет собой практическое пошаговое руководство с работающими примерами. Вы запустите современный стек безголового браузера на Node (Puppeteer, затем Playwright), загрузите страницу с активным использованием JavaScript, дождётесь нужного контента, извлечёте структурированные данные и сделаете скриншот. Затем мы честно разберём, где этот подход даёт сбой при масштабировании, и покажем однозапросную альтернативу: рендеринг страницы на сервере через Crawlbase Crawling API с JavaScript-токеном.

Что такое безголовый браузер на самом деле

Обычный браузер рисует пиксели на экране. Безголовый браузер пропускает видимый интерфейс, сохраняя всё остальное: движок JavaScript, движок компоновки, сетевой стек, куки и полный DOM. Вы управляете им программно, а не кликами мышью, что делает его идеальным для автоматизированного тестирования, создания скриншотов и парсинга страниц, формируемых на стороне клиента.

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

Почему JavaScript-сайты ломают обычные HTTP-парсеры

Если запросить одностраничное приложение или страницу с бесконечной прокруткой с помощью простого HTTP-клиента, обычно получаешь статус 200 и почти пустое тело. Нужная разметка отсутствует в этом ответе. Она добавляется после того, как браузер выполняет скрипты страницы, делает XHR или fetch-запросы и рендерит результат в DOM.

Такие инструменты, как Cheerio или Beautiful Soup, разбирают переданный им HTML, но не могут выполнять JavaScript, поэтому видят лишь пустую оболочку. Безголовый браузер закрывает этот разрыв: он выполняет страницу точно так же, как браузер реального пользователя, а затем позволяет прочитать готовый DOM. Для статических страниц с серверным рендерингом эти накладные расходы не нужны, но для чего-либо с клиентским рендерингом это разница между данными и пустым массивом.

Headless vs. plain HTTP

Используйте безголовый браузер, когда нужный контент появляется только после выполнения скриптов, когда необходимо кликать или прокручивать для получения большего количества данных, или когда нужен скриншот. Для статического HTML, уже содержащего ваши данные, обычный HTTP-запрос плюс парсер работают быстрее и дешевле. Подбирайте инструмент под страницу, а не наоборот.

Настройка проекта

Вам нужны Node.js (версия 18 или новее) и npm. Убедитесь в их наличии, создайте проект и установите Puppeteer. Puppeteer при установке автоматически загрузит совместимую версию Chrome, так что настраивать больше ничего не нужно.

bash
node --version
npm --version

mkdir headless-scraper && cd headless-scraper
npm init -y
npm install puppeteer

Важно понимать с самого начала: безголовый браузер, ресурсоёмкий инструмент. Каждый экземпляр представляет собой полноценный процесс Chrome с собственным потреблением памяти и CPU. Для одной страницы на ноутбуке это нормально, но как только понадобятся сотни параллельных страниц, это становится центральной проблемой масштабирования. Вернёмся к этому позже.

Запуск безголового браузера и загрузка страницы

Основной цикл в Puppeteer всегда одинаков: запустить браузер, открыть новую страницу, перейти по URL, выполнить работу, затем закрыть браузер, чтобы не было утечек процессов. Вот минимальный вариант, загружающий страницу и печатающий её заголовок.

javascript
const puppeteer = require('puppeteer')

async function run() {
  const browser = await puppeteer.launch({ headless: true })
  const page = await browser.newPage()

  await page.goto('https://quotes.toscrape.com/js/', {
    waitUntil: 'networkidle2',
  })

  console.log(await page.title())
  await browser.close()
}

run().catch((err) => console.error(err))

Запустите командой node index.js. Целевой сайт, JavaScript-версия Quotes to Scrape, намеренно рендерит цитаты на стороне клиента, поэтому это хорошая тестовая площадка: обычный fetch возвращает пустой список, тогда как безголовый браузер видит реальный контент. Опция waitUntil: 'networkidle2' указывает goto не завершать навигацию, пока сеть не будет простаивать некоторое время, это ваша первая и наиболее грубая стратегия ожидания.

Ждать нужного контента, а не фиксированного таймера

Ожидание, именно здесь большинство безголовых парсеров допускают ошибки. Фиксированный sleep ненадёжен: слишком короткий, и вы парсите до того, как данные появились; слишком длинный, и каждый запуск тормозит. Лучший подход, дождаться конкретного сигнала, подтверждающего присутствие нужного контента.

Puppeteer предоставляет несколько вариантов, в примерном порядке предпочтения:

  • waitForSelector блокирует выполнение до появления конкретного элемента в DOM. Это наиболее надёжный сигнал, поскольку ожидание привязано к нужным данным.
  • waitForFunction блокирует выполнение до выполнения произвольного условия JavaScript, например до достижения списком определённой длины. Используйте, когда одного факта присутствия недостаточно.
  • waitUntil в goto (load, domcontentloaded, networkidle2) управляет тем, когда разрешается навигация. Хорошо как базовый уровень, слабо как единственная гарантия.

По возможности предпочитайте ожидание селектора, а не жёсткий таймер. Это и быстрее в среднем, и значительно надёжнее при медленной сети.

javascript
await page.goto('https://quotes.toscrape.com/js/', {
  waitUntil: 'domcontentloaded',
})

// Block until the first quote is actually in the DOM.
await page.waitForSelector('.quote')

// Or wait for a richer condition: at least 10 quotes loaded.
await page.waitForFunction(() => {
  return document.querySelectorAll('.quote').length >= 10
})

Извлечение структурированных данных из отрендеренного DOM

Когда контент присутствует, page.evaluate выполняет функцию в контексте самой страницы, где доступен полный DOM и стандартные браузерные API. Возвращаемое значение сериализуется обратно в ваш Node-скрипт. Это делает извлечение простым: вы пишете обычный код querySelectorAll, как если бы работали в консоли браузера.

javascript
const quotes = await page.evaluate(() => {
  const cards = document.querySelectorAll('.quote')

  return Array.from(cards).map((card) => ({
    text: card.querySelector('.text').innerText.trim(),
    author: card.querySelector('.author').innerText.trim(),
    tags: Array.from(card.querySelectorAll('.tag')).map((t) => t.innerText),
  }))
})

console.log(quotes)

Результат, чистый массив объектов, который можно записать в JSON, загрузить в базу данных или передать в пайплайн. Сокращённый пример вывода выглядит так:

json
[
  {
    "text": "The world as we have created it is a process of our thinking.",
    "author": "Albert Einstein",
    "tags": ["change", "deep-thoughts", "thinking"]
  },
  {
    "text": "It is our choices that show what we truly are.",
    "author": "J.K. Rowling",
    "tags": ["abilities", "choices"]
  }
]

Создание скриншота

Только настоящий движок рендеринга может сделать точный скриншот, что полезно для визуального контроля качества, архивирования состояния страницы или отладки парсинга, не вернувшего данных. Puppeteer захватывает область просмотра или всю прокручиваемую страницу одним вызовом.

javascript
await page.screenshot({
  path: 'quotes.png',
  fullPage: true,
})

Если скриншоты, это главное, что вам нужно в большом объёме, содержать и поддерживать целый парк браузеров только ради изображений нецелесообразно. Crawlbase Screenshots API рендерит страницу на сервере и возвращает изображение напрямую, без необходимости управлять браузером на вашей стороне.

То же самое в Playwright

Playwright, разрабатываемый Microsoft, второй современный вариант. Он управляет Chromium, Firefox и WebKit через единый API, а его механизм автоматического ожидания делает большинство явных ожиданий, описанных выше, излишними: такие действия, как click и чтение локаторов, по умолчанию ждут готовности элемента. Структура близко повторяет Puppeteer, так что переход между ними прост.

javascript
const { chromium } = require('playwright')

async function run() {
  const browser = await chromium.launch({ headless: true })
  const page = await browser.newPage()

  await page.goto('https://quotes.toscrape.com/js/')
  await page.waitForSelector('.quote')

  const quotes = await page.$$eval('.quote .text', (els) =>
    els.map((el) => el.innerText.trim()),
  )

  console.log(quotes)
  await browser.close()
}

run().catch((err) => console.error(err))

Обе библиотеки отличны. Для более глубокого сравнения на другом языке и инструменте, статья парсинг сайтов с Python и Selenium разбирает те же идеи в стеке Selenium и Python.

Где безголовый парсинг даёт сбой при масштабировании

Всё вышесказанное прекрасно работает для одной страницы на вашем компьютере. Проблемы начинаются при необходимости масштабирования, и они проявляются двумя принципиально разными способами.

Первое, ресурсы. Каждый экземпляр безголового браузера представляет собой полноценный процесс Chrome, потребляющий сотни мегабайт ОЗУ. Запустить несколько параллельно, нормально; если нужно парсить тысячи страниц в час, придётся разворачивать целый парк, управлять утечками памяти и зависшими процессами, перезапускать упавшие экземпляры и платить за серверы. Браузер, который был одной строкой launch, превращается в инфраструктуру.

Второе, антибот-защита. Серьёзные коммерческие сайты не просто рендерят контент, они активно ищут автоматизацию. Безголовые браузеры по умолчанию раскрывают сигналы (флаг navigator.webdriver, отсутствующие или необычные отпечатки браузера, IP-адреса датацентров), которые системы обнаружения мгновенно считывают. В итоге приходится добавлять плагины для маскировки, ротировать резидентские прокси, чтобы запросы шли с IP реальных пользователей, и решать CAPTCHA, каждый из которых несёт свою постоянную нагрузку на обслуживание. Собственно парсинг перестаёт быть сложной частью.

Более полный обзор стратегий обхода блокировок см. в статье как парсить сайты, не попадая под блокировку. Если коротко: с рендерингом можно справиться самостоятельно, но надёжный рендеринг в масштабе с доверенных IP-адресов, совсем другая, значительно более сложная задача.

Однозапросная альтернатива: Crawling API с JS-токеном

Именно эту боль снимает управляемый API. Вместо того чтобы развёртывать и укреплять собственный парк браузеров, вы отправляете URL в Crawlbase Crawling API с JavaScript-токеном. API рендерит страницу в настоящем браузере на своей стороне, за пулом ротируемых доверенных резидентских IP-адресов, и возвращает готовый HTML для парсинга. Рендеринг и проблема IP сводятся к одному запросу.

Установите клиент и сделайте один вызов. Зарегистрируйтесь в Crawlbase, получите JavaScript-токен из панели управления и подставьте его вместо YOUR_CRAWLBASE_JS_TOKEN.

bash
npm install crawlbase cheerio
javascript
const { CrawlingAPI } = require('crawlbase')
const cheerio = require('cheerio')

const api = new CrawlingAPI({ token: 'YOUR_CRAWLBASE_JS_TOKEN' })

const options = {
  ajax_wait: true,
  page_wait: 5000,
}

async function scrape() {
  const response = await api.get('https://quotes.toscrape.com/js/', options)
  const $ = cheerio.load(response.body)

  const quotes = []
  $('.quote').each((i, el) => {
    quotes.push({
      text: $(el).find('.text').text().trim(),
      author: $(el).find('.author').text().trim(),
    })
  })

  console.log(quotes)
}

scrape().catch((err) => console.error(err))

Стратегии ожидания, изученные с Puppeteer, имеют прямые аналоги здесь. Опция ajax_wait указывает API дождаться асинхронного контента перед возвратом, а page_wait выдерживает фиксированное количество миллисекунд после загрузки, чтобы поздно рендерящиеся элементы появились. Для страниц, раскрывающих контент за кнопкой, css_click_selector принимает CSS-селектор в URL-кодировке и кликает по нему после рендеринга, та же идея, что page.click в Puppeteer с последующим ожиданием.

Why the JS token specifically

Crawlbase предлагает два типа токенов. Обычный токен получает статичный HTML; JavaScript (JS) токен сначала рендерит страницу в настоящем браузере. Для любой страницы с клиентским рендерингом, как в примере выше, нужен JS-токен. Обычный токен вернул бы ту же пустую оболочку, что и обычный fetch.

Crawlbase Crawling API

Рендерите страницы с активным JavaScript через доверенные резидентские IP в одном вызове. Crawling API принимает JS-токен, запускает страницу в настоящем браузере на сервере, ротирует IP за вас и возвращает готовый HTML, без необходимости держать собственный парк безголовых браузеров, пул прокси и стек для решения CAPTCHA. Попробуйте бесплатно.

Какой подход выбрать?

Оба подхода имеют своё место, и выбор в основном зависит от объёма и того, насколько активно цель защищается.

Используйте собственный безголовый браузер, когда нужен детальный контроль над страницей: сложные многоэтапные взаимодействия, вход в собственные аккаунты для тестирования, создание скриншотов для небольшого набора страниц или парсинг нескольких дружественных сайтов, где блокировка не является проблемой. Контроль здесь максимальный, а стоимость низкая при малом объёме.

Обращайтесь к управляемому Crawling API, когда вы парсите в больших объёмах, когда цель активно блокирует ботов, или когда вы просто не хотите поддерживать инфраструктуру браузеров и прокси. Если нужна чистая ротация IP без рендеринга, Smart AI Proxy покрывает это; если нужен разобранный JSON для поддерживаемых сайтов вместо сырого HTML, Crawling API также занимается извлечением. Главное, тратить время на данные, а не на поддержание парка браузеров.

Итоги

Ключевые выводы

  • Безголовый браузер выполняет полный рендеринг страницы. Он запускает JavaScript и строит настоящий DOM, видя контент, недоступный обычному HTTP-запросу.
  • Ждите селектора, а не таймера. waitForSelector и waitForFunction привязывают ожидание к нужным данным и значительно надёжнее фиксированного sleep.
  • Извлечение происходит в контексте страницы. page.evaluate (или Playwright's $$eval) выполняет DOM-код на странице и возвращает чистые структурированные объекты.
  • Масштабирование, это главная стоимость. Парки браузеров потребляют память, а антибот-защита добавляет необходимость маскировки, прокси и обработки CAPTCHA.
  • JS-токен решает обе проблемы. Crawling API рендерит на сервере за доверенными IP и возвращает готовый HTML в одном вызове.
  • Подбирайте инструмент под задачу. DIY для контроля при малом масштабе; управляемый API для объёмов и сложных целей.

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

Что такое безголовый браузер в веб-парсинге?

Безголовый браузер, это настоящий браузерный движок, например Chrome или Firefox, работающий без видимого окна. При парсинге сайтов он загружает страницу, выполняет JavaScript и строит тот же DOM, что видел бы реальный пользователь, что позволяет извлекать контент, появляющийся только после выполнения скриптов. Вы управляете им из кода, а не кликами, поэтому он идеален для JavaScript-сайтов, которые не может прочитать обычный HTTP-запрос.

Что лучше использовать для безголового парсинга: Puppeteer или Playwright?

Обе библиотеки отличны и очень похожи. Puppeteer ориентирован на Chrome и Firefox и прост в начале работы. Playwright управляет Chromium, Firefox и WebKit через единый API и имеет более мощное встроенное автоматическое ожидание, устраняющее большой объём ручного кода ожидания. Выбирайте Playwright для кроссбраузерного покрытия или если вам нравится его модель локаторов; выбирайте Puppeteer для минималистичной работы только с Chrome. Концепции этого руководства применимы к обоим.

Как лучше всего ждать контента в безголовом браузере?

Ждите конкретного элемента, а не фиксированного таймера. Используйте waitForSelector, чтобы заблокировать выполнение до появления нужного элемента в DOM, или waitForFunction для более сложного условия, например достижения списком определённой длины. Фиксированные паузы ненадёжны: слишком короткие, и вы парсите раньше времени; слишком длинные, и каждый запуск тянется дольше. Привязка ожидания к целевым данным и быстрее в среднем, и надёжнее.

Почему безголовые браузеры блокируются?

Безголовые браузеры по умолчанию раскрывают сигналы автоматизации: флаг navigator.webdriver, необычные или отсутствующие отпечатки, IP-адреса датацентров, которые системы обнаружения мгновенно помечают. Серьёзные сайты следят именно за этим. Противодействие требует настройки маскировки, ротации резидентских прокси для обеспечения реальных IP-адресов пользователей и обработки CAPTCHA, всё это постоянная работа. Управляемый API, рендерящий за доверенными IP, решает это за вас.

Можно ли делать скриншоты с помощью безголового браузера?

Да. Puppeteer и Playwright захватывают область просмотра или всю прокручиваемую страницу одним вызовом, что полезно для визуального контроля качества, архивирования и отладки пустых парсингов. Если скриншоты, ваша основная потребность в большом объёме, содержать парк браузеров только для изображений нецелесообразно; Screenshots API рендерит на сервере и возвращает изображение напрямую без управления браузером.

Когда следует использовать Crawling API вместо собственного браузера?

Используйте собственный безголовый браузер для детального контроля при малом масштабе или на дружественных сайтах. Переходите на Crawling API при парсинге в больших объёмах, при столкновении с агрессивной антибот-защитой или когда не хотите поддерживать инфраструктуру браузеров и прокси. С JS-токеном он рендерит страницу на сервере за ротируемыми резидентскими IP и возвращает готовый HTML в одном вызове, избавляя от необходимости в парке браузеров, пуле прокси и стеке для CAPTCHA.

Начать создавать

Обходите любой сайт в масштабе, без борьбы с инфраструктурой.

Crawlbase берёт на себя прокси, отпечатки и CAPTCHA, чтобы ваша команда выпускала конвейеры данных вместо поддержки обвязки краулинга. 1 000 запросов бесплатно, без карты.

Самообслуживание · Звонок отдела продаж не требуется · Доступны корпоративные объёмы краулинга