2026-04-24 20:47:18 +03:00
2026-04-24 20:46:58 +03:00
2026-04-24 20:46:58 +03:00
2026-04-24 20:47:18 +03:00
2026-04-24 20:46:58 +03:00
2026-04-09 20:34:20 +03:00
2026-04-24 20:46:58 +03:00
2026-04-24 20:47:18 +03:00
2026-04-24 20:46:58 +03:00
2026-04-24 20:46:58 +03:00
2026-04-24 20:46:58 +03:00
2026-04-24 20:46:58 +03:00
2026-04-24 20:46:58 +03:00
2026-04-24 20:47:18 +03:00
2026-04-24 20:46:58 +03:00
2026-04-24 20:46:58 +03:00

DUBIZZLE Scraper

Парсер аукционных автомобилей с dubizzle.com. Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright, FastAPI, Celery и Docker.

Как устроен сайт и его защита

У DUBIZZLE стоит Imperva Incapsula — внешний WAF и anti-bot.

  • Anti-bot — headless Chromium без прокси часто режется.
  • Динамическая подгрузка — часть данных приходит через XHR (/Search, /VehicleDetail), часть остаётся в HTML.
  • Cookie consent — при первом заходе показывают баннер.

Поэтому в проекте используются Playwright, паузы между действиями, прокси и сохранение браузерного состояния.

Локальный запуск (без Docker)

Требования

  • Python 3.11+
  • Git

Docker не нужен. Данные хранятся в SQLite-файле dubizzle_scraper.db в корне проекта.

Быстрый старт

git clone <repo-url>
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).

Готовый .env уже в репозитории и настроен на SQLite ничего менять не нужно.

Запуск парсера

Скрапинг одной машины:

dubizzle sync-vehicle "https://www.dubizzle.com/VehicleDetail/45089484~US"

Сбор листинга + скрапинг (например Toyota, 10 штук):

dubizzle sync-listing --make Toyota --limit 10

Только ссылки с листинга (без скрапинга):

dubizzle collect-listing --make Toyota

С видимым браузером (для отладки):

dubizzle --headless false sync-vehicle "https://www.dubizzle.com/VehicleDetail/45089484~US"

Проверка, что записалось в базу (dubizzle_scraper.db):

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:

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

Применить миграции и запустить в трёх терминалах:

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 (все сервисы в контейнерах)

cp .env.example .env
docker compose up -d

Будут запущены сервисы postgres, redis, migrate, api, worker, beat. API доступен на http://localhost:8000.

В Docker-образ дополнительно проверяется Python-синтаксис на этапе сборки (python -m compileall -q dubizzle_scraper), чтобы не выкатывать битый код.

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

docker compose ps
  • api имеет healthcheck на GET /health
  • worker имеет healthcheck через celery inspect ping
  • beat имеет healthcheck по файлу celerybeat-schedule

Разовый сбор через Docker с сохранением результата на хосте

Для ручного прогона листинга через Algolia без запуска beat используй отдельный one-shot сервис sync:

docker compose up -d postgres redis migrate
docker compose run --rm --profile manual sync

По умолчанию сервис запускает:

dubizzle sync-listing --limit 100 --output /app/artifacts/json/docker_sync_listing.json

Результат будет доступен на хосте в artifacts/json/docker_sync_listing.json, потому что каталог ./artifacts примонтирован в контейнеры.

Чтобы изменить лимит без редактирования docker-compose.yml, передай env:

docker compose run --rm --profile manual -e DUBIZZLE_SYNC_LISTING_LIMIT=500 sync

Для непрерывного режима оставь обычный стек:

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.

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

Задачи:

  • POST /api/v1/tasks/sync-vehicle — скрапнуть одну машину по URL
  • POST /api/v1/tasks/sync-listing — запустить полный обход листинга
  • GET /api/v1/sync-runs — история запусков

Скрапим конкретную машину:

curl -X POST http://localhost:8000/api/v1/tasks/sync-vehicle \
  -H "Content-Type: application/json" \
  -d '{"vehicle_url": "https://www.dubizzle.com/VehicleDetail/45089484~US"}'

CLI

Для отладки без API и Celery:

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.

Структура

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.

Вручную:

alembic upgrade head
alembic revision --autogenerate -m "add_column_x"

Тесты

pytest -q

Тесты работают на SQLite in-memory, без внешних зависимостей.

pyproject.toml + uv.lock

Локально можно использовать:

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

Пример:

{
  "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 и расширяемый фильтр.

Description
No description provided
Readme 1.3 MiB
Languages
Python 99.1%
Shell 0.5%
Dockerfile 0.3%
Mako 0.1%