Умный парсинг: LLM извлекает данные из любого сайта

Классический парсинг хрупок. Сайт поменял вёрстку — скрипт сломался. Данные в свободном тексте — нужно регулярное выражение на полстраницы. LLM решает эти проблемы, но создаёт новые. Разберём, когда и как использовать каждый подход.

Классический парсинг и его пределы

BeautifulSoup + CSS-селекторы — правильный выбор для структурированных данных:

import httpx
from bs4 import BeautifulSoup

r = httpx.get("https://example.com/catalog",
              headers={"User-Agent": "Mozilla/5.0"})
soup = BeautifulSoup(r.text, "lxml")

products = []
for card in soup.select(".product-card"):
    products.append({
        "name": card.select_one(".product-title").text.strip(),
        "price": card.select_one(".price").text.strip(),
        "sku": card.get("data-sku"),
    })

Это работает идеально, пока структура стабильна. Ломается при:

  • разных шаблонах карточек на одном сайте
  • данных, вложенных в неструктурированный текст («Цена по запросу, звоните: …»)
  • смене вёрстки после редизайна
  • данных, которые формируются JavaScript

Когда классический парсинг не справляется

Случай 1: нестандартный текст. Справочник компаний, где контакты написаны в произвольном формате: «тел. 8(495)123 – 45-67 доп. 102, пишите на почту info@…». CSS-селектором не вытащить.

Случай 2: разнородные шаблоны. Агрегатор объявлений, где каждый продавец оформляет карточку по-своему. Один пишет характеристики в таблице, другой — в списке, третий — сплошным текстом.

Случай 3: семантические данные. Нужно извлечь «тип сделки» из описания вакансии. Там написано «работа по договору ГПХ» в одном объявлении и «проектная занятость» в другом. Смысл один, формулировки разные.

Вот где LLM выигрывает.

LLM как парсер: базовый паттерн

import httpx
from bs4 import BeautifulSoup
import anthropic
import json

client = anthropic.Anthropic()

def parse_with_llm(html: str, schema: dict, task_description: str) -> dict:
    # Очищаем HTML от мусора
    soup = BeautifulSoup(html, "lxml")
    for tag in soup(["script", "style", "svg", "img"]):
        tag.decompose()
    clean_text = soup.get_text(separator="\n", strip=True)

    # Ограничиваем размер (токены стоят денег)
    clean_text = clean_text[:8000]

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"""{task_description}

Извлеки данные из текста и верни JSON строго по схеме:
{json.dumps(schema, ensure_ascii=False, indent=2)}

Если поле не найдено — верни null.
Верни только JSON без пояснений.

Текст:
{clean_text}"""
        }]
    )

    return json.loads(response.content[0].text)

Практические примеры

Пример 1: каталог товаров без API

schema = {
    "name": "название товара",
    "price": "цена числом без валюты",
    "currency": "валюта (RUB/USD/EUR)",
    "availability": "в наличии/под заказ/нет в наличии",
    "sku": "артикул если есть",
    "characteristics": {"ключ": "значение"}
}

r = httpx.get("https://shop.example.com/product/123",
              headers={"User-Agent": "Mozilla/5.0"}, timeout=15)

product = parse_with_llm(
    r.text,
    schema,
    "Это страница товара интернет-магазина."
)
print(product)
# {"name": "Дрель Bosch GSB 13 RE", "price": 4590, "currency": "RUB", ...}

Пример 2: контакты из бизнес-каталога

schema = {
    "company_name": "название компании",
    "phones": ["список телефонов"],
    "emails": ["список email"],
    "address": "адрес",
    "working_hours": "режим работы",
    "website": "сайт если есть"
}

# Работает даже если контакты написаны в произвольном тексте
contacts = parse_with_llm(html, schema,
    "Это страница компании в бизнес-каталоге.")

Пример 3: мониторинг цен с защитой от редизайна

Классический парсер ломается при смене вёрстки. LLM-парсер продолжает работать, потому что понимает смысл, а не структуру.

import time
import httpx

def monitor_price(url: str) -> float | None:
    try:
        r = httpx.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=15)
        result = parse_with_llm(
            r.text,
            {"price": "текущая цена числом", "in_stock": True},
            "Страница товара. Найди актуальную цену."
        )
        return result.get("price")
    except Exception as e:
        print(f"Ошибка для {url}: {e}")
        return None

urls = ["https://...", "https://...", "https://..."]
for url in urls:
    price = monitor_price(url)
    print(f"{url}: {price}")
    time.sleep(2)  # Не перегружаем сервер

Гибридный подход: классика + AI для граничных случаев

Самый эффективный паттерн на практике:

def parse_product(html: str) -> dict:
    soup = BeautifulSoup(html, "lxml")

    # Сначала пробуем классический парсинг
    price_el = soup.select_one(".price, [itemprop='price'], .product-price")

    if price_el:
        # Быстро и дёшево
        return {"price": price_el.text.strip(), "source": "classic"}

    # Если не нашли — отдаём в LLM
    return {**parse_with_llm(html, {"price": "цена"}, "Товарная страница"),
            "source": "llm"}

Классический парсинг покрывает 80 – 90% случаев (быстро, бесплатно). LLM подключается только там, где CSS-селекторы не справились.

Стоимость: ручная, классическая, AI

Подход Разработка Стоимость/1000 страниц Устойчивость
Ручной сбор ~$50 – 200 (аутсорс)
Классический парсинг 4 – 8 часов $0 – 0.50 (хостинг) Низкая
AI-парсинг 2 – 4 часа $1 – 5 (API токены) Высокая
Гибридный 4 – 6 часов $0.10 – 0.80 Очень высокая

Для регулярного мониторинга 1000+ страниц в день — гибридный подход оптимален.

Про токены и лимиты

Claude claude-opus‑4 – 5 берёт ~$3 за 1M input-токенов. Страница после очистки — 1000 – 3000 токенов. Итого: $0.003 – 0.009 за страницу.

При batch-парсинге используйте Batches API Anthropic — скидка 50% на массовые запросы без срочности.

Лимиты скорости: по умолчанию 4000 RPM для claude-opus‑4 – 5 (на момент написания). При больших объёмах — asyncio с семафором:

import asyncio
import anthropic

sem = asyncio.Semaphore(10)  # Не более 10 параллельных запросов

async def parse_async(client, html: str) -> dict:
    async with sem:
        # async вызов к API
        ...

Правовая сторона

Три вопроса перед началом парсинга:

  1. Robots.txt — проверьте User-agent: * / Disallow. Если раздел закрыт — не парсьте.
  2. Terms of Service — многие сайты запрещают автоматический сбор данных в ToS. Это не всегда юридически обязывает, но важно знать.
  3. Персональные данные — если собираете ФИО, контакты физлиц — требования 152-ФЗ. Данные юрлиц под него не подпадают.

Открытые данные (цены, характеристики товаров, публичные расписания) — как правило, парсить можно. Всё, что за авторизацией — нельзя без разрешения.

Есть идея? Реализуем

Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

Написать в Telegram

20.03.2026

Нужна консультация?

Оставьте свои контактные данные, или свяжитесь с нами удобным для вас способом

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Нажимая кнопку «Принять», вы соглашаетесь на сбор cookie. Мы используем их для обеспечения функционирования веб-сайта, аналитики действий и улучшения качества обслуживания. Если Вы не хотите, чтобы эти данные обрабатывались, отключите cookie в настройках браузера или прекратите использовать сайт.
Принять