Обход
Yozh Crawler обходит сайт с одного стартового URL и передаёт каждую найденную страницу по SSE. Он берёт на себя обнаружение — фронтир, дедуп, скоуп, извлечение ссылок, повторы, вежливость — а загрузку каждой страницы делегирует Yozh Scraper по HTTP. Обслуживать нужно ровно один стек Playwright, и все возможности скрейпера (прокси, stealth, сессии, правила извлечения) действуют и для обойдённых страниц.
- Предикаты скоупа —
same-domain/subdomains/all/regex, с include- & exclude-паттернами и жёсткими лимитамиmax_depth/max_pages. - Дедуп по отпечатку — URL канонизируются и хэшируются (SHA1), поэтому переставленные query-параметры и варианты с завершающим слэшем схлопываются в одно посещение.
- Round-robin фронтир по доменам — один медленный или огромный хост не может забрать всех воркеров у остальных.
- Адаптивный ограничитель частоты — глобальный token bucket по доменам (с джиттером), общий для всех задач;
429вдвое снижает RPS на время остывания. - Здоровье сессий — скоринг в стиле Crawlee:
401/403/429выводят сессию из ротации, ошибки повышают её счёт, успех понижает; ротация при счёте ≥ 3 или 50 использованиях. - Потоковая передача SSE — каждая задача шлёт события
stats,page,page_errorи финальныеdone/cancelled. Реагируйте на находки по мере их поступления.
:8000, краулер на :8001.Быстрый старт
Краулер прописан в корневом docker-compose.yml и поднимается вместе со скрейпером:
docker compose up --build # scraper → http://localhost:8000 # crawler → http://localhost:8001
Или запустите его локально без Docker — укажите в SCRAPER_URL адрес работающего скрейпера:
cd yozh-crawler pip install -r requirements.txt cp .env.example .env # edit SCRAPER_URL if needed python -m uvicorn src.main:app --reload --port 8001
Базовое использование
Отправьте обход с seed_url и scope. Вызов сразу возвращает job_id; далее обход выполняется в фоне на воркер-задачах.
curl -X POST http://localhost:8001/api/v1/crawl
-H "Content-Type: application/json"
-d '{
"seed_url": "https://example.com",
"scope": {"mode": "same-domain", "max_depth": 2, "max_pages": 50,
"per_domain_rps": 1.0, "per_domain_concurrency": 1},
"scrape_options": {"proxy_type": "none"},
"enable_scraping": false
}'
Ответ — это просто дескриптор задачи:
{ "job_id": "crawl_abc123" }
Дальше вы либо читаете события в реальном времени, либо опрашиваете задачу для получения полной записи.
Обнаружение vs сбор — переключатель enable_scraping
Один и тот же движок обхода работает в обоих режимах. Единственный булев флаг выбирает между дешёвой картой обнаружения и полным сбором данных — и каждый режим использует свою конфигурацию прокси, так что смешения нет.
Карта обнаружения
Лёгкий проход. Скрейпер по-прежнему рендерит каждую страницу (чтобы находить ссылки, построенные на JS), но краулер сохраняет только каркас — идеально для карт сайта, аудита ссылок и выявления изменений.
- Сохраняет
url·parent_url·depth·status·took_ms - Отбрасывает сырой HTML, скриншоты & извлечённые данные
- Использует более дешёвый
crawl_proxy - Минимальные объёмы данных, самые быстрые обходы
Полный сбор
Каждая посещённая страница сохраняется с полным ScrapeResponse — сырой HTML, опциональный скриншот и любые поля из ваших правил извлечения. Обход и структурирование за один проход.
- Сохраняет полный
ScrapeResponseдля каждой страницы -
raw_html·screenshot· извлечённыеdata - Использует
scrape_options.proxy_* - Передайте правила
extract, чтобы получать чистый JSON
crawl_proxy используется только при enable_scraping=false; scrape_options.proxy_* — при true. Если crawl_proxy равен null, прокси из scrape_options используется независимо от режима.Скоуп обхода
Скоуп ограничивает обход: где ему разрешено ходить и насколько интенсивно нагружать каждый хост. Это объект scope в запросе.
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
mode | same-domain · subdomains · all · regex | same-domain | Какие ссылки входят в скоуп. regex сопоставляется с include_patterns. |
include_patterns | string[] | [] | Regex-паттерны, которым URL должен соответствовать, чтобы попасть в очередь (используются режимом regex / как allow-list). |
exclude_patterns | string[] | [] | Regex-паттерны, отбрасывающие URL, даже если он иначе входит в скоуп. |
max_depth | int | 3 | Максимальная глубина ссылок от стартовой (стартовая — глубина 0). |
max_pages | int | 500 | Жёсткий лимит на число посещённых страниц до завершения обхода. |
per_domain_rps | float | 1.0 | Скорость пополнения token bucket по домену (запросов в секунду). |
per_domain_concurrency | int | 1 | Максимум одновременных запросов к одному домену. |
subdomains. Он берёт две последние метки как регистрируемый домен, поэтому даёт ложные совпадения на хостах из Public Suffix List, таких как github.io или co.uk. Для них предпочтите same-domain или regex.Потоковая выдача (SSE)
Каждая задача предоставляет поток Server-Sent Events. Читайте его, чтобы реагировать на страницы в момент их обнаружения, не дожидаясь завершения обхода.
curl -N http://localhost:8001/api/v1/crawl/crawl_abc123/events
Поток отправляет пять типов событий:
| Событие | Когда |
|---|---|
stats | Периодический снимок прогресса (visited / queued / failed / dedup_skipped / out_of_scope / retries). |
page | По одному на каждый посещённый URL. Несёт полный ScrapeResponse в режиме сбора и лёгкие метаданные в режиме обнаружения. |
page_error | URL, который не удалось загрузить после всех повторов. |
done | Финальное — обход завершился штатно. |
cancelled | Финальное — обход был отменён. |
Событие page в режиме обнаружения выглядит так:
{
"event": "page",
"url": "https://example.com/docs/intro",
"parent_url": "https://example.com/docs",
"depth": 2,
"status_code": 200,
"took_ms": 812,
"scrape_response": null
}
Статус и результаты
Предпочитаете опрос? Запись задачи доступна в любой момент — в том числе во время обхода, со страницами, найденными к этому моменту. /results — это алиас эндпоинта задачи, оставленный для симметрии со скрейпером.
curl http://localhost:8001/api/v1/crawl/crawl_abc123 curl http://localhost:8001/api/v1/crawl/crawl_abc123/results
{
"job_id": "crawl_abc123",
"status": "running",
"stats": {
"visited": 47, "queued": 12, "failed": 0,
"dedup_skipped": 9, "out_of_scope": 31, "retries_total": 2
},
"pages": [
{ "url": "https://example.com/", "parent_url": null,
"depth": 0, "status_code": 200, "took_ms": 640 }
]
}
status проходит queued → running → done (или failed / cancelled).Отмена обхода
Остановите выполняющуюся задачу через DELETE. Мягкая отмена — вариант по умолчанию и безопасный выбор.
# soft — stop scheduling, let in-flight requests drain curl -X DELETE "http://localhost:8001/api/v1/crawl/crawl_abc123?hard=false" # hard — abort in-flight asyncio tasks immediately curl -X DELETE "http://localhost:8001/api/v1/crawl/crawl_abc123?hard=true"
?hard=true сбрасывает текущий запрос краулера, но скрейпер не может об этом узнать — рендер страницы на его стороне завершится. Предпочитайте мягкую отмену, если только не нужно остановиться прямо сейчас.MCP
fastapi-mcp смонтирован по адресу /mcp как Streamable HTTP-эндпоинт. Укажите на него Claude или Cursor — и инструменты появятся автоматически:
healthcreate_crawlget_crawlget_crawl_resultscancel_crawl
"open-crawler": {
"type": "http",
"url": "http://localhost:8001/mcp"
}
stream_crawl_events намеренно исключён из MCP — потоковые ответы не ложатся на модель инструмента «запрос/ответ».Справочник по конфигурации
Запрос — CrawlRequest
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
seed_url | string (URL) | обязательно | Единственный URL, с которого начинается обход. |
scope | CrawlScope | defaults | Границы & вежливость — см. Скоуп обхода. |
scrape_options | ScrapeOptions | defaults | Передаётся скрейперу дословно для каждой страницы (url подставляется автоматически). |
crawl_proxy | ScrapeOptions · null | null | Дешёвый прокси для режима обнаружения. Читаются только proxy_type / proxy_pool_id / proxy_geo. |
enable_scraping | bool | false | Сохранять полный ScrapeResponse для страницы (true) или только метаданные обнаружения (false). |
Для каждой страницы — ScrapeOptions (выборочно)
| Поле | Тип | По умолчанию | Описание |
|---|---|---|---|
proxy_type | none · mobile · res_static · res_rotating · dc_static · … | none | Пул прокси, через который идёт загрузка страницы. |
device | desktop · mobile | desktop | Профиль viewport / UA. |
render | bool | true | Рендерить в браузере (нужно для ссылок, построенных на JS). |
stealth | bool | true | Применять анти-детект защиту. |
wait_until | domcontentloaded · networkidle | domcontentloaded | Когда страница считается готовой. |
screenshot | bool | false | Делать скриншот (режим сбора). |
extract | ExtractRule · null | null | Правила полей CSS/XPath → структурированные data для каждой страницы. |
session_id | string · null | null | Переиспользовать авторизованную сессию скрейпера — см. Авторизованные обходы. |
Переменные окружения
| Переменная | По умолчанию | Примечания |
|---|---|---|
SCRAPER_URL | http://web-scraper:8000 | Вышестоящий скрейпер (используйте имя docker-сервиса внутри compose-сети). |
WORKERS | 2 | Число параллельных задач обхода. |
QUEUE_MAXSIZE | 200 | Глубина очереди ожидающих задач. |
JOB_TIMEOUT_MS | 3_600_000 | Лимит реального времени на одну задачу обхода. |
SCRAPER_JOB_TIMEOUT_MS | 120_000 | Таймаут запроса к скрейперу на страницу. |
MAX_RETRIES | 3 | Повторов на запрос при временном сбое. |
RETRY_HTTP_CODES | [408,429,500,502,503,504] | Коды статуса, вызывающие повтор. |
RETRY_BACKOFF_MAX | 30.0 | Верхняя граница экспоненциального backoff (секунды). |
SESSION_MAX_ERROR_SCORE | 3.0 | Порог вывода сессии из ротации. |
SESSION_MAX_USAGE | 50 | Ротация сессии после N использований. |
SESSION_BLOCKED_CODES | [401,403,429] | Коды, мгновенно выводящие сессию из ротации. |
Справочник API
| Метод | Путь | Назначение |
|---|---|---|
| POST | /api/v1/crawl | Создать задачу. Возвращает {"job_id":"…"}. |
| GET | /api/v1/crawl/{id} | Запись задачи — статус, статистика, найденные страницы. |
| GET | /api/v1/crawl/{id}/results | Алиас записи задачи. |
| GET | /api/v1/crawl/{id}/events | SSE-поток (stats/page/page_error/done/cancelled). |
| DELETE | /api/v1/crawl/{id}?hard=bool | Отменить задачу (мягко или жёстко). |
| GET | /api/v1/health | Здоровье + доступность скрейпера. |
Важные детали
robots.txt не учитывается. Краулер обходит каждый входящий в скоуп URL независимо от /robots.txt. Вы сами отвечаете за соблюдение правил robots и условий сайта на чужих ресурсах — используйте exclude_patterns и ограничения частоты, чтобы оставаться вежливыми.POST /sessions + POST /sessions/{id}/login), затем передайте scrape_options.session_id. Скрейпер прокидывает cookies + storage state в каждом запросе, поэтому каждая обойдённая страница видит авторизованное состояние.