Как собрать данные для RAG парсингом сайтов
Скормить модели десяток PDF и сотню страниц сайта — не значит собрать данные для RAG. В чанках оказываются меню навигации, футер с копирайтом и обрывки предложений на границах абзацев, а ассистент отвечает цитатой из блока «Политика конфиденциальности» вместо ответа по делу. Разберём пайплайн от sitemap.xml до записи в Qdrant: обход без бана, извлечение текста без вёрстки, чанкинг и загрузка эмбеддингов — с командами и текстами реальных ошибок на каждом шаге.
Содержание
- Зачем парсить сайты для RAG и почему это не разовая выгрузка
- Почему curl и BeautifulSoup «в лоб» не работают
- Пошаговый пайплайн: от sitemap.xml до чистого текста
- Чанкинг: как резать текст, чтобы RAG находил ответы
- Эмбеддинги и загрузка в Qdrant
- Нюансы, о которых забывают: обновление, дедупликация, права
- Какой сервер брать под парсинг и Qdrant в MAATRIX
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Зачем парсить сайты для RAG и почему это не разовая выгрузка
RAG (Retrieval-Augmented Generation) отвечает настолько хорошо, насколько хорош кусок текста, который система нашла и подсунула модели в контекст. Источник этого текста чаще всего — не PDF-мануалы, а живой сайт: документация продукта, база знаний поддержки, блог, wiki. Проблема в том, что сайт — это HTML, а не текст: заголовок статьи, контент, меню из тридцати пунктов, футер и баннер cookie-согласия лежат в одном файле вперемешку.
Если скормить эмбеддеру страницу целиком, вектор «размывается» между содержанием и мусором, а при поиске система с той же вероятностью достанет абзац из футера, что и нужный кусок инструкции. Симптом узнаваем: на вопрос про тарифы ассистент отвечает строкой «Политика конфиденциальности · Карта сайта · Контакты» — этот кусок HTML оказался ближе всего по косинусному расстоянию к вопросу.
Второй момент: сбор данных для RAG — не разовая выгрузка, а процесс. Документация меняется, статьи поддержки правятся, старые страницы удаляются. Пайплайн должен уметь пере-обходить сайт по расписанию и обновлять только изменившееся — иначе через месяц RAG уверенно отвечает по версии продукта, которой уже нет.
Почему curl и BeautifulSoup «в лоб» не работают
Первая попытка обычно выглядит так: requests.get(url), потом BeautifulSoup(html, 'html.parser').get_text(). На простом блоге на статическом HTML это даже сработает. На современном сайте — нет, сразу по нескольким причинам.
JS-рендеринг. Сайты на React, Vue и Next.js отдают requests пустую оболочку:
<div id="__next"></div>
<script src="/_next/static/chunks/main.js"></script>
Контент дорисовывает JavaScript уже в браузере. requests браузер не эмулирует — вы получите валидный HTML почти без текста.
Анти-бот защита. Дефолтный User-Agent вида python-requests/2.32.3 многие сайты банят на входе, часто через Cloudflare:
requests.exceptions.HTTPError: 403 Client Error: Forbidden for url: https://docs.example.com/api/auth
Иногда вместо 403 приходит 200 с HTML-страницей челленджа «Checking your browser before accessing…» — код успешный, а текста в теле снова нет.
get_text() тянет всё подряд. .get_text() не отличает статью от сайдбара: в чанк попадут пункты меню, ссылки «похожие статьи» и баннер cookie. Тот же мусор, что и выше, но теперь он есть на каждой странице сайта.
robots.txt и бан по IP. Обход без учёта robots.txt и Crawl-delay — не только вопрос этики: агрессивный краулер без пауз получает 429 Too Many Requests и бан IP на несколько часов, а пайплайн встаёт, толком не начавшись.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть QdrantПошаговый пайплайн: от sitemap.xml до чистого текста
Рабочая цепочка — пять шагов, каждый закрывает одну проблему из раздела выше.
1. Найти список страниц через sitemap.xml. Почти любой сайт на CMS или статическом генераторе публикует карту сайта:
curl -s https://docs.example.com/sitemap.xml | grep -oP '(?<=<loc>).*?(?=</loc>)' > urls.txt
wc -l urls.txt
Если это sitemap_index.xml со ссылками на вложенные карты — обойдите их рекурсивно тем же способом.
2. Проверить robots.txt перед каждым запросом. Стандартная библиотека Python это уже умеет:
from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url('https://docs.example.com/robots.txt')
rp.read()
allowed = rp.can_fetch('MyRAGBot/1.0', url)
delay = rp.crawl_delay('MyRAGBot/1.0') # None, если не задан
3. Забирать страницы с паузой и повтором. Указывайте честный User-Agent со ссылкой на контакт, ставьте timeout и обрабатывайте 429 через заголовок Retry-After:
import time, requests
def fetch(url, ua='MyRAGBot/1.0 (+mailto:you@example.com)'):
r = requests.get(url, headers={'User-Agent': ua}, timeout=15)
if r.status_code == 429:
time.sleep(int(r.headers.get('Retry-After', 30)))
r = requests.get(url, headers={'User-Agent': ua}, timeout=15)
r.raise_for_status()
return r.text
4. Рендерить JS-страницы через Playwright. Там, где requests вернул пустую оболочку, нужен настоящий браузер:
pip install playwright
playwright install chromium
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(url, wait_until='networkidle', timeout=60000)
html = page.content()
browser.close()
Дефолтный таймаут в 30 секунд на тяжёлых страницах выбивает playwright._impl._errors.TimeoutError: Page.goto: Timeout 30000ms exceeded. — поднимайте до 60000 и добавляйте один повтор, прежде чем считать страницу недоступной.
5. Достать основной текст, а не всю страницу. Здесь работает trafilatura — библиотека, которая эвристически отделяет статью от меню, футера и рекламы:
import trafilatura
clean = trafilatura.extract(
html, output_format='markdown',
include_tables=True, include_comments=False, include_links=False,
)
На выходе — markdown с заголовками и таблицами, без меню и футера. Именно этот текст, а не сырой HTML, идёт дальше на чанкинг.
Чанкинг: как резать текст, чтобы RAG находил ответы
Чистый markdown ещё не готов для эмбеддера: страница на 6000 символов — это один вектор, который «размывает» смысл десятка разных вопросов. Нужно резать на фрагменты, но не как попало.
Резать по структуре, а не по счётчику символов. Наивный способ — рубить текст каждые N символов — рвёт предложения и таблицы посередине строки. Правильный порядок: сначала по заголовкам markdown, затем внутри раздела — по длине, с overlap:
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=800, chunk_overlap=120,
separators=['\n## ', '\n### ', '\n\n', '\n', '. ', ' '],
)
chunks = splitter.split_text(clean_markdown)
Порядок в separators важен: сплиттер режет по первому подходящему, поэтому границы заголовков — в приоритете, а разрыв по пробелу — крайний резерв.
Overlap — защита от обрыва мысли на границе, а не запас. Без пересечения фраза «настройте timeout в секции [server]» может оказаться в одном чанке, а сама секция [server] — в следующем: ответ не найдётся ни в одном фрагменте целиком. 10–15% от chunk_size — рабочий ориентир: для FAQ с короткими Q&A его можно убрать, для длинных инструкций — увеличить.
Метаданные — обязательное поле, не опция. Каждый чанк несёт источник: URL страницы, заголовок раздела (breadcrumb вида Установка > Docker > Переменные окружения), дату обхода. Без этого RAG не сошлётся на источник, а вы не поймёте, откуда взялся неверный фрагмент при разборе жалобы на галлюцинацию.
Таблицы — отдельная головная боль. Резка по символам разрывает markdown-таблицу посередине строки: заголовки колонок остаются в одном чанке, данные — в другом, и таблица целиком уже не находится. Проще исключать таблицы из общего сплиттера и добавлять их отдельными неразрезанными чанками — по проверке '|' in chunk.
Эмбеддинги и загрузка в Qdrant
Модель эмбеддингов переводит чанк текста в вектор фиксированной размерности — по нему Qdrant ищет ближайших соседей. Выбор модели определяет размерность коллекции, требования к памяти и нужен ли внешний API вообще.
| Модель | Размерность | Где считается | Вес модели |
|---|---|---|---|
text-embedding-3-small (OpenAI) | 1536 | облако, платный API | веса не раздаются |
intfloat/multilingual-e5-large | 1024 | локально | ~2,2 ГБ |
nomic-embed-text (через Ollama) | 768 | локально | ~270 МБ |
all-MiniLM-L6-v2 | 384 | локально | ~90 МБ |
Для RU/EN текстов берите мультиязычную модель вроде multilingual-e5-large — англоязычные модели вроде MiniLM на кириллице заметно теряют в качестве поиска. Локальный вариант через sentence-transformers не требует API-ключа и не отправляет содержимое сайта наружу:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer('intfloat/multilingual-e5-large')
vectors = model.encode(chunks, normalize_embeddings=True)
Qdrant поднимается одним контейнером:
docker run -d --name qdrant -p 6333:6333 -p 6334:6334 \
-v $(pwd)/qdrant_storage:/qdrant/storage \
qdrant/qdrant:v1.12.0
Порт 6333 — REST и веб-панель http://localhost:6333/dashboard, 6334 — gRPC. Размерность коллекции обязана совпасть с моделью:
from qdrant_client import QdrantClient
from qdrant_client.models import VectorParams, Distance
client = QdrantClient(host='localhost', port=6333)
client.create_collection(
collection_name='site_docs',
vectors_config=VectorParams(size=1024, distance=Distance.COSINE),
)
Собрали коллекцию под MiniLM (384), затем переключились на multilingual-e5-large (1024) — Qdrant отказывает предметно:
{"status":{"error":"Wrong input: Vector dimension error: expected dim: 384, got 1024"},"time":0.000123}
Пересоздать коллекцию проще, чем чинить: старые векторы с чужой размерностью бесполезны. Вторая типичная ошибка — Qdrant не поднят или порт не тот:
httpx.ConnectError: [Errno 111] Connection refused
Память. Qdrant по умолчанию держит индекс в RAM: объём ≈ число векторов × размерность × 4 байта, плюс расходы на граф HNSW. Для 100 000 чанков на multilingual-e5-large (1024 измерения): 100 000 × 1024 × 4 = 409 600 000 байт, около 390 МБ только на сырые векторы — с индексом и payload выше. От нескольких миллионов точек документация рекомендует on_disk: true или квантование.
Нюансы, о которых забывают: обновление, дедупликация, права
Первый прогон пайплайна — самая простая часть. Дальше начинаются вопросы, которые на старте не задают, а упираются в них уже через месяц работы.
Инкрементальный обход, а не полная перекачка каждый раз. Гонять весь сайт заново ежедневно — расточительно и по времени, и по нагрузке на чужой сервер. Сохраняйте хэш чистого текста в payload каждого чанка:
import hashlib
content_hash = hashlib.sha256(clean_markdown.encode('utf-8')).hexdigest()
Совпал хэш при повторном обходе — страница не менялась, пропускайте пере-эмбеддинг. Заголовки Last-Modified и ETag тоже помогают не скачивать страницу целиком ради проверки.
Удалённые страницы оставляют мёртвые векторы. Без обработки их чанки в Qdrant останутся навсегда и будут всплывать в ответах на несуществующий контент. Помечайте в payload номер обхода (crawl_batch_id) и после его завершения удаляйте точки, чей crawl_batch_id не совпал с актуальным — «что не подтвердилось, то удаляем».
Дубли между зеркалами сайта. Одна и та же документация часто лежит под /en/ и /en-us/, или под доменом и поддоменом. Без дедупликации оба URL попадут в индекс как разные чанки с одинаковым текстом, и поиск начнёт возвращать два одинаковых результата вместо одного релевантного и одного другого. Хэш контента решает и эту задачу — проверяйте content_hash перед вставкой.
Права и этика. robots.txt — не юридический документ, но его нарушение — первый аргумент против вас при претензии владельца сайта. Публичную документацию и базу знаний обычно можно парсить для внутреннего RAG без вопросов; перепродажа контента или обход платного доступа — другая история. Персональные данные — для источников из ЕС и Великобритании это территория GDPR, и такие страницы стоит исключать на этапе фильтрации URL.
PDF и вложения — тема отдельной статьи. trafilatura работает с HTML, PDF-мануалы она не развернёт. Нужен отдельный шаг с pdfplumber или pypdf — закладывайте это отдельной веткой пайплайна, а не рассчитывайте, что HTML-краулер заберёт их заодно.
Какой сервер брать под парсинг и Qdrant в MAATRIX
У пайплайна два независимых потребителя ресурсов: краулер (особенно Playwright с headless Chromium) и Qdrant с индексом в памяти. Разница между «еле работает» и «спокойно работает» — это прежде всего RAM, а не CPU.
Честный минимум: 2 vCPU, 4 ГБ RAM, 40 ГБ NVMe. Хватает для последовательного обхода без параллельного Playwright — статического сайта или с редким JS-рендерингом на несколько тысяч страниц, и коллекции Qdrant на десятки тысяч векторов размерностью 384–768. Один контекст headless Chromium занимает несколько сотен мегабайт RAM, и два-три параллельных контекста на 4 ГБ уже толкают систему в своп: dmesg покажет Out of memory: Killed process 18422 (chrome).
Комфортный вариант: 4–8 vCPU, 8–16 ГБ RAM, 80–100 ГБ NVMe. Помещаются 4–6 параллельных контекстов Playwright, локальная модель эмбеддингов на CPU и коллекция на сотни тысяч — миллион точек с запасом на пересборку индекса без простоя. Для сайта с активным JS и ежедневным инкрементальным обходом — разумная отправная точка, а не запас на вырост.
Qdrant есть в каталоге приложений MAATRIX — при заказе он разворачивается автоматически поверх Ubuntu, вручную поднимать контейнер не нужно: адрес, порт и API-ключ появляются в личном кабинете. Команды docker run выше пригодятся, если сначала хотите прогнать пайплайн локально, а на сервер перенести готовый скрипт.
Локация — Великобритания (Лондон). У сайтов документации и баз знаний из ЕС и Британии геоблокировки и троттлинг по IP из России срабатывают заметно чаще, чем по европейским адресам — краулер с российского IP рискует упереться в 403 или капчу раньше, чем соберёт хотя бы половину сайта. С лондонского адреса таких проблем меньше, а пинг до европейской аудитории RAG — тоже ниже. Если источники и аудитория российские, логика разворачивается в обратную сторону и подходит локация RU.
Оплата — картой российского банка, по СБП, криптовалютой или токеном MAAT, без иностранной карты для сервера в Лондоне. Сервер приходит с чистой Ubuntu 24.04; Qdrant разворачивается автоматически, а краулер и расписание обхода через cron — уже ваш код.
Развернуть за пару минут
Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть QdrantОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Парсинг чужого сайта для RAG законен?
Для внутреннего использования публичной документации и баз знаний — как правило, да, если вы уважаете robots.txt и условия использования сайта. Перепродажа контента, обход платного доступа или сбор персональных данных — другая история, особенно для источников из ЕС и Великобритании, где действует GDPR.
Можно обойтись без Qdrant и хранить эмбеддинги в Postgres с pgvector?
Да, для небольших объёмов (условно до сотни тысяч векторов) pgvector — рабочий и честный вариант, особенно если Postgres у вас уже есть. Qdrant выигрывает на бо́льших объёмах и там, где нужна гибкая фильтрация по payload вместе с векторным поиском.
Как понять, что чанкинг настроен плохо?
По симптомам в ответах RAG: цитаты обрываются на середине предложения, всплывает текст меню или футера, один и тот же кусок документации находится сразу в нескольких почти одинаковых чанках. Повод пересмотреть chunk_size, chunk_overlap и то, что именно достаёт со страницы trafilatura.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.