Парсинг неструктурированных сайтов в JSON с помощью Vision-моделей

27.06.2026 09:00

Любой инженер, собиравший данные с внешних ресурсов, знает эту фантомную боль. Вы тратите два дня на реверс-инжиниринг сайта-донора. Распаковываете обфусцированный 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

Для надежного пайплайна визуального парсинга мы соберем связку из трех компонентов:

  1. Автоматизация браузера (Playwright) — для рендеринга страницы, подавления поп-апов и захвата чистого скриншота.
  2. Единый шлюз доступа к LLM (RouterAPI) — для обеспечения отказоустойчивости и прозрачного переключения провайдеров.
  3. Механизм 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)

Ограничения, лимиты и экономика

Визуальный парсинг решает боль нестабильной верстки, но приносит собственные инфраструктурные компромиссы:

  1. Разрешение изображения и контекстное окно. Модели обрезают и сжимают огромные скриншоты. Отправить картинку размером 1080x8000 пикселей — значит получить кашу вместо текста. Решение: нарезка (tiling). Playwright скроллит таблицу и делает снимки блоками по 1080x1080. Затем мы отдаем эти блоки в LLM поочередно или батчем.
  2. Финансовые затраты. Традиционный парсинг DOM ничего не стоит. Запрос картинки с high-детализацией в gpt-4o обойдется примерно в $0.005–$0.01 за страницу. Если задача требует обработки миллиона карточек в день, экономика проекта рухнет.
  3. Скорость работы (Latency). Отработка XPath-селектора занимает микросекунды. Поднятие headless-браузера, рендер страницы, отправка тяжелого скриншота по сети и ожидание генерации токенов займут от 3 до 8 секунд. Метод неприменим для арбитражных роботов, где миллисекундная задержка критична.
  4. Галлюцинации на стыках. Если скриншот обрезан посередине текстовой строки, модель попытается «додумать» недостающие символы. Приходится жестко контролировать координаты viewport-а.

Выбор стратегии

Визуальный скрейпинг побеждает там, где стоимость зарплаты разработчика, непрерывно чинящего парсеры, превышает счет за API нейросетей. Если сайт донора агрессивно меняет имена классов, но визуально дизайн статичен — переводите извлечение данных на GPT-4o.

Лучшие результаты на практике показывает гибридная архитектура. Система штатно работает через легкие XPath-селекторы. Как только парсер ловит NoSuchElementException, код перехватывает ошибку, делает фолбэк на Playwright-скриншот и прогоняет его через Vision-модель. Данные спасаются без простоя, а инженер получает алерт о необходимости обновить селекторы в спокойном режиме.

Переход от парсинга структуры к парсингу смысла — закономерный шаг. Мультимодальные модели дешевеют, и скоро писать регулярки для разбора HTML-кода станет таким же анахронизмом, как писать сайты на чистом ассемблере.

Теги

Ещё по теме