Как автоматизировать загрузку данных через GraphQL в Playwright

    📝 Кратко: Многие современные SPA-сайты используют GraphQL для загрузки данных, которые не видны при обычном HTTP-запросе. Научитесь использовать **Playwright GraphQL Python** для перехвата этих запросов, чтобы собирать данные в 10 раз быстрее.
Загрузка данных через GraphQL в Playwright

Если вы регулярно занимаетесь автоматизацией или парсингом современных веб-приложений (Single Page Applications, SPA), вы наверняка сталкивались с ситуацией, когда данные на сайте загружаются, но не через обычные REST-запросы. Вместо этого в сетевой активности мелькают запросы к единственной точке входа, а тело запроса — это длинный JSON-код с полями query и variables. Добро пожаловать в мир GraphQL!

GraphQL — это язык запросов для API, который позволяет клиенту запрашивать ровно те данные, которые ему нужны, и не более. Для нас, автоматизаторов, это означает, что нам нужно «заглянуть» внутрь этого запроса и понять, как он работает.

Я сам долго искал универсальный и надежный инструмент для работы с GraphQL в автоматизации, и Playwright с его мощным API для перехвата сети оказался лучшим решением. В этой статье мы глубоко погрузимся в то, как настроить Playwright GraphQL Python для перехвата, анализа и даже модификации этих запросов, чтобы собирать большие объемы данных максимально эффективно, без необходимости рендерить всю страницу.

Почему обычный парсинг не работает с GraphQL

Чтобы понять, почему нам нужен Playwright, давайте посмотрим, как GraphQL меняет подход к сбору данных.

Отличия GraphQL от REST для автоматизации

Отличия GraphQL от REST
  1. Одна точка входа: В REST вы обращаетесь к разным URL для разных ресурсов (например, /api/users, /api/products). В GraphQL вы всегда обращаетесь к одной и той же точке входа (часто /graphql), а то, какие данные вы получите, определяется сложным JSON-запросом в теле.
  2. Сложность тела запроса: Тело запроса GraphQL содержит сложную структуру query (схема данных) и variables (параметры пагинации, фильтры). Если мы просто попытаемся повторить этот запрос через requests, нам нужно точно воссоздать эту структуру, что непросто.

Playwright решает эту проблему, позволяя нам перехватить запрос, который уже правильно сформирован браузером, извлечь его структуру и выполнить его повторно, меняя только переменные (например, номер страницы или лимит).

Перехват GraphQL-запросов с помощью Playwright

Основной инструмент, который мы будем использовать, — это метод route() в Playwright. Он позволяет нам перехватить сетевой запрос до того, как он достигнет сервера, проанализировать его и, при необходимости, изменить.

Настройка перехвата и анализ данных

Мы настроим маршрутизацию (routing) так, чтобы она реагировала только на запросы, идущие к нашему GraphQL-эндпоинту.

Установка: Вы можете установить Playwright стандартным способом, используя pip. (Если вы ищете способ ускорить управление пакетами, рекомендую использовать uvсамый быстрый пакетный менеджер на Python).

pip install playwright
playwright install

Код для перехвата:

import asyncio
import json
from playwright.async_api import async_playwright

# Укажите адрес, по которому сайт отправляет GraphQL-запросы
GRAPHQL_ENDPOINT = "/api/graphql" 
TARGET_URL = "https://example.com/data-page" # Страница, которая вызывает запрос

