diff --git a/README.md b/README.md index da7ae97..643ec34 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# IAAI Scraper +# DUBIZZLE Scraper -Парсер аукционных автомобилей с [iaai.com](https://www.iaai.com). Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright, FastAPI, Celery и Docker. +Парсер аукционных автомобилей с [dubizzle.com](https://www.dubizzle.com). Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright, FastAPI, Celery и Docker. ## Как устроен сайт и его защита -У IAAI стоит **Imperva Incapsula** — внешний WAF и anti-bot. +У DUBIZZLE стоит **Imperva Incapsula** — внешний WAF и anti-bot. - **Anti-bot** — headless Chromium без прокси часто режется. - **Динамическая подгрузка** — часть данных приходит через XHR (`/Search`, `/VehicleDetail`), часть остаётся в HTML. @@ -19,19 +19,19 @@ - **Python 3.11+** - **Git** -Docker **не нужен**. Данные хранятся в SQLite-файле `iaai_scraper.db` в корне проекта. +Docker **не нужен**. Данные хранятся в SQLite-файле `dubizzle_scraper.db` в корне проекта. ### Быстрый старт ```bash git clone -cd iaai_scraper_project +cd dubizzle_scraper_project pip install -e . playwright install firefox -iaai init-db +dubizzle init-db ``` -`iaai init-db` актуален для SQLite/локального CLI-режима. Для PostgreSQL используйте Alembic-миграции (`alembic upgrade head` или сервис `migrate` в Docker). +`dubizzle init-db` актуален для SQLite/локального CLI-режима. Для PostgreSQL используйте Alembic-миграции (`alembic upgrade head` или сервис `migrate` в Docker). Готовый `.env` уже в репозитории и настроен на SQLite ничего менять не нужно. @@ -40,42 +40,42 @@ iaai init-db **Скрапинг одной машины:** ```bash -iaai sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US" +dubizzle sync-vehicle "https://www.dubizzle.com/VehicleDetail/45089484~US" ``` **Сбор листинга + скрапинг (например Toyota, 10 штук):** ```bash -iaai sync-listing --make Toyota --limit 10 +dubizzle sync-listing --make Toyota --limit 10 ``` **Только ссылки с листинга (без скрапинга):** ```bash -iaai collect-listing --make Toyota +dubizzle collect-listing --make Toyota ``` **С видимым браузером (для отладки):** ```bash -iaai --headless false sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US" +dubizzle --headless false sync-vehicle "https://www.dubizzle.com/VehicleDetail/45089484~US" ``` -**Проверка, что записалось в базу (`iaai_scraper.db`):** +**Проверка, что записалось в базу (`dubizzle_scraper.db`):** ```bash -python -c "import sqlite3; c=sqlite3.connect('iaai_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]" +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/` и в БД `iaai_scraper.db`. +Результаты сохраняются в `artifacts/json/` и в БД `dubizzle_scraper.db`. ### Полный стек (API + Worker + Beat) Для API и автоматического сбора нужны **Redis** и **PostgreSQL**. В `.env` раскомментируй строки Redis/Celery и замени БД на PostgreSQL: ```env -IAAI_DATABASE_URL=postgresql+psycopg2://iaai:iaai@localhost:5432/iaai_scraper -IAAI_REDIS_URL=redis://localhost:6379/0 +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 ``` @@ -86,13 +86,13 @@ CELERY_RESULT_BACKEND=redis://localhost:6379/0 alembic upgrade head # Терминал 1 — API -uvicorn iaai_scraper.api.app:app --reload --port 8000 +uvicorn dubizzle_scraper.api.app:app --reload --port 8000 # Терминал 2 — Celery Worker -celery -A iaai_scraper.worker.celery_app worker --loglevel=info --concurrency=1 --pool=solo -Q scraping +celery -A dubizzle_scraper.worker.celery_app worker --loglevel=info --concurrency=1 --pool=solo -Q scraping # Терминал 3 — Celery Beat (периодический запуск) -celery -A iaai_scraper.worker.celery_app beat --loglevel=info +celery -A dubizzle_scraper.worker.celery_app beat --loglevel=info ``` API: `http://localhost:8000` · Swagger: `http://localhost:8000/docs` @@ -108,11 +108,19 @@ docker compose up -d Будут запущены сервисы `postgres`, `redis`, `migrate`, `api`, `worker`, `beat`. API доступен на `http://localhost:8000`. -В Docker-образ дополнительно проверяется Python-синтаксис на этапе сборки (`python -m compileall -q iaai_scraper`), чтобы не выкатывать битый код. +В 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` @@ -124,6 +132,38 @@ docker compose ps - `worker` имеет healthcheck через `celery inspect ping` - `beat` имеет healthcheck по файлу `celerybeat-schedule` +### Разовый сбор через Docker с сохранением результата на хосте + +Для ручного прогона листинга через `Algolia` без запуска `beat` используй отдельный one-shot сервис `sync`: + +```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: + +```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`. + ## API @@ -134,7 +174,7 @@ docker compose ps **Машины:** - `GET /api/v1/cars` — список с пагинацией - `GET /api/v1/cars/{id}` — карточка с картинками -- `GET /api/v1/cars/by-origin/{origin_id}` — поиск по IAAI stock number +- `GET /api/v1/cars/by-origin/{origin_id}` — поиск по DUBIZZLE stock number **Задачи:** - `POST /api/v1/tasks/sync-vehicle` — скрапнуть одну машину по URL @@ -146,7 +186,7 @@ docker compose ps ```bash curl -X POST http://localhost:8000/api/v1/tasks/sync-vehicle \ -H "Content-Type: application/json" \ - -d '{"vehicle_url": "https://www.iaai.com/VehicleDetail/45089484~US"}' + -d '{"vehicle_url": "https://www.dubizzle.com/VehicleDetail/45089484~US"}' ``` ## CLI @@ -154,18 +194,18 @@ curl -X POST http://localhost:8000/api/v1/tasks/sync-vehicle \ Для отладки без API и Celery: ```bash -iaai init-db -iaai collect-listing --make Toyota -iaai sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US" -iaai sync-listing --limit 10 +dubizzle init-db +dubizzle collect-listing --make Toyota +dubizzle sync-vehicle "https://www.dubizzle.com/VehicleDetail/45089484~US" +dubizzle sync-listing --limit 10 ``` -`iaai init-db` используйте для SQLite/локальных тестов. В PostgreSQL-сценарии применяйте миграции Alembic. +`dubizzle init-db` используйте для SQLite/локальных тестов. В PostgreSQL-сценарии применяйте миграции Alembic. ## Структура ```text -iaai_scraper/ +dubizzle_scraper/ scraper.py — оркестратор: связывает browser → parser → storage cli.py — CLI-команды (init-db, sync-vehicle, sync-listing, ...) proxy_bridge.py — HTTP→SOCKS5 мост (Chromium не умеет SOCKS5 с авторизацией) @@ -213,12 +253,12 @@ tests/ — тесты Всё через env-переменные (полный список в `.env.example`): -- **БД:** `IAAI_DATABASE_URL`, `IAAI_DATABASE_POOL_SIZE` -- **Redis:** `IAAI_REDIS_URL` +- **БД:** `DUBIZZLE_DATABASE_URL`, `DUBIZZLE_DATABASE_POOL_SIZE` +- **Redis:** `DUBIZZLE_REDIS_URL` - **Celery:** `CELERY_BROKER_URL`, `CELERY_BEAT_SYNC_INTERVAL_MINUTES` -- **Скрапер:** `IAAI_HEADLESS`, `IAAI_SYNC_ONLY_NEW`, `IAAI_MAX_PAGES_PER_RUN` -- **Прокси:** `IAAI_PROXY_SERVER`, `IAAI_PROXY_USERNAME`, `IAAI_PROXY_PASSWORD` -- **Паузы:** `IAAI_BETWEEN_VEHICLES_MIN_S`, `IAAI_AFTER_PAGE_CHANGE_MAX_S` и т.д. +- **Скрапер:** `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` и т.д. ## Миграции @@ -297,7 +337,7 @@ uv run pytest -q "ids_next_size": null, "ids_max_pages": null, "condition_check_enabled": false, - "lane": "iaai_cars", + "lane": "dubizzle_cars", "only_new": true, "limit": 50 }, @@ -330,7 +370,7 @@ uv run pytest -q } ``` -Путь задаётся через `IAAI_RUNTIME_CONFIG_FILE`, по умолчанию — `/app/runtime_config.json`. +Путь задаётся через `DUBIZZLE_RUNTIME_CONFIG_FILE`, по умолчанию — `/app/runtime_config.json`. CLI и API аргументы имеют приоритет, а `runtime_config.json` работает как runtime-default и расширяемый фильтр.