Как создать парсер на Playwright с Browser API
Строю backend, IT-инфраструктуру и automation-сервисы для масштабируемых SaaS-продуктов.
Пример разработки готового к использованию парсеру на Node.js и Playwright, который автоматически решает капчи, поддерживает стабильные браузерные сессии, извлекает реальные данные со страниц, при необходимости переключается между профилями и сохраняет понятную диагностику при ошибках.
Задача была простой: написать парсер на Node.js, открыть Google, найти нужный сайт, перейти по результату и собрать URL, title, H1, текст и ссылки.
Первая версия работала на Playwright с локальным Chromium.
js
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://www.google.com', {
waitUntil: 'domcontentloaded',
});
await page.getByRole('combobox').fill('2Captcha Browser API');
await page.keyboard.press('Enter');
await page.waitForURL(/\/search/);
console.log(await page.title());
await browser.close();
На обычных страницах этого вполне достаточно. Проблемы появились, когда Google начал периодически отдавать reCAPTCHA.
Один запуск проходил нормально, другой останавливался на проверке, третий попадал на другую версию страницы. Поведение зависело от IP, состояния профиля и предыдущих запусков.
Сначала пробовали увеличивать таймауты и добавлять retry. Это помогало только частично. Если браузер попал на капчу, waitForTimeout(10000) сам по себе ничего не решает.
В итоге локальный Chromium заменили на 2Captcha Browser API.
Подключение к Browser API
Перед запуском Playwright получаем connectionUri:
text
POST https://api.2captcha.com/browser/connection
js
const response = await fetch(
'https://api.2captcha.com/browser/connection',
{
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
key: process.env['2CAPTCHA_API_KEY'],
accountId: Number(process.env.BROWSER_ACCOUNT_ID),
profileId: process.env.BROWSER_PROFILE_ID,
}),
},
);
const { connectionUri } = await response.json();
connectionUri не стоит писать в логи. Он содержит данные, которые используются для подключения к браузеру.
Дальше Playwright подключается к уже запущенной сессии через CDP:
js
import { chromium } from 'playwright-core';
const browser = await chromium.connectOverCDP(connectionUri);
const context = browser.contexts()[0];
const page = await context.newPage();
Новый context здесь не создаем. Browser API уже запускает браузер с нужным профилем, поэтому берем существующий через browser.contexts()[0].
Решение капчи через CDP
После создания страницы открываем CDP-сессию:
js
const session = await page.context().newCDPSession(page);
await session.send('Captcha.setAutoSolve', {
autoSolve: true,
options: [{ type: '*' }],
});
После этого можно ждать событие завершения решения:
js
const solved = new Promise((resolve, reject) => {
session.once('Captcha.solveFinished', resolve);
session.once('Captcha.solveFailed', reject);
});
await page.goto('https://2captcha.com/demo/recaptcha-v2', {
waitUntil: 'domcontentloaded',
});
await solved;
После Captcha.solveFinished проверяем g-recaptcha-response и продолжаем сценарий:
js
const tokenExists = await page
.locator('textarea[name="g-recaptcha-response"]')
.evaluate((textarea) =>
Boolean(textarea.value && textarea.value.length > 0),
);
await page
.getByRole('button', { name: /check/i })
.click();
await page.screenshot({
path: 'artifacts/recaptcha-result.png',
fullPage: true,
});
console.log({ tokenExists });
Так надежнее, чем ставить фиксированную задержку и надеяться, что за это время капча уже решится.
Пул профилей
Еще один момент — параллельные запуски.
Один профиль поддерживает одно активное CDP-подключение. Кроме того, для каждого запуска нужен новый connectionUri.
Поэтому вместо одного BROWSER_PROFILE_ID можно использовать список:
js
function browserProfileIds() {
const pooled = process.env.BROWSER_PROFILE_IDS;
if (pooled) {
return pooled
.split(',')
.map((value) => value.trim())
.filter(Boolean);
}
return [process.env.BROWSER_PROFILE_ID];
}
Подключение к первому свободному профилю:
js
async function connectWithProfilePool({
apiKey,
accountId,
profileIds,
}) {
for (const profileId of profileIds) {
try {
const connectionUri = await getConnectionUri({
apiKey,
accountId,
profileId,
});
const browser =
await chromium.connectOverCDP(connectionUri);
return {
browser,
profileId,
};
} catch {
// Пробуем следующий профиль.
}
}
throw new Error(
'Could not connect to any Browser API profile',
);
}
Если один профиль занят, скрипт просто берет следующий.
Сбор данных
После перехода на нужную страницу сохраняем основные данные и несколько ссылок:
js
const data = await page.evaluate(() => {
const links = Array.from(
document.querySelectorAll('a[href]'),
)
.map((anchor) => ({
text:
anchor.textContent
?.replace(/\s+/g, ' ')
.trim()
.slice(0, 120) || '',
href: anchor.href,
}))
.slice(0, 25);
return {
url: location.href,
title: document.title,
h1:
document
.querySelector('h1')
?.textContent
?.replace(/\s+/g, ' ')
.trim() || null,
textPreview: document.body.innerText
.replace(/\s+/g, ' ')
.trim()
.slice(0, 700),
links,
};
});
Эти данные удобно использовать и для результата, и для отладки. Сразу видно, куда реально перешел браузер и что находилось на странице в момент выполнения скрипта.
Что сохранять при ошибке
При падении скрипта полезно сохранить хотя бы URL, текст ошибки и скриншот:
js
try {
// Основной сценарий.
} catch (error) {
await page.screenshot({
path: 'artifacts/error.png',
fullPage: true,
});
await writeFile(
'artifacts/error.json',
JSON.stringify(
{
ok: false,
message: error.message,
currentUrl: page.url(),
},
null,
2,
),
);
}
При этом в error.json и обычные логи не должны попадать:
- API key;
- пароли прокси;
- browser passwords;
connectionUri;- токены авторизации.
Иначе диагностические файлы сами становятся проблемой.
Итоговый сценарий
Сейчас парсер работает так:
- Проверяет переменные окружения.
- Получает новый
connectionUri. - Подключается через
chromium.connectOverCDP(). - Использует существующий browser context.
- Создает страницу.
- Включает
Captcha.setAutoSolve. - Открывает Google.
- Выполняет поиск.
- Переходит на результат с
2captcha.com. - Собирает URL, title, H1, текст и ссылки.
- Сохраняет скриншот.
- Открывает demo reCAPTCHA v2.
- Ждет
Captcha.solveFinished. - Проверяет
g-recaptcha-response. - Нажимает
Check. - Сохраняет результат.
Если что-то падает, дополнительно сохраняются error.png и error.json.
networkidle как основной критерий готовности страницы мы не используем. На современных сайтах фоновые запросы могут идти постоянно.
С waitForTimeout та же история: если можно дождаться локатора, смены URL или CDP-события, лучше ждать конкретное состояние.
После перехода на Browser API сам Playwright-код почти не изменился. Playwright по-прежнему отвечает за навигацию, локаторы, клики и получение данных. Browser API берет на себя браузерный профиль, удаленную сессию и обработку капчи через CDP.
В результате стало меньше случайных падений и намного проще разбирать проблемы.