From 278fbe6d086ac968b0114a39a71617bdb6e0fd47 Mon Sep 17 00:00:00 2001 From: qananasikq Date: Fri, 24 Apr 2026 21:00:28 +0300 Subject: [PATCH] update readme --- README.md | 512 +++++++++++++++++++++++------------------------------- 1 file changed, 216 insertions(+), 296 deletions(-) diff --git a/README.md b/README.md index 643ec34..ec5b59c 100644 --- a/README.md +++ b/README.md @@ -1,276 +1,281 @@ -# DUBIZZLE Scraper +# Dubizzle scraper -Парсер аукционных автомобилей с [dubizzle.com](https://www.dubizzle.com). Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright, FastAPI, Celery и Docker. +Сервис для сбора объявлений с `dubizzle.com`. -## Как устроен сайт и его защита +Основной сценарий работы: -У DUBIZZLE стоит **Imperva Incapsula** — внешний WAF и anti-bot. +- discovery списка машин через `Algolia` +- при необходимости fallback на сайт +- нормализация полей автомобиля +- upsert в PostgreSQL +- периодический запуск через `Celery Beat` -- **Anti-bot** — headless Chromium без прокси часто режется. -- **Динамическая подгрузка** — часть данных приходит через XHR (`/Search`, `/VehicleDetail`), часть остаётся в HTML. -- **Cookie consent** — при первом заходе показывают баннер. +## Что делает проект -Поэтому в проекте используются Playwright, паузы между действиями, прокси и сохранение браузерного состояния. +Проект: -## Локальный запуск (без Docker) +- собирает все доступные машины из каталога `Dubizzle` +- использует `Algolia` как основной источник discovery +- обходит лимит одного запроса через сегментацию каталога +- сохраняет автомобили и изображения в PostgreSQL +- отдает API для просмотра данных и запуска задач +- поддерживает hourly/full scan через `Celery` -### Требования +## Текущая архитектура -- **Python 3.11+** -- **Git** +Сейчас проект настроен под `Dubizzle`. -Docker **не нужен**. Данные хранятся в SQLite-файле `dubizzle_scraper.db` в корне проекта. +Ключевые особенности: -### Быстрый старт +- пакет проекта: `dubizzle_scraper` +- режим discovery по умолчанию: `algolia` +- full scan включен по умолчанию +- авто-сегментация: `DUBIZZLE_LISTING_SEGMENTS=auto` +- параллельный запуск сегментов: `CELERY_PARALLEL_SEGMENTS=true` -```bash -git clone -cd dubizzle_scraper_project -pip install -e . -playwright install firefox -dubizzle init-db -``` +### Как собирается весь каталог -`dubizzle init-db` актуален для SQLite/локального CLI-режима. Для PostgreSQL используйте Alembic-миграции (`alembic upgrade head` или сервис `migrate` в Docker). +Один широкий запрос в `Algolia` упирается примерно в лимит $10{,}000$ доступных результатов. Поэтому проект использует сегментацию по годам. -Готовый `.env` уже в репозитории и настроен на SQLite ничего менять не нужно. +Авто-сегменты (`auto`) сейчас разбивают каталог на 8 диапазонов по году: -### Запуск парсера +- `1900-2012` +- `2013-2015` +- `2016-2017` +- `2018-2019` +- `2020-2021` +- `2022-2023` +- `2024-2025` +- `2026-2027` -**Скрапинг одной машины:** +Если сегмент все равно слишком большой, discovery дополнительно режет его по `id`-диапазонам. -```bash -dubizzle sync-vehicle "https://www.dubizzle.com/VehicleDetail/45089484~US" -``` +## Стек -**Сбор листинга + скрапинг (например Toyota, 10 штук):** +- `Python 3.11+` +- `Playwright` +- `FastAPI` +- `Celery` +- `Redis` +- `PostgreSQL` +- `SQLAlchemy` +- `Alembic` +- `Docker Compose` -```bash -dubizzle sync-listing --make Toyota --limit 10 -``` +## Быстрый старт через Docker -**Только ссылки с листинга (без скрапинга):** +Это основной рекомендуемый способ запуска. -```bash -dubizzle collect-listing --make Toyota -``` - -**С видимым браузером (для отладки):** - -```bash -dubizzle --headless false sync-vehicle "https://www.dubizzle.com/VehicleDetail/45089484~US" -``` - -**Проверка, что записалось в базу (`dubizzle_scraper.db`):** - -```bash -python -c "import sqlite3; c=sqlite3.connect('dubizzle_scraper.db'); q=c.cursor(); print('cars:', q.execute('select count(*) from cars').fetchone()[0]); print('images:', q.execute('select count(*) from images').fetchone()[0]); rows=q.execute('select id, brand, model, year, origin_id from cars order by id desc limit 10').fetchall(); [print(r) for r in rows]" -``` - -Результаты сохраняются в `artifacts/json/` и в БД `dubizzle_scraper.db`. - -### Полный стек (API + Worker + Beat) - -Для API и автоматического сбора нужны **Redis** и **PostgreSQL**. В `.env` раскомментируй строки Redis/Celery и замени БД на PostgreSQL: - -```env -DUBIZZLE_DATABASE_URL=postgresql+psycopg2://dubizzle:dubizzle@localhost:5432/dubizzle_scraper -DUBIZZLE_REDIS_URL=redis://localhost:6379/0 -CELERY_BROKER_URL=redis://localhost:6379/0 -CELERY_RESULT_BACKEND=redis://localhost:6379/0 -``` - -Применить миграции и запустить в трёх терминалах: - -```bash -alembic upgrade head - -# Терминал 1 — API -uvicorn dubizzle_scraper.api.app:app --reload --port 8000 - -# Терминал 2 — Celery Worker -celery -A dubizzle_scraper.worker.celery_app worker --loglevel=info --concurrency=1 --pool=solo -Q scraping - -# Терминал 3 — Celery Beat (периодический запуск) -celery -A dubizzle_scraper.worker.celery_app beat --loglevel=info -``` - -API: `http://localhost:8000` · Swagger: `http://localhost:8000/docs` - ---- - -## Запуск через Docker (все сервисы в контейнерах) +### 1. Подготовить окружение ```bash cp .env.example .env +``` + +Если нужно, отредактируй `.env`. + +### 2. Запустить стек + +```bash docker compose up -d ``` -Будут запущены сервисы `postgres`, `redis`, `migrate`, `api`, `worker`, `beat`. API доступен на `http://localhost:8000`. +Поднимутся сервисы: -В Docker-образ дополнительно проверяется Python-синтаксис на этапе сборки (`python -m compileall -q dubizzle_scraper`), чтобы не выкатывать битый код. +- `postgres` +- `redis` +- `migrate` +- `api` +- `worker` +- `beat` -`entrypoint.sh` поднимает SOCKS5→HTTP proxy bridge (если задан `SOCKS5_PROXY_HOST`) и запускает `Xvfb` только для `worker` и CLI scraping-команд. - -По умолчанию `beat` запускает сбор листинга **раз в 1 час** (`CELERY_BEAT_SYNC_INTERVAL_MINUTES=60`). -В текущей конфигурации hourly-цикл делает **полный обход всех машин через Algolia с серверной сегментацией**: - -- `DUBIZZLE_DISCOVERY_MODE=algolia` -- `DUBIZZLE_LISTING_SEGMENTS=auto` -- `DUBIZZLE_ALWAYS_FULL_SCAN=true` -- `CELERY_PARALLEL_SEGMENTS=true` - -Это означает, что каждый beat-тик разбивает каталог на year-only сегменты и проходит их через Algolia, чтобы обойти лимит одного общего запроса и собрать весь каталог, а не только первые ~10k записей. - -Swagger-документация: `http://localhost:8000/docs` +### 3. Проверить статус ```bash docker compose ps ``` -- `api` имеет healthcheck на `GET /health` -- `worker` имеет healthcheck через `celery inspect ping` -- `beat` имеет healthcheck по файлу `celerybeat-schedule` +API будет доступен по адресу: -### Разовый сбор через Docker с сохранением результата на хосте +- `http://localhost:18000` +- Swagger: `http://localhost:18000/docs` -Для ручного прогона листинга через `Algolia` без запуска `beat` используй отдельный one-shot сервис `sync`: +> Порт пробрасывается как `${DUBIZZLE_API_HOST_PORT:-18000}:8000`. + +## Полный сброс и чистый старт + +Если нужен запуск с нуля с чистой БД и чистым Redis: + +```bash +docker compose down -v --remove-orphans +docker compose up -d --build +``` + +Важно: + +- `docker compose down` **не удаляет volume** +- `docker compose down -v` удаляет данные `PostgreSQL` и `Redis` + +## Основные Docker-сервисы + +### `postgres` + +PostgreSQL база проекта. + +По умолчанию: + +- DB: `dubizzle_scraper` +- user: `dubizzle` +- password: `dubizzle` +- host port: `15432` + +### `redis` + +Используется как: + +- broker для `Celery` +- backend для task state +- хранилище progress/state ключей + +По умолчанию host port: `16379` + +### `migrate` + +Отдельный сервис, который выполняет: + +```bash +alembic upgrade head +``` + +### `api` + +Запускает: + +```bash +uvicorn dubizzle_scraper.api.app:app --host 0.0.0.0 --port 8000 +``` + +### `worker` + +Запускает `Celery worker` и выполняет scraping-задачи. + +### `beat` + +Планировщик periodic tasks. + +По умолчанию проект настроен на запуск каждые `60` минут. + + +## Ручной запуск задач в Docker + +### Разовый sync listing ```bash -docker compose up -d postgres redis migrate docker compose run --rm --profile manual sync ``` -По умолчанию сервис запускает: +По умолчанию это запустит: ```bash dubizzle sync-listing --limit 100 --output /app/artifacts/json/docker_sync_listing.json ``` -Результат будет доступен на хосте в `artifacts/json/docker_sync_listing.json`, потому что каталог `./artifacts` примонтирован в контейнеры. +Результат будет на хосте в: -Чтобы изменить лимит без редактирования `docker-compose.yml`, передай env: +- `artifacts/json/docker_sync_listing.json` + +### Запуск с другим лимитом ```bash docker compose run --rm --profile manual -e DUBIZZLE_SYNC_LISTING_LIMIT=500 sync ``` -Для непрерывного режима оставь обычный стек: +### Запуск непрерывного режима ```bash docker compose up -d api worker beat ``` -В текущей конфигурации Docker использует `DUBIZZLE_DISCOVERY_MODE=algolia`, поэтому worker и one-shot `sync` идут в `Algolia` первым путём, а браузерный fallback включается только при ошибке discovery. -Для полного hourly/full bootstrap охвата держи включёнными `DUBIZZLE_LISTING_SEGMENTS=auto`, `DUBIZZLE_ALWAYS_FULL_SCAN=true` и `CELERY_PARALLEL_SEGMENTS=true`. +## Локальный запуск без Docker +Локальный запуск возможен, но проект в первую очередь ориентирован на `PostgreSQL + Redis`. + +### Установка + +```bash +pip install -e . +playwright install chromium +``` + +### Миграции + +```bash +alembic upgrade head +``` + +### Запуск API + +```bash +uvicorn dubizzle_scraper.api.app:app --reload --port 8000 +``` + +### Запуск worker + +```bash +celery -A dubizzle_scraper.worker.celery_app worker --loglevel=info --concurrency=1 --pool=solo -Q scraping +``` + +### Запуск beat + +```bash +celery -A dubizzle_scraper.worker.celery_app beat --loglevel=info +``` + + +### Полный sync листинга + +```bash +dubizzle sync-listing --limit 100 +``` + +Если включена сегментация и не переданы `make`, `model`, `limit`, CLI автоматически пойдет в segmented sync. ## API -**Статистика:** -- `GET /health` — статус сервиса и подключения к БД -- `GET /api/v1/stats` — сколько машин и картинок в базе, топ брендов +Базовые роуты: -**Машины:** -- `GET /api/v1/cars` — список с пагинацией -- `GET /api/v1/cars/{id}` — карточка с картинками -- `GET /api/v1/cars/by-origin/{origin_id}` — поиск по DUBIZZLE stock number +### Health -**Задачи:** -- `POST /api/v1/tasks/sync-vehicle` — скрапнуть одну машину по URL -- `POST /api/v1/tasks/sync-listing` — запустить полный обход листинга -- `GET /api/v1/sync-runs` — история запусков +- `GET /health` -Скрапим конкретную машину: +### Машины + +- `GET /api/v1/cars` +- `GET /api/v1/cars/{car_id}` +- `GET /api/v1/cars/by-origin/{origin_id}` +- `GET /api/v1/stats` + +### Задачи + +- `POST /api/v1/tasks/sync-vehicle` +- `POST /api/v1/tasks/sync-listing` +- `GET /api/v1/tasks/{task_id}` +- `GET /api/v1/sync-runs` + +### Пример запуска sync vehicle ```bash -curl -X POST http://localhost:8000/api/v1/tasks/sync-vehicle \ +curl -X POST http://localhost:18000/api/v1/tasks/sync-vehicle \ -H "Content-Type: application/json" \ -d '{"vehicle_url": "https://www.dubizzle.com/VehicleDetail/45089484~US"}' ``` -## CLI - -Для отладки без API и Celery: +### Пример запуска sync listing ```bash -dubizzle init-db -dubizzle collect-listing --make Toyota -dubizzle sync-vehicle "https://www.dubizzle.com/VehicleDetail/45089484~US" -dubizzle sync-listing --limit 10 -``` - -`dubizzle init-db` используйте для SQLite/локальных тестов. В PostgreSQL-сценарии применяйте миграции Alembic. - -## Структура - -```text -dubizzle_scraper/ - scraper.py — оркестратор: связывает browser → parser → storage - cli.py — CLI-команды (init-db, sync-vehicle, sync-listing, ...) - proxy_bridge.py — HTTP→SOCKS5 мост (Chromium не умеет SOCKS5 с авторизацией) - - browser/ - factory.py — создание браузера, stealth-инъекции, fingerprint - listing.py — сбор ссылок на машины с листинга, пагинация - network.py — перехват XHR/fetch ответов через Playwright events - pace.py — рандомные паузы и движение мыши - - parsing/ - parser.py — VehicleParser: DOM + XHR JSON + embedded JSON - mapper.py — CarMapper: нормализация → CarRecord - - storage/ - models.py — SQLAlchemy: Car, Image, SyncRun - schemas.py — Pydantic: CarRecord, ImageRecord, CarRead - enums.py — допустимые значения (drive, gearbox, body_type, ...) - db.py — PersistenceService: upsert, sync runs, статистика - - api/ - app.py — FastAPI factory, lifespan, роутеры - deps.py — dependency injection (Settings, PersistenceService) - routes/ - health.py — GET /health - cars.py — CRUD по машинам + GET /stats - tasks.py — запуск Celery-задач и история sync-runs - - worker/ - celery_app.py — конфиг Celery, beat-расписание - tasks.py — sync_vehicle_task, sync_listing_task - - core/ - config.py — Settings (dataclass), env-переменные - logs.py — логирование с trace_id (ContextVar) - retry.py — декоратор @retryable с exponential backoff - runtime_config.py — чтение и нормализация runtime-конфига - utils.py — VIN_RE, deep_find_key, вспомогательные функции - -alembic/ — миграции БД -tests/ — тесты -``` - -## Конфигурация - -Всё через env-переменные (полный список в `.env.example`): - -- **БД:** `DUBIZZLE_DATABASE_URL`, `DUBIZZLE_DATABASE_POOL_SIZE` -- **Redis:** `DUBIZZLE_REDIS_URL` -- **Celery:** `CELERY_BROKER_URL`, `CELERY_BEAT_SYNC_INTERVAL_MINUTES` -- **Скрапер:** `DUBIZZLE_HEADLESS`, `DUBIZZLE_SYNC_ONLY_NEW`, `DUBIZZLE_MAX_PAGES_PER_RUN` -- **Прокси:** `DUBIZZLE_PROXY_SERVER`, `DUBIZZLE_PROXY_USERNAME`, `DUBIZZLE_PROXY_PASSWORD` -- **Паузы:** `DUBIZZLE_BETWEEN_VEHICLES_MIN_S`, `DUBIZZLE_AFTER_PAGE_CHANGE_MAX_S` и т.д. - -## Миграции - -В Docker миграции выполняются отдельным сервисом `migrate` (`alembic upgrade head`). - -`api`, `worker`, `beat` стартуют после успешного завершения `migrate`. - -Вручную: - -```bash -alembic upgrade head -alembic revision --autogenerate -m "add_column_x" +curl -X POST http://localhost:18000/api/v1/tasks/sync-listing \ + -H "Content-Type: application/json" \ + -d '{"limit": 100, "only_new": false}' ``` ## Тесты @@ -279,98 +284,13 @@ alembic revision --autogenerate -m "add_column_x" pytest -q ``` -Тесты работают на SQLite in-memory, без внешних зависимостей. - -## `pyproject.toml` + `uv.lock` - -Локально можно использовать: - -```bash -uv sync -uv run pytest -q -``` - -### Секция `sync` - -Поддерживаются поля: - -- `name` -- `ids_initial_size` -- `ids_next_size` -- `ids_max_pages` -- `condition_check_enabled` -- `lane` -- `only_new` -- `limit` - -### Секция `filters` - -Поддерживаются поля: - -- `brands` -- `models` -- `years` -- `body_types` -- `colors` -- `drives` -- `gearboxes` -- `locations` -- `exclude_brands` -- `exclude_models` -- `exclude_years` -- `exclude_body_types` -- `exclude_colors` -- `exclude_drives` -- `exclude_gearboxes` -- `exclude_locations` -- `price.min`, `price.max` -- `mileage.min`, `mileage.max` -- `flags.damaged_only`, `flags.run_and_drive` - -Пример: - -```json -{ - "sync": { - "name": null, - "ids_initial_size": null, - "ids_next_size": null, - "ids_max_pages": null, - "condition_check_enabled": false, - "lane": "dubizzle_cars", - "only_new": true, - "limit": 50 - }, - "filters": { - "price": { - "min": null, - "max": null - }, - "mileage": { - "min": null, - "max": null - }, - "flags": { - "damaged_only": null, - "run_and_drive": null - }, - "brands": ["Toyota", "Honda"], - "models": [], - "years": [2021, 2022], - "body_types": ["SUV"], - "colors": [], - "drives": [], - "gearboxes": [], - "locations": [], - "exclude_brands": [], - "exclude_models": [], - "exclude_years": [], - "exclude_body_types": [] - } -} -``` - -Путь задаётся через `DUBIZZLE_RUNTIME_CONFIG_FILE`, по умолчанию — `/app/runtime_config.json`. - -CLI и API аргументы имеют приоритет, а `runtime_config.json` работает как runtime-default и расширяемый фильтр. +Тесты покрывают: +- mapper +- parser +- listing +- scraper +- worker tasks +- resilience +- self-heal +- db \ No newline at end of file