Классический парсинг хрупок. Сайт поменял вёрстку — скрипт сломался. Данные в свободном тексте — нужно регулярное выражение на полстраницы. 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
...
Правовая сторона
Три вопроса перед началом парсинга:
- Robots.txt — проверьте
User-agent: * / Disallow. Если раздел закрыт — не парсьте. - Terms of Service — многие сайты запрещают автоматический сбор данных в ToS. Это не всегда юридически обязывает, но важно знать.
- Персональные данные — если собираете ФИО, контакты физлиц — требования 152-ФЗ. Данные юрлиц под него не подпадают.
Открытые данные (цены, характеристики товаров, публичные расписания) — как правило, парсить можно. Всё, что за авторизацией — нельзя без разрешения.
Есть идея? Реализуем
Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

