Постоянные контексты Playwright и то, чего они всё равно не дают
Если ваш скрипт на Playwright должен оставаться залогиненным между запусками, launchPersistentContext - встроенный ответ. Он действительно полезен, и начинать стоит именно с него. Он же - лишь половина того, что нужно личности, и не покрытая половина как раз та, из-за которой аккаунты получают пометки.
Что он делает
import { chromium } from 'playwright'
const context = await chromium.launchPersistentContext('./user-data/account-01', {
headless: false,
})
const page = context.pages()[0] ?? await context.newPage()
await page.goto('https://example.com')
Вместо свежего временного профиля Chromium пишет в названный вами каталог. Куки, localStorage, IndexedDB, service worker'ы и состояние расширений переживают процесс. Запустите снова с тем же путём - вы всё ещё залогинены.
Три особенности API, на которых спотыкаются
Он возвращает BrowserContext, а не Browser. Именно это ломает скрипты.
const context = await chromium.launchPersistentContext(dir)
context.browser() // null. Это не баг.
await context.close() // правильно
Любой помощник, написанный под browser.newContext(), придётся переписать. Контекст один, и это ровно то, что вам вернули.
Он открывается уже со страницей. Постоянный контекст восстанавливает свою сессию, поэтому context.pages() обычно не пуст. Слепой вызов newPage() оставит вам лишнюю пустую вкладку.
Блокировка каталога. Один процесс Chromium на один каталог данных пользователя. Запуск того же профиля дважды даёт невнятную ошибку, поэтому при параллельной работе - по каталогу на воркер.
Чего он не даёт
Стабильного отпечатка. Вот пробел, который имеет значение. Каталог профиля сохраняет состояние браузера, а не личность устройства. Хеш canvas, рендерер WebGL, список шрифтов, аудиостек и аппаратные свойства берутся из машины и сборки Chromium, а не из каталога.
Запустите тот же постоянный профиль на другой машине или после обновления Chromium - и сайт увидит те же куки, приходящие с другого железа. Это сочетание хуже чистого выхода из аккаунта: токен сессии предъявляет устройство, которому его не выдавали.
А если вы ведёте много личностей на одной машине, постоянные контексты дадут вам N наборов куки с одним и тем же отпечатком. Изоляция состояния, нулевая изоляция личности. Любая грамотная система оценки риска кластеризует их немедленно.
Привязки прокси и локали. Playwright принимает опцию proxy, но ничто не заставляет часовой пояс, локаль и геолокацию следовать за выходным IP. Можно выставить timezoneId и locale руками, и рано или поздно вы выставите что-то неверно, а это противоречие определяется дешевле самого прокси.
Аутентификатора WebAuthn. Passkey привязаны к аутентификатору, а у обычного постоянного контекста его нет. Он не может ни зарегистрировать ключ, ни воспроизвести его, поэтому любой аккаунт, перешедший на вход без пароля, становится недостижимым. Домен CDP WebAuthn в Playwright умеет добавить виртуальный аутентификатор, но ничто не сохраняет учётные данные вместе с профилем, так что они умирают вместе с контекстом.
Когда постоянных контекстов достаточно
- Одна личность, одна машина, цели, которые не снимают отпечатки агрессивно.
- Локальная разработка и тесты, где вам просто надоело входить заново.
- Всё короткоживущее, где аккаунт одноразовый.
Если это про вас, останавливайтесь здесь. Другой инструмент вам не нужен.
Когда недостаточно
Много личностей на одной машине; личность, которая должна выглядеть тем же устройством на разных машинах и после обновлений Chromium; аккаунты, перешедшие на passkey.
Именно это профили AntiBrow и добавляют сверху: отпечаток генерируется один раз на профиль и сохраняется рядом с куками, применяется в движке браузера, так что это не скрипт-подмена в странице; привязка прокси выводит часовой пояс, локаль и геолокацию из выходного IP; а у каждого профиля свой виртуальный аутентификатор, чьи учётные данные живут вместе с профилем.
API остаётся Playwright:
import { AntiDetectBrowser } from 'anti-detect-browser'
const ab = new AntiDetectBrowser({ key: 'your-api-key' })
const { browser, page } = await ab.launch({ profile: 'account-01' })
await page.goto('https://example.com')
Те же селекторы, те же проверки, те же трассировки. Отличается только строка запуска.
Проверьте разницу за десять минут
Запустите постоянный контекст, запишите хеш canvas и рендерер WebGL, закройте, откройте заново. Они должны совпасть - и совпадут, на той же машине. Теперь запустите второй постоянный контекст в другом каталоге и сравните: снова совпадают, и вот это и есть проблема.
Бесплатный тариф - это неограниченное число локальных профилей без карты, так что сравнение не стоит ничего.
Попробуйте на бесплатном тарифе.
Неограниченное число локальных профилей, без карты. Проверьте результат детекторами сами.