Ранее мы уже упоминали разные web-драйвера и библиотеки с фреймворками для создания своих программ парсинга сайтов. В частности, к наиболее популярным решениям для управления headless-браузерами относятся Playwright, Selenium и Puppeteer. У каждого из них свои особенности и области применения. Как минимум присутствуют определённые требования по используемым языкам программирования.
В этой статье расскажем о библиотеке Puppeteer, о том, как она помогает с тестированием приложений и web-парсингом (скрейпингом сайтов). А также рассмотрим пошаговое руководство по написанию своего собственного парсера с Puppeteer и наиболее вероятные ситуации применения.
Puppeteer – это Node.js-библиотека для управления браузерами Google Chrome и Chromium по протоколу DevTool Protocol (сокращённо CDP). Библиотека имеет открытый исходный код и была представлена для демонстрации возможностей встроенного API веб-браузера (за её написанием стоит та же команда Google, что и отвечает за направление браузера, официальный сайт). До 2017 года Хромом можно было управлять только через сторонние библиотеки web-драйверов, так как собственного API-интерфейса у него не было. За организацию API обычно отвечали внешние программные решения, такие как Playwright и Selenium.
Подробное сравнение Playwright vs Puppeteer
После релиза CDP (Chrome Devtool Protocol) с браузером стало возможно общаться напрямую, например, с помощью специальных флагов и команд в консоли, а также с использованием API-интерфейса.
Чтобы показать возможности управления и была написана библиотека Puppeteer.
Чуть позже поддержка CDP появилась и в существовавших web-драйверах (Playwright, Selenium и пр.), а также стали релизиться другие высокоуровневые прослойки, такие как Chromedp.
Но сейчас не о них, а только о Puppeteer.
Ключевые возможности, которые предоставляет библиотека Puppeteer для web-скрапинга:
Обратите внимание! Каждый выпуск библиотеки тестирования и автоматизации web-скрапинга Puppeteer тесно связан с определённым выпуском браузера Chromium. Тут важно помнить, что Puppeteer – это эталонная реализация библиотеки для автоматизации браузера с задействованием его API. Отсюда и такой интересный подход.
Но из-за этого возникает ряд ограничений. Разработчики библиотеки концентрируют свои усилия только на одном языке программирования и не работают в сторону оркестрации, как это делают более продвинутые братья по цеху (Playwright и Selenium). Соответственно, Puppeteer подходит только для определённых узких задач. В нашем случае – для написания парсеров на языке JavaScript.
Начинать веб-скрапинг Puppeteer следует с установки и настройки среды NodeJS. Будем рассматривать процесс с привязкой к операционной системе Windows, как наиболее популярной платформы среди рядовых пользователей. Вместо этого вы можете установить Linux-дистрибутив (в том числе в среде Windows, например, через подсистему WSL или через платформы виртуализации, VMware, VirtualBox и т.п.) или обеспечить поддержку систем контейнеризации (например, Docker). Во многих Linux-системах установка NodeJS выполняется через штатный пакетный менеджер или через подсистему пакетов Snap.
Мы пойдём более простым путём – NodeJS, а соответственно и любой скрипт web-парсинга Puppeteer, может работать в Windows без слоёв виртуализации.
Скачайте инсталляционный пакет с официального сайта NodeJS. Там же есть примеры команд для установки через интерфейс консоли (PowerShell).
Если вам принципиально наличие менеджера версий NodeJS, то можно выполнить установку через nvm, fnm, Brew или Docker.
Путь установки NodeJS по умолчанию – «C:\Program Files\nodejs\». Но вы можете изменить его на своё значение.
Не забудьте разрешить установку менеджера пакетов NPM и добавление путей (PATH) в переменные среды окружения Windows.
Дождитесь окончания установки (скрипт может скачивать и доустанавливать разные библиотеки и программы, например, репозиторий Chocolatey, редактор Microsoft Visual Studio, среду исполнения .Net, язык программирования Python и т.п.).
Сначала создайте каталог, в котором будет храниться скрипт вашего Puppeteer-парсера:
cd c:\
mkdir puppeteer-web-scraping-script
cd puppeteer-web-scraping-script
Всё, мы в нужном каталоге. Теперь нужно проверить версию node и менеджера пакетов npm командами:
npm -v
node –version
Установите Puppeteer:
npm install puppeteer
В папке с вашим проектом появятся новые каталоги и файлы (в отдельной папке с модулями будет около 100 подпапок, в том числе @puppeteer, chromium-bidi, devtools-protocol, http-proxy-agent и другие).
В корне каталога проекта создайте текстовый файл, назовите его, например, «first-puppeteer-web-scraping-script». Смените расширение .txt на .js, должно получиться «first-puppeteer-web-scraping-script.js». Откройте этот файл в любом текстовом редакторе (мы используем Notepad++).
Наполните файл содержимым (скопируйте и вставьте код ниже):
const puppeteer = require('puppeteer')
// Подключаем (импортируем) библиотеку с "кукловодом"
async function run(){
// Главная функция будет работать асинхронно
const browser = await puppeteer.launch({
// Тут мы создаём экземпляр headless-браузера и загружаем его. Тоже асинхронно.
// Чтобы все процессы работы скрипта можно было увидеть "вживую" отключим режим скрытия.
headless: false,
// Для этого установим флаг в "false". Если вы хотите, чтобы браузер работал без отображения окон, в фоне, изменить флаг на "true"
ignoreHTTPSErrors: true,
// Тут мы просим браузер игнорировать ошибки подключения по протоколу HTTPS
})
// Открываем в браузере новую вкладку
let page = await browser.newPage();
// И просим открыть конкретный URL-адрес
await page.goto('http://httpbin.org/html', {
// Ждём, пока загружается DOM-структура
waitUntil: 'domcontentloaded',
});
// Выводим найденный HTML-контент в консоли
console.log(await page.content());
// Закрываем вкладку и браузер
await page.close();
await browser.close();
}
run();
Сохраните файл и запустите скрипт командой:
node first-puppeteer-web-scraping-script.js
Сначала откроется окно браузера, затем оно закроется и в консоли выведется HTML-содержимое страницы.
Всё, наш первый парсер отработал на 100%: он открыл браузер (вместо этого браузер может работать в фоне, без отображения графического интерфейса), перешёл на конкретную страницу и скопировал HTML-код.
Какие функции Puppeteer мы использовали:
Событие 'domcontentloaded' это штатное событие JavaScript, которое обозначает, что HTML-страница полностью загружена и браузер построил дерево DOM-структуры.
Вместо этого вы можете ждать загрузки определённого HTML-тега или селектора, а также выжидать время (таймаут). Например:
await page.waitForSelector('h2', {timeout: 3_000})
// Ждём заголовок H2 в течение 3 секунд
Лучшие прокси-серверы для доступа к ценным данным со всего мира.
Давайте немного усложним задачу и попробуем выбрать и сохранить только нужные нам элементы страницы.
Библиотека web-скрапинга Puppeteer поддерживает синтаксис анализатора XPath, поиск по имени, роли и тексту, а также поиск по префиксам и Shadow DOM (документация по синтаксису запросов).
Класс «page» в Puppeteer поддерживает большое количество встроенным методов. Среди них действия пользователя, создание сессий, запись Cookie, скриншотов, скринкастов, PDF-версий страниц, установка юзер-агента и т.п. Все подробности в официальной документации.
Нас пока интересуют только методы для поиска конкретных элементов:
У этих методов есть вариации с признаком “eval”, которые будут сначала исполнять JS-код страницы и только потом приступать к синтаксическому разбору.
Пример скрипта, который будет искать информацию о товарах на специальной тестовой странице:
const puppeteer = require('puppeteer')
// Подключаем (импортируем) библиотеку с "кукловодом"
async function run(){
// Главная функция будет работать асинхронно
const browser = await puppeteer.launch({
// Тут мы создаём экземпляр headless-браузера и загружаем его. Тоже асинхронно.
// Чтобы все процессы работы скрипта можно было увидеть "вживую" отключим режим скрытия.
headless: false,
// Для этого установим флаг в "false". Если вы хотите, чтобы браузер работал без отображения окон, в фоне, изменить флаг на "true"
ignoreHTTPSErrors: true,
// Тут мы просим браузер игнорировать ошибки подключения по протоколу HTTPS
})
// Открываем в браузере новую вкладку
let page = await browser.newPage();
// И просим открыть конкретный URL-адрес
// В нашем случае это пример страницы с карточками продуктов
await page.goto('https://web-scraping.dev/products', {
// Ждём, пока загружается DOM-структура
waitUntil: 'domcontentloaded',
});
// Для примера найдём первый заголовок h3 (он имеет класс "mb-0")
// ... и скопируем из него текст, он прописан в теге <a>
let textfirsturl = await (await page.$('.mb-0 a')).evaluate( node => node.innerText);
// заодно скопируем саму ссылку, она в том же теге
let firsturl = await (await page.$('.mb-0 a')).evaluate( node => node.getAttribute("href"));
// Выводим найденный HTML-контент в консоли
console.log("First Product:", textfirsturl, "Its URL:", firsturl);
// А тут найдём сразу все товары и ссылки
// По факту alllinks изначально является массивом, так как функция page.$$ всегда возвращает массив
let alllinks = await page.$$('.mb-0 a');
//
for (const link of alllinks){
console.log("Product:", await link.evaluate( node => node.innerText));
console.log("URL:", await link.evaluate( node => node.getAttribute("href")));
}
// Закрываем вкладку и браузер
await page.close();
await browser.close();
}
run();
Сохраняем и запускаем наш скрипт web-скрапинга Puppeteer.
Вот так будет выглядеть вывод в консоли:
Самая каверзная задача парсинга динамических сайтов (изменяющихся по JavaScript-событиям) – это обработка бесконечной прокрутки. Контент при каждой новой попытке скроллинга обновляется, так как в конец списка добавляются новые элементы.
Ниже пример нашего скрипта для парсинга списка отзывов с бесконечной подгрузкой.
Вы можете выставить свои параметры задержек и максимальное количество попыток прокрутки.
const puppeteer = require('puppeteer')
// Подключаем (импортируем) библиотеку с "кукловодом"
// Сначала опишем функцию, которая будет отвечать за обработку скроллинга
async function scrollPageDown(page) {
// Предыдущее значение высоты экрана
let prevHeight = -1;
// Максимальное количество итераций скроллинга (если страница перестанет подгружать данные раньше, то цикл завершится досрочно)
let maxScrolls = 50;
// Переменная для подсчёта сделанных итераций
let scrollCount = 0;
// Описываем логику функции
// Пока текущее количество итераций меньше максимального, выполняем попытки прокрутки
while (scrollCount < maxScrolls) {
// Скроллим страницу вниз через специальный метод для класса Page, в качестве параметра передаём значение текущей высоты элемента body
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
// Ждём пока страница загрузится, хотя бы 1 секунду, то есть 1000 миллисекунд
await new Promise(resolve => setTimeout(resolve, 1000));
// Вычисляем новую высоту тега body
let newHeight = await page.evaluate('document.body.scrollHeight');
// Если новое значение высоты равно предыдущему
if (newHeight == prevHeight) {
// Выводим текущее значение счётсчика скроллов
console.log("Scrolls Count:", scrollCount);
// Выходим из цикла
break;
}
// Делаем новое значение высоты body предыдущим
prevHeight = newHeight;
// Добавляем счётчик итераций скроллинга
scrollCount += 1;
}
};
// Тут описываем логику функции скрапинга с Puppeteer
async function parseReviews(page) {
// Ищем все элементы с классом testimonial, он используется в качестве корневого для каждого блока с рейтингом
let elements = await page.$$('.testimonial');
// Создаём массив для результатов
let results = [];
// Перебираем массив найденных элементов
for (let element of elements) {
//Ищем значение рейтинга, в нашем случае это пиктограммы звёзд
let rate = await element.$$eval('span.rating > svg', elements => elements.map(el => el.innerHTML))
results.push({
// Собираем текст отзыва из элемента с классом text
"text": await element.$eval('.text', node => node.innerHTML),
// Считаем количество звёздочек
"rate" : rate.length
});
}
// Возвращаем массив
return results;
};
// Тут опишем логику главной функции - что и когда запускать
async function run(){
// Загружаем экземпляр headless-браузера через Puppeteer
const browser = await puppeteer.launch({
// Для наглядности отключаем headless-режим
headless: false,
// Игнорируем ошибки HTTPS
ignoreHTTPSErrors: true,
});
// Открываем новую вкладку
page = await browser.newPage();
// Переходим на целевую страницу
await page.goto('https://web-scraping.dev/testimonials/');
// Сначала выполняем все процедуры скроллинга (пока не упрёмся в лимит итераций или пока страница не перестанем менять свою высоту)
await scrollPageDown(page);
// Создаём массив с отзывами и наполняем его данными (парсим с Puppeteer)
reviews = await parseReviews(page);
// Закрываем браузер
await browser.close();
// Массив выводим в консоли
console.log(reviews);
};
run();
После запуска скрипта парсинга Puppeteer, вывод в консоли будет примерно как на скрине ниже.
В нашем случае попытки прокрутки страниц закончились на 7 итерации.
Другая ситуация – обход списка страниц.
На тестовом целевом сайте страницы с перечнем товаров статичные, но присутствует деление на выборки по 10 элементов. Чтобы обойти эти списки напишем скрипт, который будет менять параметр пагинации и подставлять его в URL-адрес новой страницы.
Вот так будет выглядеть код нашего Puppeteer web-скрапера:
const puppeteer = require("puppeteer");
// Подключаем библиотеку Puppeteer
// Сначала описываем логику парсинга конкретной страницы
async function parseProducts(page) {
// Ищем все блоки с описаниями продуктов, это div-элемент с классами "row" и "product"
let boxes = await page.$$('div.row.product');
// Создаём массив
let results = [];
// Перебираем элементы в цикле
for(let box of boxes) {
results.push({
// Заголовок товара прячется за тегом "a", но собрать нужно только текстовое содержимое
"title": await box.$eval('a', node => node.innerHTML),
// Ссылка на страницу товара. Тот же тег "a", но уже собираем значение атрибута Href
"link": await box.$eval('a', node => node.getAttribute('href')),
// Цена товара, вынесена в блок div с классом price
"price": await box.$eval('div.price', node => node.innerHTML)
})
}
return results;
}
// Тут описываем логику основной функции парсинга с Puppeteer
async function run(){
// Загружаем headless-браузер
const browser = await puppeteer.launch({
// Делаем его видимым (отключаем headless-режим)
headless: false,
// Игнорируем ошибки HTTPS
ignoreHTTPSErrors: true,
});
// Открываем новую страницу
page = await browser.newPage();
// Созадём массив
data = [];
// Пока количество страниц к перебору менее 5... (то есть не более 4)
for (let i=1; i < 5; i++) {
//Переходим на страницу с нужным номером пагинации, номер берём из итерации цикла (какой цикл, такой и номер)
await page.goto(`https://web-scraping.dev/products?page=${i}`)
// Наполняем массив с описаниями товаров (выполняем функцию парсинга)
products = await parseProducts(page)
// Дополняем массив с данными массивом из функции парсинга
data.push(...products);
}
// Выводим массив в консоль
console.log(data);
// Закрываем браузер
browser.close();
}
run();
Чтобы сэкономить трафик и ускорить процесс загрузки страниц, вы можете отключить ненужный вам контент. Например, самый тяжёлый: изображения, видео, шрифты и т.п.
Блокировка осуществляется за счёт специальных констант:
const blockResourceType = ['здесь', 'список', 'типов', 'ресурсов', 'например', 'image', 'font', 'и прочие',];
const blockResourceName = ['здесь', 'список', 'ресурсов', 'например', 'cdn.api.twitter', 'fontawesome', 'google-analytics', 'googletagmanager',];
// Тогда headless-браузер нужно дополнительно настроить с помощью флагов
const page = await browser.newPage();
// А вот так выглядит активация перехвата запросов
await page.setRequestInterception(true);
// В этом случае мы получаем возможность инспектирования каждого запроса браузера
// И тогда только мы настраиваем логику: какие запросы браузеру отправлять, а какие нет
page.on('request', request => {
const requestUrl = request._url.split('?')[0];
if (
// Если тип ресурса в списке блокируемых…
(request.resourceType() in blockedResourceType) ||
// Или если конкретный ресурс в списке блокируемых…
blockResourceName.some(resource => requestUrl.includes(resource))
) {
// Исходящий запрос сбрасывается
request.abort();
// В остальных случаях
} else {
// Всё проходит как обычно
request.continue();
}
});
}
Наша команда поддержки поможет вам всегда оставаться онлайн и не останавливаться на достигнутом.
Тут всё максимально просто и понятно. Достаточно в качестве аргумента запуска браузера передать параметры прокси. Пример:
// Открываем новый экземпляр headless-браузера
const browser = await puppeteer.launch({
args: [ '--proxy-server=http://123.456.123.456:8080' ]
});
// А далее остальной код.
У такого подхода есть один минус – прокси прописывается на уровне всего браузера. Соответственно, чтобы пустить поток запросов через другой прокси-сервер, вам нужно открыть ещё один экземпляр браузера.
Существуют разные скрипты принудительной ротации прокси на уровне страниц и запросов, в том числе расширение proxy-chain для NodeJS.
Но мы рекомендуем более грамотное решение – использовать ротируемые прокси. Например, вы можете арендовать ротируемые мобильные, резидентные и серверные прокси у нас.
Тогда за ротацию прокси будет отвечать наш сервис. Backconnect-прокси подключатся к парсеру всего один раз. А в личном кабинете остаётся только определить логику ротации выходных IP-адресов: по времени или при каждом новом запросе.
Такой подход существенно снизит вероятность блокировки парсера и упростит процесс его написания.
Больше деталей в нашем отдельном материале о том, как парсить без блокировок.
Максимально краткие советы:
Из специфических рекомендаций для web-скрапинга Puppeteer:
Puppeteer – это действительно классный инструмент для написания своих скриптов автоматизации тестирования и для создания полнофункциональных парсеров сайтов. Под капотом есть всё необходимое для эмулирования поведения пользователей, для перехвата HTTP-запросов и блокирования ненужных ресурсов, для синтаксического анализа HTML-контента и даже для работы через прокси.
Но есть и нюансы – поддерживается только один язык программирования (JavaScript) и есть ограничения по привязке прокси к экземплярам браузеров.
Чтобы обеспечить максимальный комфорт парсинга без блокировок и оперативное масштабирование нужны специальные ротируемые прокси. А качественные мобильные и резидентные прокси с ротацией – это Froxy. Более 10 млн. IP в сети, ротация по времени и при каждом новом запросе.