Браузерные агенты (Playwright)
Уроки курсаБраузерные агенты (Playwright)
Параллелизм и очередь задач; лимиты и троттлинг
Представьте: вам нужно собрать данные с 100 страниц сайта. Открывать их по одной — это займёт часы. Что если открыть все 100 одновременно? Браузер рухнет от нехватки памяти, или сайт заблокирует вас за DDoS. Нужен баланс: достаточный параллелизм для скорости, но не настолько большой, чтобы сломать систему или получить бан.
Когда вы автоматизируете браузер с помощью Playwright, очень часто возникает необходимость выполнять сразу несколько задач: открывать несколько страниц, собирать данные с разных источников или параллельно запускать тесты. Параллелизм позволяет делать это одновременно, экономя время. Однако нужно учитывать, что браузерные процессы потребляют ресурсы, поэтому слишком много одновременных задач может привести к сбоям или перегрузке системы.
Ключевые аспекты параллелизма и управления задачами:
- Параллелизм (Concurrency): В Playwright вы можете запускать несколько страниц или браузерных контекстов одновременно. В Python это удобно делать с помощью
asyncio. Параллелизм позволяет ускорять сбор данных или выполнение тестов, но требует аккуратного управления ресурсами. - Очередь задач (Task Queue): Когда задач слишком много, их удобно ставить в очередь и обрабатывать по очереди или в ограниченном параллелизме. Так вы контролируете нагрузку на систему и снижаете вероятность ошибок.
- Лимиты: Ограничение количества одновременно выполняемых задач предотвращает перегрузку процессора и памяти. Например, можно разрешить запуск не более 5 страниц одновременно, даже если всего задач 20.
- Троттлинг (Throttling): Иногда нужно контролировать скорость выполнения действий, например, чтобы не заблокировали сайт или API за слишком частые запросы. Троттлинг позволяет задать минимальные интервалы между задачами или запросами.
- Стабильность: Комбинация очереди, лимитов и троттлинга обеспечивает стабильную работу браузерного агента. Без этих механизмов параллельное выполнение может приводить к зависаниям, ошибкам загрузки страниц или неправильной работе селекторов.
Параллелизм в Playwright
Playwright поддерживает параллельную работу на уровне браузеров, контекстов и страниц, но использование синхронного API (sync_playwright) не даёт настоящего параллелизма — все действия выполняются последовательно, даже если открыто несколько страниц. Реальный параллелизм достигается при использовании асинхронного API (async/await) или запуска нескольких процессов.
Для одновременной загрузки страниц и неблокирующего выполнения кода следует использовать async_playwright и asyncio.
import asyncio
import time
from playwright.async_api import async_playwright
async def open_page(context, url, idx):
start = time.perf_counter()
print(f"[{idx}] START {start:.3f}")
page = await context.new_page()
await page.goto(url)
title = await page.title()
end = time.perf_counter()
print(f"[{idx}] END {end:.3f} | duration: {end - start:.2f}s | {title}")
await page.close()
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
context = await browser.new_context() # создаём общий контекст
tasks = [open_page(context, "https://example.com", i) for i in range(1, 6)]
await asyncio.gather(*tasks)
await context.close()
await browser.close()
asyncio.run(main())
В данном примере создаются пять страниц внутри одного контекста, которые одновременно загружают сайт. Это демонстрирует, как можно использовать параллелизм для ускорения процесса.
Важно: Обратите внимание, что мы создаём контекст явно через browser.new_context(), а затем страницы через context.new_page(). Если использовать browser.new_page() напрямую (как в старых примерах), Playwright создаст новый контекст для каждой страницы автоматически. Это удобно для простых сценариев, но при параллелизме резко увеличивает расход ресурсов — контекст гораздо «тяжелее» страницы. Кроме того, каждая страница будет иметь свои куки и storage, что может быть нежелательно.
Очередь задач
Очередь задач используется для ограничения количества одновременно выполняемых операций. Это важно при работе с Playwright, где каждая страница потребляет ресурсы и избыточный параллелизм может привести к ошибкам или блокировкам.
Пример реализации очереди задач:
import asyncio
from playwright.async_api import async_playwright
async def worker(name, queue, context):
while True:
try:
url = queue.get_nowait()
except asyncio.QueueEmpty:
return
print(f"[{name}] START {url}")
page = await context.new_page()
await page.goto(url)
title = await page.title()
print(f"[{name}] END {url} → {title}")
await page.close()
queue.task_done()
async def main(urls):
queue = asyncio.Queue()
for url in urls:
await queue.put(url)
async with async_playwright() as p:
browser = await p.chromium.launch()
context = await browser.new_context() # общий контекст для всех воркеров
workers = [
asyncio.create_task(worker(f"W{i}", queue, context))
for i in range(1, 4) # максимум 3 параллельные задачи
]
await queue.join() # ждём обработки всех URL
for w in workers:
w.cancel()
await context.close()
await browser.close()
urls = ["https://example.com", "https://example.org", "https://example.net"]
asyncio.run(main(urls))
В этом примере создаётся очередь для URL-адресов, которые необходимо загрузить. Рабочие потоки обрабатывают URL-адреса из очереди, что позволяет контролировать параллелизм и нагрузку, но не гарантирует порядок завершения операций. Все воркеры используют один контекст и разделяют состояние (куки, авторизацию).
Лимиты и троттлинг
При работе с параллельными задачами важно ограничивать количество одновременных операций и частоту их запуска. Лимиты защищают систему и браузер от перегрузки, а троттлинг позволяет контролировать скорость запросов и снижать риск блокировок со стороны сайта.
В асинхронном коде троттлинг обычно реализуется через задержки между задачами или ограничение числа одновременно выполняемых операций. Важный момент: лучше разделять лимиты параллелизма (через семафор) и паузы между запросами (после освобождения семафора), чтобы не блокировать слоты во время ожидания.
import asyncio
import random
from playwright.async_api import async_playwright
async def fetch_page(url, context, semaphore):
async with semaphore: # лимит параллельных задач
page = await context.new_page()
await page.goto(url)
title = await page.title()
print(title)
await page.close()
# Троттлинг после освобождения семафора
await asyncio.sleep(random.uniform(0.8, 1.5))
async def main(urls):
semaphore = asyncio.Semaphore(2) # максимум 2 задачи одновременно
async with async_playwright() as p:
browser = await p.chromium.launch()
context = await browser.new_context()
tasks = [fetch_page(url, context, semaphore) for url in urls]
await asyncio.gather(*tasks)
await context.close()
await browser.close()
urls = ["https://example.com", "https://example.org", "https://example.net"]
asyncio.run(main(urls))
В этом примере:
Semaphoreограничивает параллелизм — не более 2 страниц загружаются одновременноsleep + randomвыполняется после освобождения семафора, контролируя частоту запросов без блокировки других задач- комбинация снижает нагрузку и делает поведение более «естественным», имитируя действия реального пользователя
Итоги
Параллелизм позволяет ускорять выполнение задач, но без контроля ресурсов легко перегрузить систему. Очередь задач помогает управлять порядком и количеством одновременных действий. Лимиты и троттлинг обеспечивают стабильность работы и предотвращают ошибки при высокой нагрузке. Правильное использование контекстов (через new_context() и context.new_page()) экономит ресурсы и позволяет контролировать изоляцию данных. Комбинированное использование этих механизмов делает браузерных агентов более надежными и предсказуемыми.
