Классический парсинг хрупок. Сайт поменял вёрстку - скрипт сломался. Данные в свободном тексте - нужно регулярное выражение на полстраницы. 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 Opus 4.5 стоит $5 за 1M входных токенов и $25 за 1M выходных. Страница после очистки - 1000-3000 токенов, то есть $0.005-0.015 за страницу по входу. Модель уже в статусе legacy, поэтому перед расчётом сверяйтесь с актуальным прайсом Anthropic.
При batch-парсинге используйте Batches API Anthropic - скидка 50% на массовые запросы без срочности.
Лимиты скорости: на стартовом тарифе для семейства Opus 4.x это 1000 запросов в минуту, 2 000 000 входных и 400 000 выходных токенов в минуту, причём лимит общий на все модели Opus 4.x. На старших тарифах он выше, свой смотрите в консоли. При больших объёмах - 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-ФЗ. Данные юрлиц под него не подпадают.
Открытые данные (цены, характеристики товаров, публичные расписания) - как правило, парсить можно. Всё, что за авторизацией - нельзя без разрешения.
Условия и возможности Claude меняются, поэтому перед запуском я уточняю их у самого сервиса.