improve batch sync add postgres upsert fix sync locking improve listing sync speed up scraper clean up project prepare for github update docker setup
345 lines
12 KiB
Markdown
345 lines
12 KiB
Markdown
# IAAI Scraper
|
||
|
||
Парсер аукционных автомобилей с [iaai.com](https://www.iaai.com). Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright, FastAPI, Celery и Docker.
|
||
|
||
## Как устроен сайт и его защита
|
||
|
||
У IAAI стоит **Imperva Incapsula** — внешний WAF и anti-bot.
|
||
|
||
- **Anti-bot** — headless Chromium без прокси часто режется.
|
||
- **Динамическая подгрузка** — часть данных приходит через XHR (`/Search`, `/VehicleDetail`), часть остаётся в HTML.
|
||
- **Cookie consent** — при первом заходе показывают баннер.
|
||
|
||
Поэтому в проекте используются Playwright, паузы между действиями, прокси и сохранение браузерного состояния.
|
||
|
||
## Локальный запуск (без Docker)
|
||
|
||
### Требования
|
||
|
||
- **Python 3.11+**
|
||
- **Git**
|
||
|
||
Docker **не нужен**. Данные хранятся в SQLite-файле `iaai_scraper.db` в корне проекта.
|
||
|
||
### Быстрый старт
|
||
|
||
```bash
|
||
git clone <repo-url>
|
||
cd iaai_scraper_project
|
||
pip install -e .
|
||
playwright install firefox
|
||
iaai init-db
|
||
```
|
||
|
||
`iaai init-db` актуален для SQLite/локального CLI-режима. Для PostgreSQL используйте Alembic-миграции (`alembic upgrade head` или сервис `migrate` в Docker).
|
||
|
||
Готовый `.env` уже в репозитории и настроен на SQLite ничего менять не нужно.
|
||
|
||
### Запуск парсера
|
||
|
||
**Скрапинг одной машины:**
|
||
|
||
```bash
|
||
iaai sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US"
|
||
```
|
||
|
||
**Сбор листинга + скрапинг (например Toyota, 10 штук):**
|
||
|
||
```bash
|
||
iaai sync-listing --make Toyota --limit 10
|
||
```
|
||
|
||
**Только ссылки с листинга (без скрапинга):**
|
||
|
||
```bash
|
||
iaai collect-listing --make Toyota
|
||
```
|
||
|
||
**С видимым браузером (для отладки):**
|
||
|
||
```bash
|
||
iaai --headless false sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US"
|
||
```
|
||
|
||
**Проверка, что записалось в базу (`iaai_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]"
|
||
```
|
||
|
||
Результаты сохраняются в `artifacts/json/` и в БД `iaai_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
|
||
CELERY_BROKER_URL=redis://localhost:6379/0
|
||
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
||
```
|
||
|
||
Применить миграции и запустить в трёх терминалах:
|
||
|
||
```bash
|
||
alembic upgrade head
|
||
|
||
# Терминал 1 — API
|
||
uvicorn iaai_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
|
||
|
||
# Терминал 3 — Celery Beat (периодический запуск)
|
||
celery -A iaai_scraper.worker.celery_app beat --loglevel=info
|
||
```
|
||
|
||
API: `http://localhost:8000` · Swagger: `http://localhost:8000/docs`
|
||
|
||
---
|
||
|
||
## Запуск через Docker (все сервисы в контейнерах)
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
docker compose up -d
|
||
```
|
||
|
||
Будут запущены сервисы `postgres`, `redis`, `migrate`, `api`, `worker`, `beat`. API доступен на `http://localhost:8000`.
|
||
|
||
В Docker-образ дополнительно проверяется Python-синтаксис на этапе сборки (`python -m compileall -q iaai_scraper`), чтобы не выкатывать битый код.
|
||
|
||
`entrypoint.sh` поднимает SOCKS5→HTTP proxy bridge (если задан `SOCKS5_PROXY_HOST`) и запускает `Xvfb` только для `worker` и CLI scraping-команд.
|
||
|
||
По умолчанию `beat` запускает сбор листинга **раз в 1 час** (`CELERY_BEAT_SYNC_INTERVAL_MINUTES=60`).
|
||
|
||
Swagger-документация: `http://localhost:8000/docs`
|
||
|
||
```bash
|
||
docker compose ps
|
||
```
|
||
|
||
- `api` имеет healthcheck на `GET /health`
|
||
- `worker` имеет healthcheck через `celery inspect ping`
|
||
- `beat` имеет healthcheck по файлу `celerybeat-schedule`
|
||
|
||
## API
|
||
|
||
**Здоровье и статистика:**
|
||
- `GET /health` — статус сервиса и подключения к БД
|
||
- `GET /api/v1/stats` — сколько машин и картинок в базе, топ брендов
|
||
|
||
**Машины:**
|
||
- `GET /api/v1/cars` — список с пагинацией
|
||
- `GET /api/v1/cars/{id}` — карточка с картинками
|
||
- `GET /api/v1/cars/by-origin/{origin_id}` — поиск по IAAI stock number
|
||
|
||
**Задачи:**
|
||
- `POST /api/v1/tasks/sync-vehicle` — скрапнуть одну машину по URL
|
||
- `POST /api/v1/tasks/sync-listing` — запустить полный обход листинга
|
||
- `GET /api/v1/sync-runs` — история запусков
|
||
|
||
Скрапим конкретную машину:
|
||
|
||
```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"}'
|
||
```
|
||
|
||
## CLI
|
||
|
||
Для отладки без 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
|
||
```
|
||
|
||
`iaai init-db` используйте для SQLite/локальных тестов. В PostgreSQL-сценарии применяйте миграции Alembic.
|
||
|
||
## Структура
|
||
|
||
```text
|
||
iaai_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`):
|
||
|
||
- **БД:** `IAAI_DATABASE_URL`, `IAAI_DATABASE_POOL_SIZE`
|
||
- **Redis:** `IAAI_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` и т.д.
|
||
|
||
## Миграции
|
||
|
||
В Docker миграции выполняются отдельным сервисом `migrate` (`alembic upgrade head`).
|
||
|
||
`api`, `worker`, `beat` стартуют после успешного завершения `migrate`.
|
||
|
||
Вручную:
|
||
|
||
```bash
|
||
alembic upgrade head
|
||
alembic revision --autogenerate -m "add_column_x"
|
||
```
|
||
|
||
## Тесты
|
||
|
||
```bash
|
||
pytest -q
|
||
```
|
||
|
||
Тесты работают на SQLite in-memory, без внешних зависимостей.
|
||
|
||
## `pyproject.toml` + `uv.lock`
|
||
|
||
Проект переведён на современную схему зависимостей:
|
||
|
||
- `pyproject.toml` — декларация зависимостей и метаданных проекта
|
||
- `uv.lock` — зафиксированные версии для воспроизводимых установок
|
||
|
||
Локально можно использовать:
|
||
|
||
```bash
|
||
uv sync
|
||
uv run pytest -q
|
||
```
|
||
|
||
## Runtime-фильтры
|
||
|
||
Файл `runtime_config.json` управляет runtime-поведением sync и фильтрацией автомобилей.
|
||
|
||
### Секция `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": "iaai_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": []
|
||
}
|
||
}
|
||
```
|
||
|
||
Путь задаётся через `IAAI_RUNTIME_CONFIG_FILE`, по умолчанию — `/app/runtime_config.json`.
|
||
|
||
CLI и API аргументы имеют приоритет, а `runtime_config.json` работает как runtime-default и расширяемый фильтр.
|
||
|