🕷️

AI-агент для парсинга сайтов

Crawl4AI + BeautifulSoup + LLM: извлечение структурированных данных из любого сайта. Headless-браузер, JavaScript-рендеринг, антиблокировка.

Средний ⏱ 20 мин
🌐 Целевой сайт 🖥️ Crawl4AI Headless Browser 📄 HTML Parser BeautifulSoup 🔍 CSS-селекторы XPath + Selector 🧹 Очистка Markdown/JSON 🧠 LLM-экстрактор Структурирование Архитектура AI-парсера: Crawl4AI → BeautifulSoup → LLM → структурированные данные

# 1. Установка инструментов

Нам понадобятся три компонента: Crawl4AI для рендеринга JavaScript-сайтов, BeautifulSoup для парсинга HTML, и OpenAI-совместимый API для структурирования данных через LLM.

# Установка Crawl4AI + зависимости
pip install crawl4ai beautifulsoup4 lxml openai python-dotenv

# Playwright (нужен для Crawl4AI)
playwright install chromium

# .env файл с API-ключом
OPENAI_API_KEY=sk-your-key-here

# 2. Базовый парсинг с BeautifulSoup

Начнём с простого: статический сайт, извлекаем заголовки, ссылки и текст через CSS-селекторы. Это работает для сайтов без JavaScript-рендеринга.

import requests
from bs4 import BeautifulSoup
import json

def parse_static_page(url):
    headers = {"User-Agent": "Mozilla/5.0 (compatible; QantcoreBot/1.0)"}
    resp = requests.get(url, headers=headers, timeout=30)
    soup = BeautifulSoup(resp.text, "lxml")
    
    # Извлекаем заголовки
    titles = [h.get_text(strip=True) for h in soup.find_all(["h1", "h2", "h3"])]
    
    # Извлекаем все ссылки
    links = [{"text": a.get_text(strip=True), "href": a.get("href")} for a in soup.find_all("a", href=True)]
    
    # Основной текст
    body = soup.find("body")
    text = body.get_text(separator="\n", strip=True) if body else ""
    
    return {"url": url, "titles": titles, "links": links, "text_length": len(text)}

result = parse_static_page("https://example.com")
print(json.dumps(result, indent=2, ensure_ascii=False))

# 3. JavaScript-рендеринг с Crawl4AI

Современные сайты (React, Vue, Angular) загружают контент динамически. Crawl4AI поднимает headless Chromium, ждёт рендеринга и возвращает уже готовый HTML. Это ключевое преимущество перед requests.

import asyncio
from crawl4ai import AsyncWebCrawler, CrawlerRunConfig

async def crawl_js_page(url):
    config = CrawlerRunConfig(
        wait_for="css:.content",          # Ждём появления элемента
        page_timeout=30000,              # 30 секунд таймаут
        js_code="window.scrollTo(0, document.body.scrollHeight)",  # Скролл для ленивой загрузки
        extract_metadata=True,
    )
    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(url, config=config)
        
        return {
            "url": url,
            "title": result.metadata.get("title"),
            "html": result.html,           # Отрендеренный HTML
            "markdown": result.markdown,     # Авто-конвертация в MD
            "screenshot": result.screenshot,  # Base64 скриншот
        }

# Запуск
data = asyncio.run(crawl_js_page("https://qantcore.space"))
print(f"Title: {data['title']}")
print(f"HTML length: {len(data['html'])}")
print(f"Markdown length: {len(data['markdown'])}")

# 4. LLM-экстрактор: из HTML в структуру

Сырой HTML содержит мусор. LLM может извлечь ровно то что нужно — цены, описания, контакты — и вернуть структурированный JSON. Используем function calling для гарантии формата вывода.

from openai import OpenAI
import json

client = OpenAI()

def extract_structured(html, schema_description):
    """Извлекает структурированные данные из HTML через LLM."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",  # Дёшево для парсинга
        temperature=0,
        messages=[{
            "role": "system",
            "content": f"Извлеки из HTML: {schema_description}. Ответ ТОЛЬКО JSON."
        }, {
            "role": "user",
            "content": html[:8000]  # Обрезаем для экономии токенов
        }],
        response_format={"type": "json_object"}
    )
    return json.loads(response.choices[0].message.content)

# Пример: извлечение товаров из интернет-магазина
html_sample = "<div class='product'><h2>iPhone 15</h2><span class='price'>799</span></div>"
result = extract_structured(html_sample, "названия товаров и цены")
print(json.dumps(result, indent=2, ensure_ascii=False))

# 5. Полный конвейер: Crawl4AI → BS4 → LLM

Собираем всё вместе: headless-браузер рендерит страницу, BeautifulSoup удаляет мусор, LLM извлекает структурированные данные. На выходе — чистый JSON готовый к загрузке в БД.

import asyncio
from crawl4ai import AsyncWebCrawler
from bs4 import BeautifulSoup
from openai import OpenAI

client = OpenAI()

async def scrape_and_extract(url, extraction_prompt):
    # 1. Рендерим страницу
    async with AsyncWebCrawler() as crawler:
        result = await crawler.arun(url)
    
    # 2. Очищаем HTML от скриптов/стилей
    soup = BeautifulSoup(result.html, "lxml")
    for tag in soup.find_all(["script", "style", "nav", "footer"]):
        tag.decompose()
    clean_html = soup.prettify()
    
    # 3. LLM извлекает данные
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        temperature=0,
        messages=[
            {"role": "system", "content": "Извлеки из HTML: {extraction_prompt}. Ответ — JSON."},
            {"role": "user", "content": clean_html[:8000]}
        ],
        response_format={"type": "json_object"}
    )
    return {
        "source_url": url,
        "title": soup.title.string if soup.title else "",
        "extracted_data": json.loads(response.choices[0].message.content)
    }

# Использование
output = asyncio.run(scrape_and_extract(
    "https://news.ycombinator.com",
    "заголовки, ссылки и количество комментариев для топ-10 постов"
))
print(json.dumps(output, indent=2, ensure_ascii=False))

# 6. Антиблокировка и ротация прокси

Сайты банят ботов по IP, User-Agent и частоте запросов. Решение: ротация User-Agent, задержки между запросами, прокси-пул.

import random, time

# Пул User-Agent'ов
USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
]

# Прокси (если есть)
PROXIES = [
    "http://proxy1:8080",
    "http://proxy2:8080",
]

async def crawl_with_anti_block(url, use_proxy=False):
    config = CrawlerRunConfig(
        user_agent=random.choice(USER_AGENTS),
        proxy=random.choice(PROXIES) if use_proxy else None,
        delay_before_return_html=2.0,  # Задержка для JS-рендеринга
    )
    async with AsyncWebCrawler() as crawler:
        return await crawler.arun(url, config=config)

# Массовый парсинг с задержками
async def crawl_many(urls, delay=3):
    results = []
    for i, url in enumerate(urls):
        res = await crawl_with_anti_block(url)
        results.append(res)
        print(f"[{i+1}/{len(urls)}] {url}")
        if i < len(urls) - 1:
            time.sleep(delay)  # Вежливая задержка
    return results
✅ Итог

Связка Crawl4AI + BeautifulSoup + LLM даёт универсального парсера для любых сайтов. Crawl4AI решает JavaScript-рендеринг, BS4 чистит HTML, LLM превращает хаос в JSON. Для продакшена добавьте прокси-пул и rate limiting. Стоимость парсинга одной страницы через GPT-4o-mini — около $0.001.

🔗 Полезные ссылки

📖 Crawl4AI Docs 📖 BeautifulSoup Docs 🤖 OpenAI API