async def intercept_graphql_request():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()

        # 1. Настраиваем маршрут для перехвата запросов
        async def handle_route(route):
            request = route.request
            url = request.url

            # Проверяем, что запрос идет к нашему GraphQL-эндпоинту
            if GRAPHQL_ENDPOINT in url and request.method == "POST":

                # 2. Получаем тело запроса
                # Тело POST-запроса всегда в виде байтов, парсим его в JSON
                try:
                    post_data = json.loads(request.post_data)

                    print("&#x1f4c4 Перехвачен GraphQL-запрос:")
                    print(f"   Operation Name: {post_data.get('operationName')}")
                    print(f"   Variables (фрагмент): {list(post_data.get('variables', {}).keys())[:2]}...")

                    # Сохраняем запрос для дальнейшего повтора
                    # В реальном коде сохраняем 'query' и 'variables'
                    global SAVED_QUERY
                    SAVED_QUERY = post_data

                except Exception as e:
                    print(f"Ошибка парсинга тела запроса: {e}")

            # Обязательно продолжаем запрос, чтобы страница загрузилась
            await route.continue_()

        # Регистрируем функцию-обработчик маршрута
        await page.route(f"**{GRAPHQL_ENDPOINT}**", handle_route)

        # Загружаем страницу, которая вызовет GraphQL-запрос
        print(f"&#x1f50e Загружаем целевую страницу: {TARGET_URL}")
        await page.goto(TARGET_URL)
        await page.wait_for_selector('div#loaded_data') # Ждем загрузки данных

        await browser.close()

# Глобальная переменная для сохранения запроса
SAVED_QUERY = None

if __name__ == "__main__":
    asyncio.run(intercept_graphql_request())

# Теперь SAVED_QUERY содержит полную структуру запроса, включая "query"
if SAVED_QUERY:
    print("\n&#x26a1 Структура запроса сохранена и готова к повтору без браузера!")

Объяснение кода:

  • page.route(f"**{GRAPHQL_ENDPOINT}**", handle_route): Устанавливает перехват всех POST-запросов, чья URL содержит наш эндпоинт.
  • route.request.post_data: Получаем тело запроса (payload) в виде байтов, которое содержит сам GraphQL-запрос.
  • json.loads(request.post_data): Десериализуем байты в словарь Python, который содержит поля query (сам GraphQL-запрос) и variables (параметры).
  • await route.continue_(): Критически важно! Без этой строки Playwright заблокирует загрузку страницы. Мы говорим ему: «Проанализировав, продолжай запрос».

Автоматизация без браузера: повтор и модификация запроса

После того как мы перехватили и сохранили структуру GraphQL-запроса (SAVED_QUERY), самое неэффективное — это продолжать запускать Playwright для каждой новой страницы или порции данных. Наша цель — собрать данные напрямую через HTTP (используя библиотеку requests), но с помощью ранее перехваченного GraphQL-запроса.

Циклический сбор данных через requests

Используя перехваченный query и меняя только variables, мы можем быстро собирать большие объемы данных. В этом и заключается вся мощь подхода Playwright GraphQL Python: используем Playwright для анализа (один раз), а requests для сбора (много раз).

import requests
import json
import time

# Используем структуру запроса, которую мы сохранили на предыдущем шаге
# В реальном коде SAVED_QUERY был бы импортирован или загружен из файла
SAVED_QUERY = {
    "operationName": "DataListQuery",
    "query": "query DataListQuery($first: Int!, $after: String) { dataList(first: $first, after: $after) { edges { node { id title createdAt } } pageInfo { hasNextPage endCursor } } }",
    "variables": {
        "first": 10,  # Количество элементов на странице
        "after": None # Курсор для пагинации (null для первой страницы)
    }
}

GRAPHQL_URL = "https://example.com/api/graphql" # Полный URL

def fetch_graphql_data(cursor_id=None, limit=10):
    """Выполняет запрос GraphQL напрямую через requests."""
    
    # 1. Модифицируем переменные
    SAVED_QUERY['variables']['first'] = limit
    SAVED_QUERY['variables']['after'] = cursor_id
    
    # 2. Отправляем запрос
    headers = {
        'Content-Type': 'application/json',
        # Добавьте заголовки авторизации, если нужны (например, 'Authorization')
    }
    
    response = requests.post(
        GRAPHQL_URL,
        headers=headers,
        data=json.dumps(SAVED_QUERY) # Отправляем JSON-строку
    )
    
    response.raise_for_status() # Вызывает ошибку для HTTP-статусов 4xx/5xx
    
    return response.json()


# Пример цикла для пагинации (сбора нескольких страниц)
all_collected_data = []
current_cursor = None # Начинаем с первой страницы (null/None)
page_count = 0

