Puppeteer: автоматизация браузера на Node.js — мастерская и 10 сценариев
Что можно автоматизировать с Puppeteer: личные кабинеты без API, сбор данных, формы, документы и отчёты. Объясняем возможности, разбираем 10 рабочих сценариев и даём проект с исходниками.

Puppeteer — библиотека для автоматизации браузера из Node.js. Она позволяет поручить коду работу с веб-сайтом: открыть личный кабинет, заполнить форму, собрать данные, скачать документы, сформировать PDF или проверить пользовательский маршрут. Браузер выполняет JavaScript страницы и взаимодействует с интерфейсом, а ваше приложение получает данные, файлы или подтверждение выполненной операции.
Инструмент пригодится, когда нужное действие доступно через сайт, но удобной интеграции нет, либо важен именно результат работы браузера. Например, разработчику CRM нужно обновлять статусы из внешнего кабинета, команде поддержки — получать отчёты по расписанию, а веб-разработчику — проверять формы и сохранять превью страниц. Ниже разберём, что умеет Puppeteer, где его использовать и как довести сценарий до проверяемого результата.
Разберём весь рабочий маршрут: от первого запуска до очереди заданий в сервисе. На странице есть живая мини-мастерская, а в исходниках — локальный стенд с десятью исполняемыми сценариями, собственными страницами, API и демонстрационными данными. Можно менять параметры, изучать код, смотреть результаты и затем запускать Chrome у себя.
Главная возможность: полноценный браузер под управлением кода
Puppeteer позволяет вашему коду работать с веб-сайтом через настоящий браузер. Node.js запускает Chrome на той машине, где выполняется сценарий: на компьютере разработчика, сервере приложения или отдельном исполнителе фоновых заданий. Браузер открывает страницу, загружает ресурсы, выполняет её JavaScript, строит интерфейс и хранит состояние сессии. Сценарий управляет этим процессом и проверяет, что получилось.
Поэтому выражение «просмотр веб-страницы кодом на сервере» хорошо передаёт идею. Это и чтение страницы, и взаимодействие с ней: ввести текст, выбрать пункт списка, нажать кнопку, дождаться следующего экрана, загрузить файл, прочитать изменившуюся таблицу. В режиме headless окно не показывается на рабочем столе, но браузер продолжает исполнять страницу и отрисовывать её. Для отладки можно включить видимое окно и наблюдать тот же маршрут.
Документация: Puppeteer: назначение и возможности, Современный headless и chrome-headless-shell, Locators и селекторы Puppeteer.
- 1. ЗадачаПриложение передаёт параметры: запрос, период, документ.
- 2. Сценарий Node.jsОписывает действия, ожидания и проверки результата.
- 3. Настоящий браузерОткрывает сайт, выполняет JavaScript, работает с интерфейсом.
- 4. Результат в приложенииДанные, файл, подтверждённое действие или понятная ошибка.
Чтение через DOM позволяет получить текущее состояние документа, включая данные, которые интерфейс загрузил и добавил позже. Например, page.evaluate() читает элементы внутри страницы, а page.screenshot() сохраняет то, как она выглядит. Это разные результаты одной работающей страницы: структурированные значения и изображение. Если интерфейс рисует график на Canvas, отдельных чисел может не быть в DOM — способ получения результата выбирают по устройству страницы.
| Что поручить браузеру | Как выглядит маршрут | Что получает ваше приложение |
|---|---|---|
| Работать с личным кабинетом | Войти → открыть раздел → выбрать период → прочитать данные | Заказы, статусы, остатки, начисления или другие доступные сведения |
| Получать документы | Найти документ → нажать скачивание → проверить файл | PDF, XLSX, CSV или другой файл, который отдаёт сайт |
| Выполнять операции через интерфейс | Заполнить поля → приложить файл → отправить → проверить подтверждение | Идентификатор созданной записи и подтверждённый статус |
| Искать и собирать сведения | Ввести запрос → применить фильтры → пройти страницы | Нормализованный список для поиска, сравнения или загрузки в свою систему |
| Формировать отчёты и превью | Открыть нужное состояние → дождаться данных и шрифтов → сохранить | Скриншот, PDF, превью карточки или отчёт |
| Проверять работу сервиса | Пройти пользовательский маршрут → проверить ответ и экран | Результат проверки, причина ошибки и диагностические материалы |
Эти действия можно соединять в процессы. Например: каждое утро войти в разрешённый кабинет, получить новые заказы, скачать связанные документы и передать всё в собственную CRM. Или по кнопке пользователя открыть нужную страницу отчёта, выставить параметры и вернуть PDF. Запуск по расписанию, очередь и сохранение результата организует ваше приложение; Puppeteer выполняет браузерную часть.
click() само по себе этого не подтверждает.Документация: JavaScript в контексте страницы, Скриншоты страниц и элементов, Загрузка локальных файлов.
Какие действия можно автоматизировать: страницы, вкладки, кнопки и формы
Рассмотрим Puppeteer через знакомые действия пользователя. Для каждого действия важны три вещи: какую часть интерфейса найти, что с ней сделать и по какому признаку понять, что задача выполнена. Один сценарий может сочетать все перечисленные действия и выбирать следующий шаг по полученному результату.
Переходить по страницам и возвращаться назад
Можно открыть известный адрес, перейти по ссылке, обновить документ или вернуться по истории вкладки. Это пригодится, когда сведения распределены по нескольким экранам: список → карточка → история изменений → обратно к списку. Например, сценарий проходит карточки заявок и собирает подробности, которых нет в общей таблице. После перехода он ждёт нужный заголовок, строку или другое подтверждение готовности данных.
Часть сайтов обновляет содержимое без загрузки нового документа. В таком интерфейсе нажатие на раздел может менять таблицу в той же странице. Поэтому маршрут описывают через ожидаемый результат действия, а не только смену адреса. Готовые примеры формы с переходом и обновления по API показывают оба варианта.
Документация: Возврат по истории страницы, Ожидание навигации, Ожидание ответа API.
Работать с несколькими вкладками и новыми окнами
Сценарий может открыть отдельную вкладку, получить список открытых страниц, дождаться страницы, появившейся после клика, и продолжить действия именно в ней. Например, общий список заказов остаётся открытым, а карточка заказа или печатный документ обрабатывается в другой вкладке. После чтения или скачивания ненужную вкладку закрывают и продолжают работу со списком. Так не приходится каждый раз восстанавливать поиск и фильтры.
У каждой страницы есть собственный объект Page. Код хранит ссылки на нужные страницы и обращается к ним напрямую; для большинства действий не требуется визуально переключать активную вкладку мышью. В видимом режиме page.bringToFront() помогает показать нужную страницу человеку. Вкладки одного контекста могут использовать общую сессию; для разных аккаунтов предусмотрены отдельные контексты.
Документация: Открытые страницы браузера, Вывод вкладки на передний план, Браузеры и изолированные контексты.
Нажимать кнопки, раскрывать меню и прокручивать страницу
Можно раскрыть меню, навести указатель на элемент, нажать кнопку или прокрутить страницу до нужного блока. Это полезно для интерфейсов, в которых действие появляется только после выбора строки или сведения спрятаны в раскрывающейся карточке. Например, открыть меню документа, выбрать выгрузку и дождаться файла. Элементы обычно находят по селектору, тексту или доступному имени; координаты мыши применяют там, где они действительно нужны.
Прокрутка помогает добраться до ленивой загрузки и длинных списков. Но один скролл не означает, что получены все записи: часть интерфейсов создаёт в DOM только видимые строки. Для полной выгрузки задают условие завершения, проходят страницы или порции данных и проверяют отсутствие повторов. В примере каталога разобран маршрут с пагинацией.
Вводить запросы, выбирать фильтры и заполнять формы
Сценарий может вводить текст в поля, выбирать значения списка, менять флажки, использовать клавиатуру и отправлять форму. Применение зависит от задачи: найти документ по номеру, выбрать период отчёта, войти в кабинет или подготовить заявку из данных вашей системы. После изменения фильтра он ждёт обновлённую таблицу; после отправки формы — номер записи или другое явное подтверждение.
Сложные календари и редакторы проходят теми действиями, которые поддерживает конкретный интерфейс. Важно запускать его обработчики ввода и учитывать валидацию. Простое присваивание значения HTML-полю не всегда равнозначно пользовательскому вводу: приложение может не обновить внутреннее состояние. Этому посвящены локаторы и поиск элементов и пример заполнения формы.
Документация: Locators и селекторы Puppeteer.
Читать результат действия и сохранять нужные данные
После завершения действия можно прочитать текст, атрибуты, ссылки, строки таблицы и значения полей, наблюдать сетевые ответы или проверить сообщение об ошибке. Например, поиск нашёл десять документов: сценарий собирает их номера, даты и ссылки в массив объектов, а приложение сохраняет этот массив и показывает его пользователю. Форматирование страницы — пробелы, символ валюты, подписи — преобразуют в согласованный формат данных с проверкой ошибок.
Puppeteer также позволяет выполнить JavaScript внутри страницы. Это удобно, когда нужно извлечь несколько полей сразу или проверить сложное условие готовности. Получение данных разбираем на каталоге, а передачу значений между Node.js и страницей — в разделе evaluate.
Загружать файлы, скачивать документы и сохранять внешний вид
Код может выбрать подготовленный файл для загрузки, дождаться ответа обработчика и получить подтверждение. В обратную сторону — нажать скачивание и сохранить документ, который выдаёт сайт. Это применяют при загрузке прайс-листов, приложении документов к заявкам и получении готовых выгрузок. Отдельные возможности — скриншот экрана и печать страницы в PDF: они сохраняют состояние интерфейса, даже если на сайте нет своей кнопки экспорта такого вида.
Проверяйте подходящий результат: завершённую загрузку, содержимое скачанного файла, выбранный период отчёта или наличие нужного блока на снимке. Готовые реализации находятся в разделах загрузки и скачивания, скриншотов и PDF.
Документация: JavaScript в контексте страницы, Загрузка локальных файлов, Скриншоты страниц и элементов, PDF и печатные стили.
Кому пригодится Puppeteer и с какой задачи начать
Хорошая первая задача — короткая повторяемая операция с понятным результатом. Оцените, сколько времени уходит на неё вручную, как часто она нужна и насколько сложно проверить успех. Получение отчёта или чтение списка обычно проще для первого внедрения, чем длинный процесс с несколькими изменениями данных.
| Кому пригодится | Подходящая первая задача | Практическая польза |
|---|---|---|
| Разработчику CRM или внутреннего сервиса | Обновлять сведения из доступного кабинета без подходящего API | Пользователь получает данные в своей системе без ручного переноса |
| Команде, которая регулярно готовит отчёты | Получать один и тот же документ за выбранный период | Повторяемая подготовка файла с известным временем формирования |
| Веб-разработчику и тестировщику | Проверять ключевую форму после изменения сайта | Быстрее замечать поломку пользовательского маршрута |
| Специалисту по данным | Выгружать разрешённый каталог с нескольких страниц | Получать структуру для дальнейшего анализа вместо ручного копирования |
Например, в CRM «Сфера» такой процесс может сформировать месячный отчёт или проверить создание заявки. Важно заранее определить успех: файл существует, итог совпадает с данными, заявка появилась с ожидаемым статусом. Сам факт клика по кнопке ничего этого не доказывает.
Документация: Puppeteer: назначение и возможности.
Мини-мастерская: попробуйте процесс на странице
В разделе «10 сценариев» выберите задачу и рассмотрите файлы настоящего запуска: например, десктопный и мобильный снимки или PDF счёта. Ниже расположена живая DOM-лаборатория: выберите CSS-селектор, найдите карточки, добавьте товар с задержкой и отправьте учебную форму. Здесь изменения происходят сразу в странице.
Открыть мастерскую на весь экран ↗ Скачать проект Node.js ZIP
В конструкторе «Соберите первый скрипт» переключите задачу, ширину окна, режим Chrome и таймаут. Параметры меняют создаваемый код; новый скриншот или PDF появится после его локального запуска по инструкции под кодом. Чтобы сравнить обычный и подменённый ответ API без установки, откройте сохранённые результаты соответствующих сценариев. Если встроенному окну тесно, отдельная вкладка оставляет больше пространства для кода и результата.
Учебные данные синтетические. Отправка формы не создаёт клиентскую заявку на этом сайте, а проверка файлов использует специально подготовленный пример. Так можно спокойно повторять действия и разбирать ошибки, не затрагивая рабочие записи.
Node.js → Browser → Context → Page: кто за что отвечает
| Уровень | Ответственность | Пример |
|---|---|---|
| Node.js | Запуск сценария, файлы, очередь, секреты, журнал | writeFile(), параметры задания |
| Browser | Процесс Chrome или подключение к нему | puppeteer.launch() |
| BrowserContext | Изолированное состояние сессии | browser.createBrowserContext() |
| Page | Вкладка: адрес, DOM, события, скриншот | context.newPage() |
| Frame | Документ внутри вкладки, включая iframe | page.waitForFrame() |
Команды отправляются браузеру по протоколу управления. Страница продолжает жить по своим правилам: загружает ресурсы, выполняет JavaScript, изменяет DOM. Поэтому сценарий состоит не просто из команд, а из чередования действия и проверяемого ожидания. Один браузер может обслуживать несколько контекстов; внутри каждого могут быть несколько вкладок.
Контекст помогает разделить пользователей и учебные роли. Он не является отдельной виртуальной машиной и сам по себе не ограничивает доступ браузера к сети сервера. Вкладки одного контекста, напротив, могут разделять состояние авторизации — это полезно для нескольких экранов одного пользователя.
Документация: Браузеры и изолированные контексты, Создание BrowserContext.
Установка: актуальная версия и совместимый браузер
Материал и проект используют Puppeteer 25.16.0; для этой версии нужен Node.js 22.12.0 или новее. Версии проверены 6 ноября 2026 года. Для нового окружения выбирайте поддерживаемую LTS-ветку Node, подходящую под это требование. Puppeteer распространяется по Apache-2.0; у приложения и дополнительных зависимостей свои лицензии.
Новый самостоятельный проект
node --version
npm --version
mkdir puppeteer-demo
cd puppeteer-demo
npm init -y
npm install puppeteer@25.16.0
npx puppeteer browsers install
Пакет puppeteer рассчитан на загрузку совместимого Chrome for Testing при установке. Если установочные скрипты отключены политикой пакетного менеджера, браузер нужно установить отдельной командой. puppeteer-core браузер не загружает: его выбирают для заранее подготовленного Chrome или удалённого подключения. Просто заменить имя пакета и убрать настройку браузера нельзя.
В репозитории сохраняют package-lock.json, а на другой машине используют npm ci. Это фиксирует зависимости проекта; браузерные файлы и системные библиотеки всё равно должны быть доступны конкретному окружению. Кэш загрузки Puppeteer и каталог профиля Chrome выполняют разные задачи.
Документация: Требования к Node.js и системе, Установка Puppeteer и puppeteer-core, Лицензия Apache-2.0.
Как запустить готовый проект и найти результаты
Распакуйте архив, откройте терминал в папке с package.json и установите зависимости. Для начала достаточно одного сценария. Команды scenario и all самостоятельно поднимают временный локальный стенд, поэтому отдельно запускать сервер перед ними не требуется.
Команды скачиваемой мастерской
npm ci
npx puppeteer browsers install
npm run scenario -- screenshot
npm run scenario -- pdf
npm run all
npm test
# Отдельный режим: локальный интерфейс запуска
npm start
# Откройте http://127.0.0.1:8790/
В архиве src/fixtures.mjs содержит учебные страницы и API, src/data.mjs — исходные данные, src/scenarios.mjs — сценарии, а src/cli.mjs — команды запуска. Каталог results/ хранит создаваемые файлы. Подробности режима запуска и требования перечислены в README проекта.
| ID сценария | Что проверяется | Что изучить в результате |
|---|---|---|
| screenshot | Дашборд при разных размерах окна | PNG и параметры съёмки |
| Печать учебного счёта | PDF, текст и итог документа | |
| catalog | Каталог из 12 товаров на трёх страницах | Данные без пропусков и дублей |
| form | Заполнение формы и переход после отправки | Значения на странице подтверждения |
| network | Загрузка показателей через API | URL, HTTP-статус, JSON и DOM |
| interception | Контролируемая подмена ответа | Отличие исходных и подставленных данных |
| contexts | Разные роли и темы в отдельных сессиях | Изоляция cookies и localStorage |
| files | Загрузка файла и скачивание CSV | Полученный файл и проверка содержимого |
| diagnostics | Учебные ошибки страницы и запросов | Разные типы ошибок в журнале |
| batch | Несколько заданий с ограничением параллельности | Результат каждой работы и освобождение ресурсов |
Первый самостоятельный скрипт: от страницы до PNG
Сохраните следующий код как first.mjs в новом проекте и выполните node first.mjs. Расширение .mjs включает ES-модули. Здесь страница создаётся из строки, поэтому пример не зависит от доступности внешнего сайта. В реальной работе вместо setContent() обычно используется goto().
Полный файл first.mjs
import puppeteer from 'puppeteer';
import {mkdir} from 'node:fs/promises';
await mkdir('results', {recursive: true});
const browser = await puppeteer.launch({headless: true});
try {
const page = await browser.newPage();
page.setDefaultTimeout(10_000);
await page.setViewport({width: 1200, height: 800});
await page.setContent(`<!doctype html><html lang="ru">
<meta charset="utf-8"><title>Первый отчёт</title>
<style>body{font:24px system-ui;padding:48px;background:#edf8f5}</style>
<h1>Учебный отчёт</h1><p>Обработано: <strong>12</strong> заявок.</p>
</html>`);
await page.screenshot({path: 'results/first.png', fullPage: true});
console.log(await page.title());
} finally {
await browser.close();
}
Вызовы возвращают Promise, поэтому await сохраняет нужный порядок: сначала появилась страница, потом сделан снимок. Блок finally закрывает браузер и при успехе, и при исключении. Если забыть его в длительной службе, неудачные задания постепенно оставят после себя процессы и вкладки.
launch, connect и режимы headless
launch() запускает браузер для вашего процесса; connect() подключается к уже запущенному экземпляру. Для первого знакомства удобен обычный Chrome с видимым окном. При повторяемом выполнении окно чаще не нужно. Современное headless: true использует headless Chrome; headless: "shell" выбирает отдельный старый shell с другими возможностями.
Режим для пошаговой отладки
// Запустить собственный браузер и увидеть его действия.
const browser = await puppeteer.launch({
headless: false,
slowMo: 120
});
try {
const page = await browser.newPage();
await page.goto('http://127.0.0.1:8790/fixture/dashboard');
} finally {
await browser.close();
}
Отдельный вариант для подготовленного удалённого браузера
import puppeteer from 'puppeteer-core';
const endpoint = process.env.BROWSER_WS_ENDPOINT;
if (!endpoint) throw new Error('Задайте BROWSER_WS_ENDPOINT');
const browser = await puppeteer.connect({browserWSEndpoint: endpoint});
try {
console.log(await browser.version());
} finally {
await browser.disconnect();
}
disconnect() разрывает подключение, сохраняя удалённый браузер работающим; close() завершает браузер. У рабочего пула заранее определяют, кому принадлежит его жизненный цикл. Адрес отладочного подключения предоставляет широкое управление браузером: его не публикуют вместе с фронтендом и не выводят в общий журнал.
Документация: Современный headless и chrome-headless-shell, Браузеры и изолированные контексты.
Как находить элементы: CSS, test-id и доступные имена
Селектор должен описывать устойчивый контракт. Для собственного приложения удобно выделять data-testid; для пользовательского действия — роль и доступное имя. Длинная цепочка div:nth-child(3) > span ломается после невинного изменения вёрстки. Уникальность важна: одинаковые кнопки в нескольких карточках требуют уточнения области поиска.
Действия через locator, чтение через $$eval
await page.locator('[name="name"]').fill('Анна Тестовая');
await page.locator('[name="email"]').fill('anna@example.test');
// Доступное имя берётся из семантики элемента и ARIA.
await page.locator('::-p-aria([name="Отправить заявку"][role="button"])').click();
// Обычная выборка читает DOM, но сама не ждёт его появления.
const count = await page.$$eval('.product-card', nodes => nodes.length);
Locator перед действием проверяет пригодность элемента, включая видимость и стабильность положения. Это не означает, что бизнес-данные уже готовы. Кнопка может быть доступна, пока таблица ещё содержит старые строки. После действия всё равно проверяют ожидаемый результат. У $() и $$() нет аналогичного автоматического ожидания.
Документация: Locators и селекторы Puppeteer.
Ожидания без гадания: навигация, DOM и API
Фиксированная пауза в две секунды оказывается слишком долгой на быстром компьютере и слишком короткой на медленном. Лучше ждать событие, которое действительно обозначает завершение работы. Для обычной формы это переход, для SPA — конкретный ответ API и обновлённый DOM, для отчёта — готовность данных и графика.
Правильный порядок при переходе после клика
// Сначала регистрируем ожидание, затем нажимаем кнопку.
const [response] = await Promise.all([
page.waitForNavigation({waitUntil: 'domcontentloaded', timeout: 15_000}),
page.locator('button[type="submit"]').click()
]);
if (response && !response.ok()) {
throw new Error(`HTTP ${response.status()}`);
}
await page.waitForSelector('[data-testid="success-name"]');
DOMContentLoaded означает готовность исходного документа к работе скриптов, но не завершение всех запросов приложения. load также не обещает готовность данных, запрашиваемых позднее. Сетевой покой полезен как дополнительный сигнал, однако повторяющиеся запросы могут мешать его наступлению, а тихая сеть не доказывает правильность экрана.
Собственный признак готовности интерфейса
await page.goto(`${base}/fixture/catalog`, {
waitUntil: 'domcontentloaded', timeout: 15_000
});
await page.waitForFunction(() =>
document.querySelector('#catalog')?.dataset.ready === 'true',
{timeout: 10_000}
);
В примерах далее page — открытая вкладка, base — адрес локального стенда, out — подготовленный каталог результата. Фрагменты показывают ключевой шаг; весь жизненный цикл, импорты и обработка ошибок находятся в скачиваемом проекте. Они не являются двадцатью независимыми файлами, запускаемыми без окружения.
Документация: Ожидание навигации, Ожидание сетевого покоя, Page.goto и HTTPResponse.
Сценарий 1. Скриншоты дашборда и отдельного блока
Запуск в скачанном проекте: npm run scenario -- screenshot. Результаты сохраняются в results/screenshot/.
Снимки нужны для превью, проверки вёрстки и истории состояния отчёта. Сначала задайте размеры окна, откройте страницу и дождитесь её данных. fullPage захватывает доступную длину документа; он не обязан сам загрузить все карточки бесконечного списка. Для проверки отдельного компонента удобнее сохранить только его область.
Снимок страницы и выбранной области
await page.setViewport({width: 1440, height: 1000, deviceScaleFactor: 1});
await page.goto(`${base}/fixture/dashboard`, {waitUntil: 'domcontentloaded'});
await page.evaluate(() => document.fonts.ready);
await page.screenshot({path: `${out}/dashboard.png`, fullPage: true});
const card = await page.waitForSelector('main');
try {
await card.screenshot({path: `${out}/main.png`});
} finally {
await card.dispose();
}
Для сравнения между запусками фиксируют данные, дату, размер окна, масштаб, шрифты и анимации. Иначе в diff попадут сегодняшнее время, другая переноска строки или промежуточный кадр. Скриншот сам по себе не выполняет визуальное сравнение: для проверки нужен эталон и выбранный способ оценки различий.
Документация: Скриншоты страниц и элементов.
Сценарий 2. PDF счёта с печатными стилями
Запуск в скачанном проекте: npm run scenario -- pdf. Результаты сохраняются в results/pdf/.
Puppeteer печатает HTML средствами браузера. Это удобно для счетов, актов, коммерческих предложений и отчётов, когда шаблон уже написан на HTML/CSS. В стенде есть отдельная страница счёта. Перед печатью проверяют её итог, чтобы красивый PDF не скрывал ошибку данных.
Печать документа в PDF
await page.goto(`${base}/fixture/invoice`, {waitUntil: 'domcontentloaded'});
await page.waitForSelector('[data-testid="invoice-total"]');
await page.emulateMediaType('print');
await page.pdf({
path: `${out}/invoice.pdf`,
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
margin: {top: '12mm', right: '12mm', bottom: '14mm', left: '12mm'},
waitForFonts: true,
timeout: 15_000
});
По умолчанию используется media print. Если нужен именно экранный вид, явно выберите screen. Параметр preferCSSPageSize отдаёт приоритет размеру из @page. Фон печатается только при соответствующей настройке. Ожидание шрифтов уже включено по умолчанию, но оно не заменяет ожидание графика, внешнего изображения или окончания вашего расчёта.
Печатный слой шаблона
@page { size: A4; margin: 12mm; }
@media print {
.screen-actions { display: none !important; }
.invoice-summary { break-inside: avoid; }
thead { display: table-header-group; }
body { print-color-adjust: exact; -webkit-print-color-adjust: exact; }
}
PDF нужно открыть и проверить глазами: обрезанные таблицы, пустые страницы и разрыв подписи не обнаруживаются проверкой размера файла. Для многостраничного отчёта испытайте длинные названия, много строк и отсутствие необязательных полей. У шаблонов колонтитулов свои ограничения; не рассчитывайте на наследование всей разметки основной страницы.
Документация: PDF и печатные стили, Формат, поля и шрифты PDF.
Сценарий 3. Каталог, пагинация и экспорт данных
Запуск в скачанном проекте: npm run scenario -- catalog. Результаты сохраняются в results/catalog/.
В учебном каталоге 12 товаров, по четыре на страницу. Задача — собрать все записи, сохранив числовую цену, идентификатор и остаток. Извлечение из DOM полезно, когда именно интерфейс является проверяемым объектом. Если сервис предоставляет подходящий API, для обычной выгрузки данных чаще удобнее запросить его напрямую.
Ограниченный сбор трёх страниц
const products = new Map();
await page.goto(`${base}/fixture/catalog`, {waitUntil: 'domcontentloaded'});
for (let pageNumber = 1; pageNumber <= 3; pageNumber++) {
await page.waitForFunction(expected => {
const catalog = document.querySelector('#catalog');
return catalog?.dataset.ready === 'true'
&& Number(catalog.dataset.page) === expected;
}, {timeout: 10_000}, pageNumber);
const rows = await page.$$eval('.product-card', cards => cards.map(card => ({
id: card.dataset.id,
title: card.querySelector('.product-title')?.textContent.trim() ?? '',
price: Number(card.dataset.price),
stock: Number(card.dataset.stock)
})));
for (const row of rows) {
if (products.has(row.id)) throw new Error('Повторный ID товара');
products.set(row.id, row);
}
if (pageNumber < 3) await page.locator('#next-page').click();
}
if (products.size !== 12) throw new Error('Получен неполный учебный каталог');
await writeFile(`${out}/catalog.json`, JSON.stringify([...products.values()], null, 2));
В реальном каталоге число страниц обычно приходит из интерфейса или API. Задайте верхний предел, проверяйте смену номера страницы и завершайте работу при отсутствии следующей. При бесконечной прокрутке дополнительно следите, появляются ли новые ID. Одна и та же последняя страница не должна удерживать сборщик в вечном цикле.
CSV для просмотра в табличной программе
function csvCell(value) {
let text = String(value ?? '');
// Текстовые значения не должны становиться формулами в таблице.
if (/^[=+@\-\t\r]/.test(text)) text = "'" + text;
return '"' + text.replaceAll('"', '""') + '"';
}
const rows = [...products.values()];
const csv = [['id', 'title', 'price', 'stock'],
...rows.map(row => [row.id, row.title, row.price, row.stock])]
.map(row => row.map(csvCell).join(';')).join('\r\n');
await writeFile(`${out}/catalog.csv`, '\uFEFF' + csv, 'utf8');
Это прикладное преобразование данных средствами Node, а не специальный метод Puppeteer. Уточняйте формат десятичного разделителя и CSV для получателя. Исходные JSON-данные удобнее сохранять рядом: так можно проверить, что отображаемые цены не превратились в непригодные для расчёта строки с символом валюты.
Документация: JavaScript в контексте страницы.
Сценарий 4. Форма и подтверждение результата
Запуск в скачанном проекте: npm run scenario -- form. Результаты сохраняются в results/form/.
Сценарий заполняет имя, адрес и услугу, отправляет форму локальному обработчику и проверяет страницу подтверждения. Так тестируют регистрацию заявки, настройки профиля и обычные административные формы. HTML-валидация, HTTP-ответ и появление результата — разные этапы, и каждый может завершиться по-своему.
Заполнение, отправка и проверка
await page.goto(`${base}/fixture/form`, {waitUntil: 'domcontentloaded'});
await page.locator('[name="name"]').fill('Анна Тестовая');
await page.locator('[name="email"]').fill('anna@example.test');
// Значение option берём из собственного учебного select.
const service = await page.$eval('[name="service"]', select =>
[...select.options].find(option => option.value)?.value
);
if (!service) throw new Error('В форме отсутствует услуга');
await page.locator('[name="service"]').fill(service);
await Promise.all([
page.waitForNavigation({waitUntil: 'domcontentloaded'}),
page.locator('button[type="submit"]').click()
]);
const name = await page.$eval('[data-testid="success-name"]', el => el.textContent.trim());
if (name !== 'Спасибо, Анна Тестовая.') throw new Error('Подтверждение содержит другое имя');
Для негативных случаев нужны отдельные ожидания: сообщение под полем, отсутствие перехода, сохранение введённых значений. Не ставьте ожидание навигации там, где приложение намеренно остаётся на форме. И не повторяйте отправку автоматически после таймаута: сервер мог принять данные, а ответ потеряться. Для повторяемых записывающих операций нужен предусмотренный сервером ключ идемпотентности.
Документация: Locators и селекторы Puppeteer, Ожидание навигации.
Сценарий 5. Нажатие кнопки, ответ API и обновлённые показатели
Запуск в скачанном проекте: npm run scenario -- network. Результаты сохраняются в results/network/.
В SPA адрес страницы нередко остаётся прежним: кнопка делает fetch, затем перерисовывает карточки. Здесь ожидание навигации вообще не подходит. Сначала подписываемся на нужный запрос, нажимаем кнопку, проверяем статус и структуру ответа, после чего ждём изменения интерфейса.
Ожидание конкретного ответа без гонки
await page.goto(`${base}/fixture/network`, {waitUntil: 'domcontentloaded'});
const [response] = await Promise.all([
page.waitForResponse(res => {
const url = new URL(res.url());
return url.origin === base && url.pathname === '/api/metrics'
&& url.searchParams.get('period') === 'week'
&& res.request().method() === 'GET';
}, {timeout: 10_000}),
page.locator('#load-data').click()
]);
if (!response.ok()) throw new Error(`Метрики: HTTP ${response.status()}`);
const metrics = await response.json();
await page.waitForSelector('#metrics[data-ready="true"]');
await writeFile(`${out}/metrics.json`, JSON.stringify(metrics, null, 2));
Не добавляйте условие status() === 200 в единственный фильтр, если хотите увидеть настоящую ошибку сервера. Иначе ответ 500 будет проигнорирован, а сценарий сообщит лишь о таймауте. После HTTP-успеха проверяется договорённая структура JSON: статус 200 не превращает произвольный объект в корректные показатели.
Документация: Ожидание ответа API.
Сценарий 6. Подмена API и проверка сложных состояний
Запуск в скачанном проекте: npm run scenario -- interception. Результаты сохраняются в results/interception/.
Перехват полезен, когда нужно воспроизводимо показать пустой список, необычную сумму, ошибку или медленный ответ. Та же идея разобрана в материале про Requestly; здесь подмена входит в автоматический сценарий. Она проверяет поведение клиента, но не подтверждает исправность настоящего API.
Вариант для проверки HTTP 503
await page.setRequestInterception(true);
const intercept = request => {
if (request.isInterceptResolutionHandled()) return;
const url = new URL(request.url());
const isMetrics = url.origin === base && url.pathname === '/api/metrics';
const action = isMetrics
? request.respond({
status: 503,
contentType: 'application/json',
body: JSON.stringify({error: 'Учебная ошибка сервера'})
})
: request.continue();
action.catch(error => console.error('Перехват:', error.message));
};
page.on('request', intercept);
try {
await page.goto(`${base}/fixture/network`);
await page.locator('#load-data').click();
await page.waitForSelector('#metrics[data-ready="error"]');
const message = await page.$eval('#network-status', el => el.textContent);
if (!message.includes('503')) throw new Error('Не показана ошибка 503');
} finally {
await page.setRequestInterception(false);
page.off('request', intercept);
}
После включения перехвата каждому запросу нужен исход: продолжить, ответить или прервать. Забытый continue() способен остановить загрузку страницы. Если обработчиков несколько, проверка isInterceptResolutionHandled() и выбранное действие должны идти без await между ними; после асинхронной работы состояние проверяют заново.
Отключение изображений может ускорить сбор текстовых данных, но испортить скриншот или PDF. Отключение CSS меняет размеры и видимость элементов. Поэтому оптимизацию ресурсов выбирают под цель задания и проверяют её эффект, а не добавляют в каждый скрипт автоматически.
Документация: Перехват запросов и обработчики.
Сценарий 7. Две сессии, cookies и localStorage
Запуск в скачанном проекте: npm run scenario -- contexts. Результаты сохраняются в results/contexts/.
В стенде один пользователь видит одну роль и тему, второй — другую. Контексты позволяют проверить это в одном процессе браузера, сохраняя сессии раздельными. Такой подход подходит для сравнения кабинета менеджера и администратора, языка или личных настроек.
Изолированная учебная сессия
const context = await browser.createBrowserContext();
try {
await context.setCookie({
name: 'role', value: 'manager',
domain: new URL(base).hostname, path: '/'
});
const page = await context.newPage();
await page.goto(`${base}/fixture/contexts`);
await page.evaluate(() => localStorage.setItem('theme', 'dark'));
await page.reload({waitUntil: 'domcontentloaded'});
const state = await page.evaluate(() => ({
role: document.querySelector('#role')?.textContent,
theme: document.querySelector('#theme')?.textContent
}));
console.log(state);
} finally {
await context.close();
}
В учебном примере cookie с ролью лишь показывает механизм. В рабочем приложении права проверяет сервер, а не произвольная строка в браузере. Настоящие session cookies, токены и профили содержат секреты: их нельзя добавлять в публичный ZIP, скриншоты или логи. У каждого автоматического пользователя должны быть отдельные необходимые ему права.
Для сохранения состояния между запусками можно выделить отдельный userDataDir. Это самостоятельный профиль автоматизации, а не папка вашего повседневного Chrome. Один профиль нельзя одновременно отдавать нескольким независимым процессам. HTTP Basic Authentication через page.authenticate() решает другую задачу и не заменяет вход через форму сайта.
Документация: Работа с cookies, Создание BrowserContext, LaunchOptions и профиль браузера.
Сценарий 8. Загрузка файла и настоящее скачивание CSV
Запуск в скачанном проекте: npm run scenario -- files. Результаты сохраняются в results/files/.
Загрузка выбирает файл на машине, где работает браузер, и передаёт его в обычный input. Перетаскивание картинки интерфейсу для этого не требуется. В стенде используется подготовленный учебный файл, ответ обработчика и CSV каталога. Для реальной формы проверяют ограничения размера, типа и сообщение после обработки.
Файл в input и ответ обработчика
const input = await page.waitForSelector('input#upload');
try {
await input.uploadFile(absoluteFilePath);
} finally {
await input.dispose();
}
const [response] = await Promise.all([
page.waitForResponse(res => new URL(res.url()).pathname === '/api/upload'
&& res.request().method() === 'POST'),
page.locator('#send-file').click()
]);
if (!response.ok()) throw new Error(`Загрузка: HTTP ${response.status()}`);
Разрешение скачивания в отдельный каталог
const context = await browser.createBrowserContext({
downloadBehavior: {policy: 'allow', downloadPath: absoluteDownloadDirectory}
});
try {
const page = await context.newPage();
await page.goto(`${base}/fixture/files`);
await page.locator('#download-csv').click();
const deadline = Date.now() + 10_000;
let downloaded = '';
while (Date.now() < deadline) {
try {
downloaded = await readFile(`${absoluteDownloadDirectory}/catalog.csv`, 'utf8');
if (downloaded.trim()) break;
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
if (!downloaded.trim()) throw new Error('CSV не скачан за 10 секунд');
// out — постоянная папка, отдельная от временной папки скачивания.
await writeFile(`${out}/catalog-downloaded.csv`, downloaded, 'utf8');
// Полный сценарий дополнительно сравнивает CSV с известным содержимым.
} finally {
await context.close();
}
Один клик ещё не подтверждает, что файл скачан. Исполняемый сценарий ждёт завершённый файл с ограничением времени, затем проверяет содержимое. Папка должна быть отдельной для задания, чтобы старый CSV не был принят за новый. Проверенные байты копируем в постоянную папку результатов до закрытия контекста: временные скачивания могут очищаться вместе с его состоянием. Высокоуровневые рецепты скачивания Playwright нельзя механически переносить в Puppeteer: это разные API.
uploadFile() принимает пути отдельными аргументами, например uploadFile(path1, path2). Перед вызовом полезно убедиться, что файл существует. При удалённом браузере особенно важно понимать, на какой машине находится путь. Нельзя возвращать пользователю произвольный файл хоста только потому, что он указал его имя в форме.
Документация: Загрузка локальных файлов, Настройки скачивания в контексте, DownloadBehavior: каталог и политика.
Сценарий 9. Ошибки страницы, HTTP и диагностика
Запуск в скачанном проекте: npm run scenario -- diagnostics. Результаты сохраняются в results/diagnostics/.
Ошибка JavaScript в странице, ответ HTTP 503 и невозможность подключиться к серверу — три разных события. Если записывать только общее «не получилось», исправлять нечего. В диагностическом сценарии специально создаются разные сбои, чтобы увидеть их в журнале и сопоставить с результатом.
Разные события — разные причины
const events = [];
page.on('console', msg => events.push({kind: 'console', level: msg.type(), text: msg.text()}));
page.on('pageerror', error => events.push({kind: 'pageerror', text: String(error)}));
page.on('requestfailed', request => events.push({
kind: 'requestfailed',
path: new URL(request.url()).pathname,
reason: request.failure()?.errorText
}));
page.on('response', response => {
if (response.status() >= 400) events.push({
kind: 'http', status: response.status(), path: new URL(response.url()).pathname
});
});
await page.goto(`${base}/fixture/diagnostics`);
await page.locator('#trigger-errors').click();
HTTP 404 или 503 — полученный ответ сервера, поэтому он не обязан появиться в requestfailed. Для него нужен обработчик response и анализ статуса. В журнале полезны ID задания, текущий шаг, длительность и безопасное описание ошибки. Полный URL может содержать токен, а console — данные формы; перед сохранением такие поля очищают.
Артефакт на месте сбоя
try {
await page.waitForSelector('[data-testid="result-ready"]', {timeout: 5000});
} catch (error) {
await page.screenshot({path: `${out}/failure.png`, fullPage: true}).catch(() => {});
await writeFile(`${out}/failure.json`, JSON.stringify({
step: 'result-ready', error: String(error),
path: new URL(page.url()).pathname
}, null, 2));
throw error;
}
Снимок ошибки может содержать персональные данные. Для рабочего сервиса определяют срок хранения и доступ к артефактам. Сохранение HTML полезно не всегда: в нём тоже могут оказаться скрытые значения. Диагностика должна помогать владельцу задания, а не превращаться в общую витрину содержимого кабинетов.
Документация: События страницы и сетевые ошибки.
Сценарий 10. Пакетная обработка без сотни вкладок одновременно
Запуск в скачанном проекте: npm run scenario -- batch. Результаты сохраняются в results/batch/.
Наивный Promise.all(urls.map(...)) запускает всю коллекцию разом. Для десятков тяжёлых страниц это легко упирается в память и лимиты сервера. Рабочий вариант использует ограниченное число исполнителей, отдельное состояние заданий и обязательное закрытие созданных контекстов.
Небольшой пул с результатом каждого задания
async function runPool(items, concurrency, execute) {
let cursor = 0;
const results = new Array(items.length);
const workers = Math.min(Math.max(1, concurrency), items.length);
await Promise.all(Array.from({length: workers}, async () => {
while (cursor < items.length) {
const index = cursor++;
try {
results[index] = {ok: true, value: await execute(items[index])};
} catch (error) {
results[index] = {ok: false, error: String(error)};
}
}
}));
return results;
}
const results = await runPool(jobs, 2, async job => {
const context = await browser.createBrowserContext();
try {
const page = await context.newPage();
page.setDefaultNavigationTimeout(15_000);
await page.goto(job.url, {waitUntil: 'domcontentloaded'});
return {id: job.id, title: await page.title()};
} finally {
await context.close();
}
});
Два исполнителя — учебная настройка, а не универсальная норма. Подберите лимит по измеренной памяти, времени и нагрузке конкретных страниц. Сохраняйте частичные результаты: ошибка одного адреса не должна стирать успешно выполненные задания. При падении процесса браузера пулу может понадобиться новый экземпляр, а не бесконечное повторение вызовов к закрытой вкладке.
Документация: Браузеры и изолированные контексты.
evaluate: почему переменная из Node не видна странице
Функция внутри evaluate() выполняется в браузере, а не в месте её объявления в Node. У неё есть document и window, но нет ваших Node-переменных, файловой системы и импортированного fs. Передавайте необходимые значения аргументами, а обратно возвращайте простые данные.
Передача аргумента между двумя средами
const minimumPrice = 5000;
const rows = await page.evaluate(minimum => {
return [...document.querySelectorAll('.product-card')]
.map(card => ({id: card.dataset.id, price: Number(card.dataset.price)}))
.filter(row => row.price >= minimum);
}, minimumPrice);
// Здесь снова Node.js, поэтому файл записываем уже после evaluate.
await writeFile(`${out}/expensive.json`, JSON.stringify(rows, null, 2));
Возвращать DOM-узел как обычный JSON бессмысленно: это объект браузера со своим окружением. Для длительной работы с ним нужен handle, который затем освобождают. Ещё проще вернуть ID, текст или числовое значение. Не собирайте JavaScript строкой из пользовательского ввода — аргументы функции яснее и не превращают текст в выполняемый код.
Документация: JavaScript в контексте страницы.
iframe, Shadow DOM и диалоги: отдельные границы
iframe содержит свой документ. Поиск через page.locator() не превращает его автоматически в часть основного DOM. Сначала найдите нужный Frame по устойчивому имени или адресу, затем работайте внутри него. Shadow DOM — другая граница: для открытых корней Puppeteer предоставляет расширенные селекторы.
Элементы внутри iframe и открытого Shadow DOM
// Дополнительный рецепт для страницы с iframe name="preview".
const frame = await page.waitForFrame(frame => frame.name() === 'preview');
await frame.locator('[data-testid="save"]').click();
// Дополнительный рецепт для собственного элемента с open shadow root.
await page.locator('settings-panel >>> button[data-action="apply"]').click();
Эти два фрагмента описывают отдельные случаи и требуют соответствующей разметки; их не нужно вставлять в каталог стенда. Закрытый shadow root нельзя считать доступным через тот же селектор. Перед автоматизацией сложного компонента полезно договориться о доступных именах и стабильных точках взаимодействия.
Осознанно отменяем учебное подтверждение
// Обработчик нужен до действия, вызывающего confirm/alert/prompt.
page.once('dialog', dialog => {
dialog.dismiss().catch(error => console.error(error.message));
});
await page.locator('[data-testid="delete-demo"]').click();
Нативный confirm() и HTML-модальное окно требуют разной работы: первый обрабатывается через событие dialog, второе — обычными селекторами. Если приложение открывает новую вкладку, работать следует с полученным Page этой вкладки; действия в исходной странице не переместятся туда сами.
Документация: Работа с iframe, Locators и селекторы Puppeteer, События страницы и сетевые ошибки.
Мобильная версия, lazy loading и стабильный кадр
Для адаптивности важно проверить несколько ширин, доступность меню, горизонтальные переполнения, длинные подписи и форму с ошибками. Puppeteer умеет менять viewport и эмулировать метрики устройства. Настраивайте их до навигации: некоторые приложения выбирают поведение только при первоначальной загрузке.
Проверка узкого экрана
await page.setViewport({
width: 390, height: 844, deviceScaleFactor: 1,
isMobile: true, hasTouch: true
});
await page.goto(`${base}/fixture/dashboard`, {waitUntil: 'domcontentloaded'});
const overflow = await page.evaluate(() =>
document.documentElement.scrollWidth > document.documentElement.clientWidth
);
await page.screenshot({path: `${out}/mobile.png`, fullPage: true});
if (overflow) throw new Error('Найдена горизонтальная прокрутка страницы');
Это эмуляция в используемом браузерном движке. Она не превращает Chrome в физический iPhone и не подтверждает поведение Safari, экранной клавиатуры и жестов настоящего устройства. Проверка overflow — полезный сигнал, но не вся оценка удобства: кнопка может помещаться на экране и при этом быть слишком маленькой для пальца.
Lazy loading требует отдельного маршрута прокрутки и ожидания конкретных элементов. Ограничьте число прокруток и общий срок выполнения. Для сравнимого кадра можно включить предусмотренный приложением режим без анимаций или prefers-reduced-motion, однако приложение должно действительно уважать эту настройку. Принудительное отключение анимаций не заменяет проверку их обычного поведения.
Документация: Эмуляция параметров устройства, Скриншоты страниц и элементов.
Таймауты, HTTP 429 и повтор только подходящих операций
Таймаут означает, что условие не наступило за отведённое время. Причиной может быть медленная сеть, неверный селектор, сломанная страница или ожидание не того события. Сначала сохраните диагностику. Бесконечно увеличивать лимит или повторять всё подряд — значит прятать проблему и делать сбой дороже.
Ограниченный повтор безопасного чтения
import {setTimeout as delay} from 'node:timers/promises';
async function retryRead(operation, attempts = 3) {
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await operation();
} catch (error) {
// retryable выставляет ваш классификатор конкретной ошибки.
if (!error.retryable || attempt === attempts - 1) throw error;
await delay(300 * 2 ** attempt + Math.floor(Math.random() * 100));
}
}
}
Такой helper сам не решает, какая ошибка временная. Для 429 учитывают Retry-After и лимиты сервиса; для 401/403 обычно исправляют авторизацию, а для пропавшего селектора — сценарий. Отправку заказа, оплату и изменение записи повторяют только с предусмотренной защитой от дублей. Даже при чтении сохраняйте общий срок задания, чтобы серия попыток не жила бесконечно.
У goto() может быть null вместо HTTPResponse, например при некоторых переходах внутри документа. Проверяйте ответ там, где он есть, и отдельно ожидаемое содержимое. При диагностике важнее точный шаг «ожидание таблицы», чем общее сообщение «Puppeteer не работает».
Документация: Page.goto и HTTPResponse, Ожидание ответа API.
Интеграция с сервисом без API: интерфейс становится точкой взаимодействия
У внешнего сервиса может не быть публичного API для нужной операции, хотя человек выполняет её в кабинете каждый день. Тогда браузерный сценарий позволяет связать этот сервис с вашим приложением: повторить разрешённый пользовательский маршрут и преобразовать результат в удобные данные. Так автоматизируют получение статусов, документов, отчётов, остатков, списков заявок и другие операции, доступные учётной записи.
- Получить задание. Например, обновить статусы заказов за неделю. Приложение определяет аккаунт, период и допустимые действия.
- Открыть сайт и авторизоваться. Сценарий находит поля, вводит учётные данные и дожидается признака успешного входа. Сохранённую сессию можно использовать повторно, пока она действительна.
- Пройти нужный маршрут. Открыть раздел, заполнить поиск, установить фильтр, раскрыть строку, перейти на следующую страницу или запросить документ.
- Получить и проверить результат. Прочитать актуальный DOM, дождаться ответа на действие или завершения скачивания. Проверить аккаунт, период, структуру данных и ожидаемое подтверждение.
- Передать результат в своё приложение. Вернуть объект из Node.js, сохранить данные в БД или отправить их во внутренний API. Пользователь видит уже привычный экран вашей системы.
Отсутствие API у внешнего сервиса не мешает вашему приложению иметь собственный API. Например, ваш backend ставит задание в очередь; Node.js с Puppeteer работает с внешним интерфейсом; затем backend получает структурированный результат. Не нужно передавать в приложение весь HTML: выделите нужные поля и сохраните также источник, время получения и результат проверки. Запуск может быть ручным, по расписанию или по событию — это выбор архитектуры приложения.
Для входа важно различать успешную авторизацию, неверный пароль, истёкшую сессию и дополнительное подтверждение. Cookies и другие данные сессии позволяют сохранять состояние, но не гарантируют бессрочный вход. Пароли и сессии хранят на серверной стороне с ограниченным доступом; их не включают в публичный код, логи и примеры. Если требуется действие человека — например, подтверждение входа, — процесс должен показать понятный статус и продолжиться после него, когда это допускает выбранный сценарий.
Документация: Locators и селекторы Puppeteer, Работа с cookies, Создание BrowserContext.
Такую интеграцию нужно сопровождать: сайт может изменить разметку, название кнопки или последовательность экранов. Используйте устойчивые признаки, проверяйте ожидаемую структуру и сохраняйте последнее корректное значение отдельно от состояния обновления. Если поле пропало или вход не состоялся, это ошибка получения данных; нельзя молча подставлять ноль или пустой список и перезаписывать ими результат.
Для действий, меняющих данные, отдельно продумайте повтор после сбоя. Если браузер потерял связь после отправки формы, запись уже могла сохраниться. Сначала проверьте, появилась ли она, и только затем решайте, нужно ли повторять операцию. Частоту запусков, доступные действия и подтверждения согласуют с правилами сервиса и правами учётной записи.
Как превратить сценарий в понятную функцию веб-сервиса
Представьте кнопку «Сформировать отчёт» в CRM. Пользователь выбирает период и состав разделов. Сервер проверяет права, сохраняет задание, а исполнитель строит документ в фоне. Интерфейс показывает, что работа принята, какой этап выполняется и где скачать результат. Если она завершилась ошибкой, пользователь видит причину и следующее действие.
| Состояние | Что видит человек | Что хранит сервер |
|---|---|---|
| В очереди | Задание принято, можно закрыть страницу | ID, автор, параметры, время создания |
| Выполняется | Загрузка данных / подготовка документа | Шаг, начало, срок выполнения |
| Готово | Скачать файл и увидеть время формирования | Ссылка, размер, срок хранения |
| Ошибка | Что не получилось и можно ли повторить | Код причины, попытка, диагностический ID |
| Отменено | Выполнение остановлено | Инициатор отмены и освобождение ресурсов |
Честный прогресс лучше выдуманных процентов: если продолжительность шага неизвестна, покажите его название. Скачивание должно проверять права на результат, а не только знание URL. Для долгой операции полезно восстановление состояния по ID после перезагрузки страницы. Кнопка повтора создаёт понятную новую попытку, не скрывая историю ошибки.
В PHP-проекте обычно удобнее отправлять задание отдельному Node-исполнителю через очередь или внутренний API. Долгий запуск Chrome внутри каждого HTTP-запроса занимает PHP worker и делает ответ зависимым от таймаута прокси. Материал про вызов Node.js из PHP помогает понять связь процессов, но публичному сервису всё равно нужны очередь, контроль входных параметров и учёт результатов.
Развёртывание: браузер, sandbox, сеть и стоимость задания
Chrome требует больше ресурсов, чем HTTP-клиент: памяти, процессов, шрифтов и системных библиотек. На сервере проверяют совместимость браузера, наличие зависимостей, права на рабочие каталоги и корректное завершение дочерних процессов. Официальный Docker-образ Puppeteer служит отправной точкой; параметры контейнера нужно согласовать с инфраструктурой.
Не добавляйте --no-sandbox как универсальное лекарство от ошибки запуска. Документация прямо рекомендует настроенный sandbox. Официальный пример контейнера использует --init для управления процессами и дополнительную capability для sandbox; это не повод переносить привилегии без разбора в любой хостинг.
В сервисе генерации документов безопаснее принимать ID отчёта и разрешённые настройки, а адрес страницы строить на сервере. Поле «введите любой URL» превращает браузер в сетевого клиента от имени инфраструктуры. Для такого продукта нужны ограничения исходящих соединений, адресов после редиректов и доступа к внутренним сервисам; одной проверки строкового префикса недостаточно.
Ограничивайте число одновременных заданий, длительность, размеры результата и срок хранения. Секреты передавайте через защищённую конфигурацию исполнителя, а не через публичный JavaScript. Периодически обновляйте браузер и зависимости, прогоняя фиксированные сценарии после обновления. Стоимость измеряйте временем и ресурсами реальных заданий — универсальной цифры «страниц в секунду» здесь нет.
Документация: Официальный контейнер Puppeteer, Диагностика запуска и sandbox.
Когда достаточно fetch, Cheerio или нужен Playwright
| Инструмент | Когда подходит | Чего сам не даёт |
|---|---|---|
| Node.js fetch / Guzzle в PHP | Нужен JSON API, файл или HTTP-запрос | Выполнение интерфейса браузером и скриншоты |
| Cheerio | Нужные данные уже есть в полученном HTML | Рендеринг CSS, выполнение JavaScript страницы |
| Puppeteer | Нужен управляемый Chrome/Firefox, PDF или сценарий браузера | Готовая продуктовая очередь и права пользователей |
| Playwright Test | Нужен комплекс E2E-тестов, отчёты и несколько движков | Бизнес-правила сервиса и его инфраструктура |
Puppeteer поддерживает Chrome и Firefox; считать его инструментом исключительно для одного старого Chromium уже неверно. Playwright Test включает собственный runner, проверки, изоляцию, параллельное выполнение и инструменты отчётности, а также Chromium, Firefox и WebKit. Выбор зависит от задачи: автоматическая печать и большой набор регрессионных тестов имеют разные требования.
Для обычного API-запроса браузер часто избыточен. У Node есть встроенный fetch; в PHP можно использовать Guzzle. Cheerio быстро разбирает разметку, но не исполняет JavaScript, который дорисовывает данные. Перед внедрением проверьте самый простой способ получить требуемый результат.
Документация: Встроенный fetch в Node.js, Cheerio: HTML без браузера, Playwright Test и браузерные движки, Puppeteer: назначение и возможности.
Частые вопросы и ошибки старых примеров
| Вопрос или симптом | Объяснение и действие |
|---|---|
| Кнопка нажалась, а результата нет | Дождитесь и проверьте бизнес-результат, а не только завершение click. |
| Скрипт случайно падает на переходе | Регистрируйте waitForNavigation до действия, вызывающего переход. |
| networkidle зависает | Выберите признак готовности данных или конкретный ответ API. |
| Страница получила 500, но goto завершился | Анализируйте HTTP-статус и ожидаемое содержимое. |
| После перехвата всё перестало загружаться | Каждый запрос должен получить continue, respond или abort. |
| Переменная undefined внутри evaluate | Передайте значение аргументом из Node в браузер. |
| PDF отличается от экрана | Проверьте print CSS, фон, формат, поля и шрифты. |
| Скачивание иногда возвращает старый файл | Используйте отдельный чистый каталог задания и проверку содержимого. |
| На третьем запуске заканчивается память | Закрывайте вкладки/контексты в finally и ограничьте параллельность. |
| Эмуляция телефона прошла | Дополнительно проверьте реальные устройства и нужные движки. |
Можно ли установить Puppeteer обычным script на WordPress? Страница браузера не может самостоятельно скачать и запустить ещё один Chrome. Есть ограниченный вариант клиентского Puppeteer API с подключением к отдельному браузеру через отладочный WebSocket, но это другая архитектура. Для нашего практикума выбран понятный Node-проект, а публичная мастерская работает с учебным DOM.
Что изменилось со времён старых руководств? Материал RUVDS на Хабре от 31 октября 2017 года послужил отправной точкой для темы. Его требования к Node и старые API не следует переносить в новый проект. Сейчас нужно сверяться с установленной версией, использовать актуальные методы ожидания и учитывать современный headless. Текущая статья и стенд написаны заново под указанные выше версии.
Может ли Puppeteer сделать всё за пользователя? Он выполняет запрограммированный маршрут и видит доступное браузеру состояние. Изменение интерфейса, дополнительное подтверждение или недоступный ресурс требуют обработки в сценарии. Автоматизацию применяют там, где разрешён доступ, и соблюдают ограничения конкретного сервиса; она не является обещанием обхода проверок или капчи.
Документация: Puppeteer API из клиентской страницы, Современный headless и chrome-headless-shell.
Исходники, порядок изучения и следующие шаги
Сначала выполните screenshot и pdf, откройте полученные файлы. Затем пройдите form и network, наблюдая разницу между переходом документа и обновлением SPA. После этого изучите перехват, изоляцию сессий и файловые операции. Пакетную обработку добавляйте, когда один сценарий уже даёт стабильный проверяемый результат.
Скачать исходники и локальный стенд Вернуться в мини-мастерскую ↗
Для собственной задачи замените учебную страницу своим шаблоном, определите устойчивые селекторы и критерии успеха, затем добавьте диагностический файл при сбое. Сохраняйте небольшой известный набор данных для повторной проверки. Так скрипт постепенно становится обслуживаемым инструментом, результаты которого можно объяснить и воспроизвести.
Для дальнейшего разбора пригодятся Chrome DevTools, подмена API с Requestly и настройки и секреты через .env в PHP. Основные API Puppeteer сверяйте с официальной документацией соответствующей версии; ссылки на неё размещены рядом с примерами.


