Любой инженер, собиравший данные с внешних ресурсов, знает эту фантомную боль. Вы тратите два дня на реверс-инжиниринг сайта-донора. Распаковываете обфусцированный JavaScript, подбираете каскад XPath-выражений, настраиваете прокси и обход Cloudflare. Скрипт элегантно собирает цены, пакует их в базу. Выкатываете в прод.
Через неделю звонит продакт-менеджер: «Дашборды пустые». Вы открываете DevTools и видите — разработчики донора выкатили редизайн. Вместо теперь . Или хуже: они переехали на Tailwind, и классы выглядят как flex pt-4 text-gray-900. Вся логика парсинга превратилась в мусор. Приходится заново лезть в код. И так по кругу.
Мы привыкли парсить DOM-дерево. Мы жестко привязываемся к структуре HTML, которая создана для рендеринга в браузере, а не для машиночитаемой передачи данных. Парадокс в том, что пользователь даже не замечает технических изменений. Для него таблица с ценами остается таблицей.
Что если скрипты начнут «смотреть» на интерфейс глазами пользователя?
От DOM-дерева к пикселям
Появление мультимодальных LLM (в частности, GPT-4o) ломает привычный подход к веб-скрейпингу. Вместо выкачивания мегабайтов грязного HTML и вырезания скриптов, мы делаем один скриншот целевой области экрана.
Эту картинку мы отдаем нейросети вместе с жестко заданной JSON-схемой: «Найди карточки товаров и верни массив». Модели неважно, написан сайт на React или легаси-jQuery. Ей плевать на CSS-классы и скрытые поля. Она считывает визуальную иерархию, распознает текст (OCR) и связывает заголовки столбцов с их значениями.
Такой пайплайн спасает при извлечении данных из:
- Финансовых отчетов со сложной плавающей версткой.
- Каталогов, где цены выводятся через Canvas или обфусцированные SVG-шрифты для защиты от ботов.
- Сайтов с динамической структурой, меняющейся при каждом деплое.
Реализация: Playwright + RouterAPI + GPT-4o
Для надежного пайплайна визуального парсинга мы соберем связку из трех компонентов:
- Автоматизация браузера (Playwright) — для рендеринга страницы, подавления поп-апов и захвата чистого скриншота.
- Единый шлюз доступа к LLM (RouterAPI) — для обеспечения отказоустойчивости и прозрачного переключения провайдеров.
- Механизм Structured Outputs (JSON Mode) — для принуждения модели отвечать строго по контракту.
Шаг 1: Готовим визуальный контекст в Playwright
Задача — получить чистую картинку конкретного блока. Скриншот всей страницы делать бессмысленно: это долго, дорого по токенам и снижает точность распознавания мелких деталей. Playwright умеет фокусироваться на конкретном узле.
from playwright.sync_api import sync_playwright
def capture_target_area(url: str, selector: str, output_path: str = "target.png"):
with sync_playwright as p:
browser = p.chromium.launch(headless=True)
# Устанавливаем большой viewport, чтобы избежать мобильной верстки
context = browser.new_context(viewport={"width": 1920, "height": 1080})
page = context.new_page
# Ждем завершения фоновых аякс-запросов
page.goto(url, wait_until="networkidle")
# Агрессивно вычищаем визуальный мусор: куки-баннеры, чат-ботов, липкие шапки
page.evaluate("""
document.querySelectorAll('.cookie-banner, .intercom-app, header.sticky').forEach(el => el.remove);
""")
# Фокусируемся на контейнере с данными
element = page.locator(selector)
element.screenshot(path=output_path)
browser.close
Удаление перекрывающих элементов — не прихоть, а необходимость. Если плавашка закроет цену, нейросеть не сможет «заглянуть» под нее и либо вернет null, либо начнет галлюцинировать.
Шаг 2: Извлекаем данные через GPT-4o и RouterAPI
Картинка target.png готова. Превращаем пиксели в структурированные данные. Мы берем GPT-4o — модель выдает отличное соотношение качества OCR и скорости работы.
Чтобы защитить продакшен от падений API самого OpenAI и rate-лимитов, запрос уходит через RouterAPI. Это дает автоматический фолбэк: если эндпоинт OpenAI ответит 502 ошибкой, RouterAPI перекинет запрос на запасного провайдера (например, через резервный маршрут RouterAPI), и процесс не прервется.
Для гарантии формата используем response_format типа json_schema (Structured Outputs). На выходе получаем валидный JSON, который десериализуется в DTO без написания регулярок и костылей.
import base64
import requests
import json
def encode_image(image_path: str) -> str:
with open(image_path, "rb") as image_file:
return base64.b64encode(image_file.read).decode('utf-8')
def extract_json_from_image(image_path: str) -> dict:
base64_img = encode_image(image_path)
headers = {
"Authorization": "Bearer YOUR_ROUTERAPI_KEY",
"Content-Type": "application/json"
}
# Жестко фиксируем схему возвращаемых данных
schema = {
"type": "json_schema",
"json_schema": {
"name": "catalog_extraction",
"strict": True,
"schema": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Полное название товара"},
"price_rub": {"type": "number", "description": "Цена в рублях (число)"},
"availability": {"type": "boolean", "description": "True если товар в наличии"},
"articul": {"type": "string"}
},
"required": ["name", "price_rub", "availability", "articul"],
"additionalProperties": False
}
}
},
"required": ["items"],
"additionalProperties": False
}
}
}
payload = {
"model": "gpt-4o",
"messages": [
{
"role": "system",
"content": "Ты — точный экстрактор данных. Найди все товары на картинке и верни их строго в формате JSON. Если артикул отсутствует, верни 'N/A'."
},
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": f"data:image/png;base64,{base64_img}"}}
]
}
],
"response_format": schema,
"temperature": 0.1 # Рубим креативность, оставляем сухую экстракцию
}
response = requests.post("https://api.routerapi.com/v1/chat/completions", headers=headers, json=payload)
response.raise_for_status
raw_content = response.json['choices'][0]['message']['content']
return json.loads(raw_content)
Ограничения, лимиты и экономика
Визуальный парсинг решает боль нестабильной верстки, но приносит собственные инфраструктурные компромиссы:
- Разрешение изображения и контекстное окно. Модели обрезают и сжимают огромные скриншоты. Отправить картинку размером 1080x8000 пикселей — значит получить кашу вместо текста. Решение: нарезка (tiling). Playwright скроллит таблицу и делает снимки блоками по 1080x1080. Затем мы отдаем эти блоки в LLM поочередно или батчем.
- Финансовые затраты. Традиционный парсинг DOM ничего не стоит. Запрос картинки с
high-детализацией вgpt-4oобойдется примерно в $0.005–$0.01 за страницу. Если задача требует обработки миллиона карточек в день, экономика проекта рухнет. - Скорость работы (Latency). Отработка XPath-селектора занимает микросекунды. Поднятие headless-браузера, рендер страницы, отправка тяжелого скриншота по сети и ожидание генерации токенов займут от 3 до 8 секунд. Метод неприменим для арбитражных роботов, где миллисекундная задержка критична.
- Галлюцинации на стыках. Если скриншот обрезан посередине текстовой строки, модель попытается «додумать» недостающие символы. Приходится жестко контролировать координаты viewport-а.
Выбор стратегии
Визуальный скрейпинг побеждает там, где стоимость зарплаты разработчика, непрерывно чинящего парсеры, превышает счет за API нейросетей. Если сайт донора агрессивно меняет имена классов, но визуально дизайн статичен — переводите извлечение данных на GPT-4o.
Лучшие результаты на практике показывает гибридная архитектура. Система штатно работает через легкие XPath-селекторы. Как только парсер ловит NoSuchElementException, код перехватывает ошибку, делает фолбэк на Playwright-скриншот и прогоняет его через Vision-модель. Данные спасаются без простоя, а инженер получает алерт о необходимости обновить селекторы в спокойном режиме.
Переход от парсинга структуры к парсингу смысла — закономерный шаг. Мультимодальные модели дешевеют, и скоро писать регулярки для разбора HTML-кода станет таким же анахронизмом, как писать сайты на чистом ассемблере.