AI Agents PRO: LangGraph, AutoGen и LLMOps в продакшнеИнструменты и интеграцииБраузерные агенты (Playwright)

Браузерные агенты (Playwright)

Уроки курсаБраузерные агенты (Playwright)

Параллелизм и очередь задач; лимиты и троттлинг

Представьте: вам нужно собрать данные с 100 страниц сайта. Открывать их по одной — это займёт часы. Что если открыть все 100 одновременно? Браузер рухнет от нехватки памяти, или сайт заблокирует вас за DDoS. Нужен баланс: достаточный параллелизм для скорости, но не настолько большой, чтобы сломать систему или получить бан.

Когда вы автоматизируете браузер с помощью Playwright, очень часто возникает необходимость выполнять сразу несколько задач: открывать несколько страниц, собирать данные с разных источников или параллельно запускать тесты. Параллелизм позволяет делать это одновременно, экономя время. Однако нужно учитывать, что браузерные процессы потребляют ресурсы, поэтому слишком много одновременных задач может привести к сбоям или перегрузке системы.

Ключевые аспекты параллелизма и управления задачами:

  1. Параллелизм (Concurrency): В Playwright вы можете запускать несколько страниц или браузерных контекстов одновременно. В Python это удобно делать с помощью asyncio. Параллелизм позволяет ускорять сбор данных или выполнение тестов, но требует аккуратного управления ресурсами.
  2. Очередь задач (Task Queue): Когда задач слишком много, их удобно ставить в очередь и обрабатывать по очереди или в ограниченном параллелизме. Так вы контролируете нагрузку на систему и снижаете вероятность ошибок.
  3. Лимиты: Ограничение количества одновременно выполняемых задач предотвращает перегрузку процессора и памяти. Например, можно разрешить запуск не более 5 страниц одновременно, даже если всего задач 20.
  4. Троттлинг (Throttling): Иногда нужно контролировать скорость выполнения действий, например, чтобы не заблокировали сайт или API за слишком частые запросы. Троттлинг позволяет задать минимальные интервалы между задачами или запросами.
  5. Стабильность: Комбинация очереди, лимитов и троттлинга обеспечивает стабильную работу браузерного агента. Без этих механизмов параллельное выполнение может приводить к зависаниям, ошибкам загрузки страниц или неправильной работе селекторов.

Параллелизм в 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()) экономит ресурсы и позволяет контролировать изоляцию данных. Комбинированное использование этих механизмов делает браузерных агентов более надежными и предсказуемыми.