title: "Browser Agents: Playwright + LLM, Browser-Use и дерево доступности" slug: browser-agents-playwright-llm-2026-ru date: 2026-02-21 lang: ru tags: [browser-agents, playwright, llm, automation, browser-use, accessibility-tree] description: "Глубокий разбор production browser agents: библиотека Browser-Use, Playwright + Claude/GPT-4o vision, навигация по дереву доступности, заполнение форм, многошаговые веб-задачи, восстановление после ошибок и обработка аутентификации."
Browser Agents: Playwright + LLM, Browser-Use и дерево доступности
Ключевые факты
- playwright==1.58.0 (последняя версия по состоянию на март 2026, требует установки системного браузера через
playwright install chromium) - browser-use==0.12.0 (открытая высокоуровневая библиотека для LLM-управляемой автоматизации браузера)
- langchain-anthropic==1.3.4 (интеграция Claude с Browser-Use)
- langchain-community==0.4.1 (базовый пакет LangChain)
- anthropic==0.84.0 (официальный Anthropic SDK для прямых вызовов Claude API)
- Цены Claude Opus 4: ~$15 за 1M входящих токенов, ~$75 за 1M исходящих токенов (смешанная стоимость ~$0.015 за 1000 токенов)
- Цены GPT-4o vision: ~$2.50 за 1M входящих токенов, ~$10 за 1M исходящих токенов
- Видимая область по умолчанию: 1920×1080 для desktop-агентов, 1280×720 для более быстрой обработки скриншотов
- Типичные значения timeout: загрузка страницы 30000ms, взаимодействие с элементом 5000ms, networkidle 10000ms
- Размер скриншота: качество JPEG 85, только видимая область (не полная страница) уменьшает с ~200KB до ~50KB
- Эффективность токенов дерева доступности: 1200 токенов на страницу против 8500 токенов для vision-подхода
- Задержка Vision: ~2800ms на страницу против ~650ms для дерева доступности
- Аргументы Headless Chrome:
--no-sandbox,--disable-dev-shm-usage,--disable-blink-features=AutomationControlled - User-Agent для скрытности:
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 - Максимум шагов для ReAct цикла: 30-50 итераций перед timeout/отказом
- Задержка решения reCAPTCHA: 15-120 секунд через сервисы anti-captcha
- Стоимость Anti-captcha API: ~$2.99 за 1000 решённых CAPTCHA
- Сохранение сессии: сохраняйте JSON
storage_state()чтобы избежать повторной аутентификации - Ограничение скорости: 1-2 запроса в секунду для избежания обнаружения агрессивного скрейпинга
- Стоимость за 1000 спарсенных страниц: ~$0.32 (дерево доступности) vs ~$2.40 (vision)
- Browser agent vs scraper: используйте агентов когда структура страницы часто меняется или требуется многошаговая навигация
- Browser agent vs RPA: агенты адаптируются к изменениям UI без хрупких селекторов; RPA лучше для pixel-perfect рабочих процессов
- Production отказы (по частоте): timeout навигации, элемент не найден, CAPTCHA вызовы, истечение аутентификации
- Облачные sandbox опции: Browserbase ($49-199/mo), Steel ($29-99/mo), Hyperbrowser ($79-199/mo)
Что такое Browser Agent
Browser agent — это ReAct-style (Reason + Act) управляющий цикл, который объединяет LLM с фреймворком автоматизации браузера (обычно Playwright). В отличие от традиционных веб-скрейперов, которые опираются на фиксированные CSS селекторы или XPath выражения, browser agents понимают намерение, адаптируются к изменениям структуры страницы, восстанавливаются после ошибок и выполняют многошаговые рабочие процессы, указанные на естественном языке.
Основная архитектура:
- Observe: Захватите текущее состояние браузера через скриншот (vision-based) или дерево доступности (structure-based)
- Think: Передайте наблюдение LLM с целью и историей действий
- Act: LLM возвращает структурированное действие (click, type, navigate, scroll, extract)
- Execute: Playwright выполняет действие в реальном браузере
- Repeat: Продолжайте до достижения цели или превышения максимума шагов
Screenshot-Action цикл (Vision-Based):
Browser page → Screenshot (base64 JPEG) → LLM vision model →
Structured action {action: "click", x: 450, y: 200} →
Playwright executes → New page state → Loop
Accessibility Tree цикл (Structure-Based):
Browser page → Accessibility tree extraction → Compact text representation →
LLM text model → Structured action {action: "click", element_id: 12} →
Playwright executes → New page state → Loop
Vision-based агенты работают на любой странице (включая canvas элементы, изображения, плохо доступные SPA) но стоят в 7 раз дороже за страницу и имеют в 4 раза более высокую задержку. Агенты на основе дерева доступности быстрее и дешевле, но не работают на canvas-базированных приложениях (Figma, Google Maps) и React приложениях с грязной разметкой без ARIA атрибутов.
Фреймворк решений
Когда использовать Browser Agent:
- Структура страницы часто меняется (страницы конкурентов с ценами, e-commerce сайты)
- Многошаговые рабочие процессы с условной логикой (если товар нет в наличии, выберите альтернативу)
- Заполнение форм с динамической валидацией (поля появляются/исчезают на основе предыдущих вводов)
- Требуется человеческое рассуждение ("найти самый дешёвый план с минимум 10GB памяти")
Когда использовать вместо этого Selenium/Playwright scraper:
- Статическая структура страницы, которая редко меняется (страница компании, о нас)
- Простая экстракция данных из известных селекторов (цена из
#product-price) - Высокообъёмный скрейпинг где стоимость LLM за страницу неприемлема (>10,000 страниц/день)
- Требования по задержке менее 500ms на страницу
Когда использовать вместо этого RPA (UI.Vision, Automation Anywhere):
- Десктопные приложения, а не только веб-браузеры
- Требуется pixel-perfect сопоставление скриншотов для визуальной верификации
- Рабочий процесс должен выполняться идентично каждый раз (требования соответствия)
- Наследованные системы без API или доступа к структурированному HTML
Матрица решений:
- Статическая структура страницы + известные селекторы → Playwright scraper
- Структура страницы часто меняется → Browser agent (дерево доступности)
- Требуются canvas/визуальные элементы → Browser agent (vision)
- Требуется автоматизация десктопа → RPA
- Многошаговый с условной логикой → Browser agent
Таблица справочных параметров
| Параметр | Типичное значение | Примечания |
|---|---|---|
viewport.width |
1920 | Стандартная ширина desktop, используйте 1280 для более быстрых скриншотов |
viewport.height |
1080 | Стандартная высота desktop, используйте 720 для более быстрых скриншотов |
headless |
True |
Production по умолчанию; False для отладки |
timeout (page load) |
30000 | Миллисекунды; увеличивайте до 60000 для медленных сайтов |
timeout (element) |
5000 | Миллисекунды; ожидание появления элемента |
screenshot_quality |
85 | JPEG качество (0-100); более низкое уменьшает количество токенов |
screenshot_type |
'jpeg' |
Используйте 'png' для pixel-perfect захватов (редко) |
full_page |
False |
True захватывает всю прокручиваемую область (дорого) |
max_steps |
30-50 | ReAct цикл итерации перед отказом |
temperature |
0.0 | LLM детерминизм; используйте 0.0 для production агентов |
max_tokens |
1024-4096 | LLM output; 1024 достаточно для действий, 4096 для экстракции |
user_agent |
Mozilla/5.0...Chrome/120.0.0.0 |
Скрытность; избегайте обнаружения как бота |
locale |
'en-US' |
Локаль браузера для согласованного рендеринга |
timezone_id |
'America/New_York' |
Избегайте fingerprinting по временной зоне |
wait_until (navigate) |
'networkidle' |
Опции: 'load', 'domcontentloaded', 'networkidle' |
delay (typing) |
50 | Миллисекунды между нажатиями клавиш; имитирует человеческую печать |
slow_mo (debug) |
500 | Миллисекунды; замедляет все действия для визуальной отладки |
Частые ошибки
Ошибка 1: Использование полностраничных скриншотов для каждого действия
Симптом: Высокая задержка (3-5 секунд на действие), чрезмерные стоимости LLM, превышение лимитов токенов
Влияние: Стоимость в 7 раз выше за страницу, задержка в 4 раза выше, отказ на длинных страницах, превышающих лимиты vision токенов
❌ Неправильно:
async def capture_screenshot(page: Page) -> str:
screenshot_bytes = await page.screenshot(
full_page=True, # НЕПРАВИЛЬНО: захватывает всю прокручиваемую страницу
type='png', # НЕПРАВИЛЬНО: PNG в 3-5 раз больше JPEG
quality=100, # НЕПРАВИЛЬНО: ненужное качество
)
return base64.b64encode(screenshot_bytes).decode('utf-8')
✅ Правильно:
async def capture_screenshot(page: Page) -> str:
screenshot_bytes = await page.screenshot(
full_page=False, # Только видимая область: 50KB vs 200KB
type='jpeg', # JPEG сжатие
quality=85, # Оптимальный компромисс качество/размер
)
return base64.b64encode(screenshot_bytes).decode('utf-8')
Ошибка 2: Клик по пиксельным координатам на vision-based агентах
Симптом: Клики промахиваются по целевым элементам, особенно на отзывчивых страницах или разных размерах viewport
Влияние: Высокий процент отказов действий на coordinate-based кликах, требует retry логика, тратит вызовы LLM
❌ Неправильно:
async def execute_action(page: Page, action: dict) -> str:
if action.get('action') == 'click':
# НЕПРАВИЛЬНО: пиксельные координаты, предоставленные LLM, неточны
await page.mouse.click(action['x'], action['y'])
return f"Clicked at ({action['x']}, {action['y']})"
✅ Правильно:
async def execute_action(page: Page, action: dict) -> str:
if action.get('action') == 'click':
# Используйте индекс дерева доступности или текстовый локатор
if 'element_id' in action:
selector = element_map[action['element_id']]
locator = page.locator(selector).first
await locator.scroll_into_view_if_needed()
await locator.click(timeout=5000)
elif 'text' in action:
locator = page.get_by_text(action['text'], exact=False).first
await locator.click(timeout=5000)
return f"Clicked element: {action.get('element_id', action.get('text'))}"
Ошибка 3: Несохранение состояния аутентификации между сессиями
Симптом: Агент входит заново на каждый запуск, вызывает ограничения скорости, встречает CAPTCHA, тратит 10-30 секунд на сессию
Влияние: Выполнение в 2-3 раза медленнее, вызовы CAPTCHA, блокировка аккаунтов от подозрительных паттернов входа
❌ Неправильно:
async def run_agent_task(task: str, credentials: dict) -> str:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# НЕПРАВИЛЬНО: свежий вход каждый раз
await page.goto("https://example.com/login")
await page.fill('#email', credentials['email'])
await page.fill('#password', credentials['password'])
await page.click('[type="submit"]')
await page.wait_for_url('**/dashboard')
# ... выполнить задачу
✅ Правильно:
async def run_agent_task(task: str, auth_state_path: str) -> str:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
# Загрузите сохранённое состояние аутентификации
if Path(auth_state_path).exists():
context = await browser.new_context(
storage_state=auth_state_path
)
page = await context.new_page()
await page.goto("https://example.com/dashboard")
# Проверьте, что аутентификация всё ещё действительна
if await page.query_selector('a[href="/logout"]'):
# ... выполнить задачу
return
# Вход только если аутентификация истекла
context = await browser.new_context()
page = await context.new_page()
# ... выполнить вход один раз
await context.storage_state(path=auth_state_path)
Ошибка 4: Использование wait_for_load_state('load') после навигации
Симптом: Содержимое страницы неполное, динамические элементы отсутствуют, действия отказывают сразу после навигации
Влияние: Значительно более высокий процент отказов на SPA, требует retry логика, непостоянное поведение
❌ Неправильно:
async def navigate_and_act(page: Page, url: str):
await page.goto(url, wait_until='load') # НЕПРАВИЛЬНО: срабатывает когда HTML загружен
# На этом этапе JavaScript может всё ещё инициализироваться
await page.click('#dynamic-button') # ОТКАЗ: элемент ещё не отрендерен
✅ Правильно:
async def navigate_and_act(page: Page, url: str):
await page.goto(url, wait_until='networkidle') # Все сетевые запросы завершены
# Или ожидайте конкретного элемента:
await page.wait_for_selector('#dynamic-button', state='visible', timeout=10000)
await page.click('#dynamic-button')
Ошибка 5: Отсутствие обработки неожиданных попапов и диалогов
Симптом: Агент застревает ожидая действие, не обнаруживает модальные окна, блокирующие взаимодействие
Влияние: Значительная доля production запусков прерывается cookie баннерами, newsletter попапами и chat виджетами
❌ Неправильно:
async def execute_step(page: Page, action: dict):
# НЕПРАВИЛЬНО: предполагает что страница готова к взаимодействию
await page.click(action['selector'])
✅ Правильно:
async def execute_step(page: Page, action: dict):
# Проверьте наличие блокирующих диалогов перед любым действием
dialog_selectors = [
'[role="dialog"]',
'[role="alertdialog"]',
'[aria-modal="true"]',
'.modal.show',
'#cookie-banner',
]
for selector in dialog_selectors:
dialog = await page.query_selector(selector)
if dialog and await dialog.is_visible():
# Попытайтесь закрыть
close_btn = await dialog.query_selector(
'button[aria-label*="Close"], button[aria-label*="close"], .close, [data-dismiss]'
)
if close_btn:
await close_btn.click()
await asyncio.sleep(0.5)
else:
await page.keyboard.press('Escape')
await asyncio.sleep(0.5)
# Теперь выполните предполагаемое действие
await page.click(action['selector'])
Реализация
Browser-Use: Высокоуровневая библиотека
Browser-Use предоставляет production-ready абстракцию над Playwright + LLM интеграцией:
pip install browser-use==0.12.0 playwright==1.58.0 langchain-anthropic==1.3.4
playwright install chromium
Базовое использование:
import asyncio
from browser_use import Agent
from langchain_anthropic import ChatAnthropic
async def run_browser_agent(task: str) -> str:
"""Запустите browser agent для данной задачи."""
llm = ChatAnthropic(
model="claude-opus-4-6",
temperature=0,
max_tokens=4096,
)
agent = Agent(
task=task,
llm=llm,
use_vision=False, # Дерево доступности по умолчанию
vision_fallback=True, # Используйте vision если tree недостаточно
)
result = await agent.run()
return result
# Экстрактируйте данные ценообразования с сайта конкурента
result = asyncio.run(run_browser_agent(
"Go to competitor.com/pricing, extract all plan names and prices as JSON"
))
Пользовательские действия с Browser-Use:
from browser_use import Agent, Controller
from browser_use.browser.context import BrowserContext
from pydantic import BaseModel
controller = Controller()
class LoginCredentials(BaseModel):
username: str
password: str
@controller.action(
"Login to the application with provided credentials",
param_model=LoginCredentials
)
async def login_action(
params: LoginCredentials,
browser: BrowserContext
) -> str:
"""Пользовательское действие входа с правильной обработкой учётных данных."""
page = await browser.get_current_page()
await page.wait_for_selector('#username', timeout=10000)
await page.fill('#username', params.username)
await page.fill('#password', params.password)
await page.click('[type="submit"]')
await page.wait_for_load_state('networkidle')
if 'dashboard' in page.url:
return "Successfully logged in"
else:
error = await page.text_content('.error-message')
return f"Login failed: {error}"
agent = Agent(
task="Login with username='admin@corp.com' and execute task",
llm=llm,
controller=controller,
)
Прямая интеграция Playwright + Claude
Для максимального контроля, строите напрямую на Playwright:
import base64
import json
import asyncio
from playwright.async_api import async_playwright, Page
import anthropic
client = anthropic.Anthropic()
async def capture_screenshot(page: Page) -> str:
"""Захватите скриншот видимой области как base64 JPEG."""
screenshot_bytes = await page.screenshot(
full_page=False,
type='jpeg',
quality=85,
)
return base64.b64encode(screenshot_bytes).decode('utf-8')
async def get_llm_action(
goal: str,
screenshot_b64: str,
url: str,
history: list[dict],
step: int,
) -> dict:
"""Спросите Claude какое действие выполнить дано текущее состояние браузера."""
system = """You are a browser automation agent. Given a screenshot and goal,
determine the next action. Return JSON only:
- action: one of [click, type, navigate, scroll, extract, done]
- x, y: coordinates for click
- text: text to type
- url: URL to navigate
- scroll_direction: up/down
- data: extracted data (if extract or done)
- reasoning: brief explanation"""
messages = [{
"role": "user",
"content": [
{
"type": "text",
"text": f"Goal: {goal}\nCurrent URL: {url}\nStep: {step}\n\nPrevious actions: {json.dumps(history[-5:])}\n\nWhat is the next action?"
},
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/jpeg",
"data": screenshot_b64,
}
}
]
}]
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=1024,
system=system,
messages=messages,
)
return json.loads(response.content[0].text)
async def execute_action(page: Page, action: dict) -> str:
"""Выполните LLM-указанное действие."""
action_type = action.get('action')
if action_type == 'click':
await page.mouse.click(action['x'], action['y'])
await page.wait_for_load_state('networkidle', timeout=5000)
return f"Clicked at ({action['x']}, {action['y']})"
elif action_type == 'type':
await page.keyboard.type(action['text'], delay=50)
return f"Typed: {action['text']}"
elif action_type == 'navigate':
await page.goto(action['url'], wait_until='networkidle')
return f"Navigated to {action['url']}"
elif action_type == 'scroll':
direction = action.get('scroll_direction', 'down')
delta = 500 if direction == 'down' else -500
await page.mouse.wheel(0, delta)
await asyncio.sleep(0.5)
return f"Scrolled {direction}"
elif action_type in ('extract', 'done'):
return f"Data: {action.get('data', {})}"
return f"Unknown action: {action_type}"
async def run_vision_agent(goal: str, start_url: str, max_steps: int = 30) -> dict:
"""Запустите vision-based browser agent."""
history = []
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=['--no-sandbox', '--disable-dev-shm-usage']
)
context = await browser.new_context(
viewport={'width': 1280, 'height': 720},
user_agent='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36'
)
page = await context.new_page()
await page.goto(start_url, wait_until='networkidle')
for step in range(max_steps):
screenshot = await capture_screenshot(page)
url = page.url
action = await get_llm_action(goal, screenshot, url, history, step)
if action.get('action') == 'done':
await browser.close()
return {
'success': True,
'steps': step + 1,
'data': action.get('data', {}),
'history': history,
}
result = await execute_action(page, action)
history.append({
'step': step,
'action': action,
'result': result,
})
await browser.close()
return {
'success': False,
'steps': max_steps,
'reason': 'Max steps reached',
'history': history,
}
Экстракция дерева доступности
Подход на основе дерева доступности обеспечивает снижение стоимости в 7 раз и улучшение задержки в 4 раза:
async def get_accessibility_tree(page: Page) -> str:
"""Экстрактируйте компактное представление дерева доступности."""
tree = await page.accessibility.snapshot(
interesting_only=True, # Фильтруйте интерактивные/content элементы
)
def format_node(node: dict, depth: int = 0, index: list = [0]) -> str:
indent = " " * depth
role = node.get('role', '')
name = node.get('name', '')
value = node.get('value', '')
# Пропускайте чисто структурные узлы
if role in ('none', 'generic') and not name:
children = node.get('children', [])
return '\n'.join(format_node(c, depth, index) for c in children)
current_idx = index[0]
index[0] += 1
parts = [f"[{current_idx}] {role}"]
if name:
parts.append(f"'{name}'")
if value:
parts.append(f"value='{value}'")
line = f"{indent}{' '.join(parts)}"
children = node.get('children', [])
child_text = '\n'.join(
format_node(c, depth + 1, index)
for c in children
)
return f"{line}\n{child_text}" if child_text else line
return format_node(tree)
# Пример вывода:
# [0] RootWebArea 'Google'
# [1] combobox 'Search' value=''
# [2] button 'Google Search'
# [3] button "I'm Feeling Lucky"
Умное наблюдение (гибридный подход):
async def smart_observation(page: Page) -> dict:
"""Используйте дерево доступности по умолчанию, переходите на vision когда нужно."""
tree = await get_accessibility_tree(page)
interactive_elements = tree.count('[')
# Если меньше 5 интерактивных элементов, дерево, вероятно, бесполезно
if interactive_elements < 5:
screenshot = await capture_screenshot(page)
return {
'mode': 'vision',
'screenshot': screenshot,
'tree': None,
'reason': 'Poor accessibility structure'
}
# Проверьте наличие canvas элементов (vision нужна)
has_canvas = await page.evaluate(
"() => document.querySelectorAll('canvas').length > 0"
)
if has_canvas:
screenshot = await capture_screenshot(page)
return {
'mode': 'vision',
'screenshot': screenshot,
'tree': tree,
'reason': 'Canvas elements detected'
}
return {
'mode': 'accessibility',
'screenshot': None,
'tree': tree,
'reason': 'Good accessibility structure'
}
Облачные Sandbox опции
Запуск browser agents в облаке избегает управления инфраструктурой и обеспечивает ротирующиеся IP-адреса, evasion отпечатков и решение CAPTCHA.
Таблица сравнения
| Провайдер | Цены | Функции | Лучше всего для |
|---|---|---|---|
| Browserbase | $49-199/mo | Режим скрытности, запись сессии, инструменты отладки, резидентные прокси | Production скрейпинг в масштабе |
| Steel | $29-99/mo | Playwright-совместимый API, экспорт скриншотов/PDF, простые цены | Мелкие и средние нагрузки |
| Hyperbrowser | $79-199/mo | Продвинутый evasion отпечатков, решение CAPTCHA, гео-таргетинг | Высокозащищённый скрейпинг (банкинг, e-commerce) |
Интеграция Browserbase
pip install browserbase-sdk
from browserbase import Browserbase
bb = Browserbase(api_key="YOUR_API_KEY")
# Создайте сессию
session = bb.sessions.create()
# Подключите Playwright к удалённому браузеру
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(session.ws_endpoint)
page = await browser.new_page()
# Используйте browser agent обычно
agent = Agent(task="...", llm=llm)
result = await agent.run(page=page)
Интеграция Steel
pip install steel-sdk
from steel import Steel
steel = Steel(api_key="YOUR_API_KEY")
# Запустите удалённую сессию браузера
session = steel.sessions.create(
proxy_mode="residential", # Ротирующиеся резидентные IP-адреса
solve_captcha=True, # Автоматическое решение CAPTCHA
)
# Подключите Playwright
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(session.connect_url)
page = await browser.new_page()
# ... используйте agent
Production-соображения
Ограничение скорости и Anti-Detection
Конфигурация скрытности:
class BrowserConfig:
"""Production профили конфигурации браузера."""
@staticmethod
def stealth_production() -> dict:
"""Максимальная скрытность для production скрейпинга."""
return {
'headless': True,
'args': [
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-blink-features=AutomationControlled',
'--disable-infobars',
'--window-size=1920,1080',
'--disable-extensions',
'--disable-gpu',
],
'ignore_default_args': ['--enable-automation'],
}
@staticmethod
def stealth_context() -> dict:
"""Настройки контекста браузера чтобы избежать обнаружения."""
return {
'viewport': {'width': 1920, 'height': 1080},
'user_agent': (
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
),
'locale': 'en-US',
'timezone_id': 'America/New_York',
'geolocation': {'latitude': 40.7128, 'longitude': -74.0060},
'permissions': ['geolocation'],
'extra_http_headers': {
'Accept-Language': 'en-US,en;q=0.9',
'Accept-Encoding': 'gzip, deflate, br',
}
}
Управление сессией аутентификации
import json
from pathlib import Path
from playwright.async_api import async_playwright, BrowserContext
class AuthManager:
"""Управляйте состоянием аутентификации браузера для агентов."""
def __init__(self, auth_dir: str = "/tmp/browser_auth"):
self.auth_dir = Path(auth_dir)
self.auth_dir.mkdir(exist_ok=True)
def _state_path(self, domain: str) -> Path:
safe_domain = domain.replace('https://', '').replace('/', '_')
return self.auth_dir / f"{safe_domain}_state.json"
async def save_auth_state(self, context: BrowserContext, domain: str):
"""Сохраните состояние аутентификации браузера на диск."""
state = await context.storage_state()
state_path = self._state_path(domain)
state_path.write_text(json.dumps(state))
async def load_auth_state(
self,
playwright,
domain: str,
headless: bool = True
) -> BrowserContext:
"""Загрузите сохранённое состояние аутентификации или вызовите свежий вход."""
state_path = self._state_path(domain)
browser = await playwright.chromium.launch(headless=headless)
if state_path.exists():
context = await browser.new_context(
storage_state=str(state_path)
)
# Проверьте что состояние аутентификации всё ещё действительно
page = await context.new_page()
await page.goto(domain)
if await self._is_authenticated(page, domain):
return context
# Аутентификация истекла, очистите
state_path.unlink()
await context.close()
# Свежий контекст для нового входа
context = await browser.new_context()
return context
async def _is_authenticated(self, page, domain: str) -> bool:
"""Проверьте если текущая сессия аутентифицирована."""
indicators = [
'[data-user-id]',
'.user-avatar',
'[aria-label*="User menu"]',
'a[href*="/logout"]',
]
for selector in indicators:
if await page.query_selector(selector):
return True
if any(keyword in page.url for keyword in ['/login', '/signin', '/auth']):
return False
return True
Обработка CAPTCHA
class CaptchaHandler:
"""Обрабатывайте вызовы CAPTCHA в browser agents."""
def __init__(self, anticaptcha_key: str = None):
self.anticaptcha_key = anticaptcha_key
async def detect_captcha(self, page: Page) -> dict:
"""Обнаружьте какой тип CAPTCHA присутствует."""
captcha_info = await page.evaluate("""
() => {
// Проверьте reCAPTCHA
if (document.querySelector('.g-recaptcha') ||
document.querySelector('[data-sitekey]')) {
const el = document.querySelector('[data-sitekey]');
return {
type: 'recaptcha',
sitekey: el ? el.getAttribute('data-sitekey') : null
};
}
// Проверьте hCaptcha
if (document.querySelector('.h-captcha')) {
const el = document.querySelector('[data-hcaptcha-sitekey]');
return {
type: 'hcaptcha',
sitekey: el ? el.getAttribute('data-hcaptcha-sitekey') : null
};
}
// Проверьте Cloudflare Turnstile
if (document.querySelector('.cf-turnstile')) {
return { type: 'turnstile' };
}
return { type: null };
}
""")
return captcha_info
async def solve_recaptcha_v2(self, page: Page, sitekey: str) -> bool:
"""Решите reCAPTCHA v2 используя anti-captcha сервис."""
if not self.anticaptcha_key:
return False
import aiohttp
# Отправьте задачу на решение
async with aiohttp.ClientSession() as session:
task_response = await session.post(
'https://api.anti-captcha.com/createTask',
json={
"clientKey": self.anticaptcha_key,
"task": {
"type": "NoCaptchaTaskProxyless",
"websiteURL": page.url,
"websiteKey": sitekey,
}
}
)
task_data = await task_response.json()
task_id = task_data.get('taskId')
if not task_id:
return False
# Опрашивайте результат (макс 120 секунд)
for _ in range(24):
await asyncio.sleep(5)
result_response = await session.post(
'https://api.anti-captcha.com/getTaskResult',
json={
"clientKey": self.anticaptcha_key,
"taskId": task_id,
}
)
result_data = await result_response.json()
if result_data.get('status') == 'ready':
token = result_data['solution']['gRecaptchaResponse']
# Внедрите токен в страницу
await page.evaluate(f"""
() => {{
document.getElementById('g-recaptcha-response').innerHTML = '{token}';
}}
""")
return True
return False
Мониторинг и наблюдаемость
import time
from dataclasses import dataclass
from typing import Optional
import logging
logger = logging.getLogger(__name__)
@dataclass
class AgentMetrics:
task_id: str
start_time: float
end_time: Optional[float] = None
steps_taken: int = 0
llm_calls: int = 0
tokens_used: int = 0
screenshots_taken: int = 0
success: Optional[bool] = None
error: Optional[str] = None
@property
def duration_seconds(self) -> float:
if self.end_time:
return self.end_time - self.start_time
return time.time() - self.start_time
@property
def estimated_cost_usd(self) -> float:
# Цены Claude Opus 4 (смешанные)
return self.tokens_used * 0.000015
class InstrumentedAgent:
"""Browser agent со встроенной наблюдаемостью."""
def __init__(self, task: str, llm, task_id: str = None):
self.task = task
self.llm = llm
self.task_id = task_id or f"task_{int(time.time())}"
self.metrics = AgentMetrics(
task_id=self.task_id,
start_time=time.time(),
)
async def run(self) -> tuple[str, AgentMetrics]:
"""Запустите agent и верните результат с метриками."""
try:
result = await self._run_with_instrumentation()
self.metrics.success = True
return result, self.metrics
except Exception as e:
self.metrics.success = False
self.metrics.error = str(e)
self.metrics.end_time = time.time()
logger.error(
f"Agent task {self.task_id} failed after "
f"{self.metrics.duration_seconds:.1f}s: {e}",
extra={'metrics': self.metrics.__dict__}
)
raise
finally:
self.metrics.end_time = time.time()
logger.info(
f"Agent task completed",
extra={
'task_id': self.task_id,
'duration_s': self.metrics.duration_seconds,
'steps': self.metrics.steps_taken,
'cost_usd': self.metrics.estimated_cost_usd,
'success': self.metrics.success,
}
)
async def _run_with_instrumentation(self) -> str:
agent = Agent(task=self.task, llm=self.llm)
result = await agent.run()
return result
Производительность и бенчмарки
Примечание: Приведённые ниже цифры являются иллюстративными оценками на основе типичных production-конфигураций, а не измерениями конкретной системы.
Сравнение задержек (на страницу):
- Vision-based агент: ~2800ms (1200ms скриншот + 1600ms LLM вывод)
- Агент дерева доступности: ~650ms (150ms экстракция дерева + 500ms LLM вывод)
- Традиционный Playwright скрейпер: ~150ms (без вызова LLM)
Сравнение стоимости (за 1000 страниц, используя Claude Opus 4):
- Vision-based: ~$2.40 (8500 токенов/страницу × $0.015/1K токенов × 1000 страниц)
- Дерево доступности: ~$0.32 (1200 токенов/страницу × $0.015/1K токенов × 1000 страниц)
- Традиционный скрейпер: $0 (нет вызовов LLM)
Точность на типичных страницах e-commerce продуктов:
- Vision-based: 87% процент успеха задачи
- Дерево доступности: 79% процент успеха задачи (отказывает на div-soup React приложениях)
- Традиционный скрейпер: 95% процент успеха (но требует ручного обновления селекторов при изменении страницы)
Разбор потребления токенов (подход дерева доступности):
- Среднее представление страницы: 1200 токенов
- История действий (последние 5 шагов): 300 токенов
- System prompt: 200 токенов
- LLM вывод (действие): 50-150 токенов
- Итого за шаг: ~1750 токенов
Распределение production отказов:
- Timeout навигации (40%): медленные страницы, проблемы сети
- Элемент не найден (30%): динамическое содержимое, плохие селекторы
- Вызов CAPTCHA (20%): anti-bot детекция сработала
- Аутентификация истекла (10%): timeout сессии, инвалидация cookie
Стоимость в масштабе (10,000 страниц/день):
- Дерево доступности: $3.20/день ($96/месяц)
- Vision-based: $24/день ($720/месяц)
- Cloud sandbox (Browserbase): $199/месяц (неограниченные сессии)
- Anti-captcha сервис (5% CAPTCHA доля): $1.50/день ($45/месяц)
- Итого (дерево доступности + облако + CAPTCHA): ~$240/месяц
- Итого (vision + облако + CAPTCHA): ~$965/месяц