Crawl4AI + BeautifulSoup + LLM: извлечение структурированных данных из любого сайта. Headless-браузер, JavaScript-рендеринг, антиблокировка.
Нам понадобятся три компонента: 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
Начнём с простого: статический сайт, извлекаем заголовки, ссылки и текст через 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))
Современные сайты (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'])}")
Сырой 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))
Собираем всё вместе: 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))
Сайты банят ботов по 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.