while True:
    page_count += 1
    print(f"\n&#x1f4c5 Собираем страницу #{page_count}...")
    
    try:
        result = fetch_graphql_data(cursor_id=current_cursor)
        
        # 1. Обработка данных
        new_items = result['data']['dataList']['edges']
        all_collected_data.extend(new_items)
        print(f"   Собрано новых элементов: {len(new_items)}")
        
        # 2. Проверка пагинации (курсор)
        page_info = result['data']['dataList']['pageInfo']
        has_next_page = page_info['hasNextPage']
        current_cursor = page_info['endCursor'] # Получаем курсор для следующей страницы
        
        if not has_next_page:
            print("&#x2705 Пагинация завершена.")
            break
            
        # Пауза, чтобы не перегружать сервер
        time.sleep(1) 
        
    except requests.exceptions.HTTPError as e:
        print(f"Ошибка HTTP: {e}")
        break
    except Exception as e:
        print(f"Непредвиденная ошибка: {e}")
        break

print(f"\n&#x1f389 Общий результат: собрано {len(all_collected_data)} элементов.")

Объяснение кода:

  • requests.post(..., data=json.dumps(SAVED_QUERY)): Мы отправляем точно такой же POST-запрос, как и браузер, но используем быструю библиотеку requests вместо тяжелого Playwright.
  • Модификация переменных: Перед каждым запросом мы меняем current_cursor (часто называемый after) и limit (first), чтобы запросить следующую порцию данных.
  • Пагинация по курсору (endCursor): В GraphQL обычно используется курсорная пагинация, а не нумерация страниц. Мы извлекаем endCursor из pageInfo текущего ответа и передаем его как after в следующий запрос.

Преимущества и Best Practices подхода Playwright + GraphQL

Использование Playwright GraphQL Python таким образом дает огромные преимущества в скорости и эффективности по сравнению с традиционным парсингом.

Эффективность и рекомендации

Скорость и ресурсы: После первого запуска Playwright для анализа, вся дальнейшая работа по сбору данных выполняется быстрым и легким HTTP-запросом через requests. Это в разы быстрее, чем рендерить страницу в браузере для каждого блока данных.

Точность данных: Вы запрашиваете данные напрямую из API, получая чистый JSON, который легко обрабатывать, без необходимости парсить сложный HTML.

Обработка авторизации: Если для GraphQL-запроса требуется токен, его можно перехватить в заголовках первого запроса (или найти в локальном хранилище Playwright) и затем добавить в заголовок Authorization в ваших запросах requests. Узнайте, как безопасно хранить такие токены: Как безопасно настроить хранение API ключей Python.

Соблюдение лимитов: Даже при быстром сборе данных через requests, всегда включайте паузы (time.sleep(1)) между запросами, чтобы не перегружать сервер и не получить бан IP.

✅ Заключение

Автоматизация сбора данных с современных SPA-сайтов требует использования передовых инструментов. Подход Playwright GraphQL Python (используя Playwright для анализа схемы и requests для быстрого сбора) является самым эффективным способом работы с GraphQL API.

Вы научились перехватывать сложный запрос, извлекать его структуру и затем использовать эту структуру для циклического и контролируемого сбора данных, меняя только параметры пагинации. Это позволяет собирать информацию не в секундах, а в миллисекундах.

🔁 Если вам полезны советы по автоматизации и парсингу, посмотрите также:
Как ускорить Python в 10 раз с помощью асинхронности — узнайте больше о высокопроизводительных сетевых операциях, которые можно применить и к `requests`
Как писать чистый и читаемый код на Python — структурируйте свой асинхронный код Playwright для большей надежности
Как безопасно настроить хранение API ключей Python — защитите свои токены, которые могут потребоваться для авторизации в GraphQL
💬 Остались вопросы? Пишите в комментариях — с радостью уточню, дополню или помогу с вашим кодом.
📢 Подписывайтесь на Telegram-канал PythonAuto, чтобы не пропустить новые гайды по автоматизации, парсингу и Python.
👉 Ваш интерес — лучшая мотивация для новых статей!

Оставьте комментарий