fix readme
This commit is contained in:
188
README.md
188
README.md
@@ -1,37 +1,49 @@
|
|||||||
# IAAI Scraper
|
# IAAI Scraper
|
||||||
|
|
||||||
Парсер аукционных автомобилей с [iaai.com](https://www.iaai.com). Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright (headless Chromium), крутится в Docker.
|
|
||||||
|
|
||||||
|
Парсер аукционных автомобилей с [iaai.com](https://www.iaai.com). Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright, FastAPI, Celery и Docker.
|
||||||
|
|
||||||
## Как устроен сайт и его защита
|
## Как устроен сайт и его защита
|
||||||
|
|
||||||
У IAAI стоит **Imperva Incapsula** — внешний WAF и anti-bot.
|
У IAAI стоит **Imperva Incapsula** — внешний WAF и anti-bot.
|
||||||
|
|
||||||
- **Anti-bot** — headless Chromium без прокси часто режется.
|
- **Anti-bot** — headless Chromium без прокси часто режется.
|
||||||
- **Динамическая подгрузка** — часть данных приходит через XHR (`/Search`, `/VehicleDetail`), часть есть в HTML.
|
- **Динамическая подгрузка** — часть данных приходит через XHR (`/Search`, `/VehicleDetail`), часть остаётся в HTML.
|
||||||
- **Cookie consent** — при первом заходе показывают баннер.
|
- **Cookie consent** — при первом заходе показывают баннер.
|
||||||
|
|
||||||
Поэтому в проекте используется Playwright, паузы между действиями и прокси.
|
Поэтому в проекте используются Playwright, паузы между действиями, прокси и сохранение браузерного состояния.
|
||||||
|
|
||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env # подправить под себя
|
cp .env.example .env
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Поднимутся 5 контейнеров: postgres, redis, api, worker, beat. API на `http://localhost:8000`.
|
Будут запущены сервисы `postgres`, `redis`, `migrate`, `api`, `worker`, `beat`. API доступен на `http://localhost:8000`.
|
||||||
|
|
||||||
|
В production Docker-образ дополнительно проверяет Python-синтаксис на этапе сборки (`python -m compileall -q iaai_scraper`), чтобы не выкатывать битый код.
|
||||||
|
|
||||||
|
`entrypoint.sh` поднимает SOCKS5→HTTP proxy bridge (если задан `SOCKS5_PROXY_HOST`) и запускает `Xvfb` только для `worker` и CLI scraping-команд.
|
||||||
|
|
||||||
По умолчанию `beat` запускает сбор листинга **раз в 1 час** и обрабатывает **до 26 машин за запуск** (`CELERY_BEAT_SYNC_INTERVAL_MINUTES=60`, `CELERY_BEAT_SYNC_LIMIT=26`).
|
По умолчанию `beat` запускает сбор листинга **раз в 1 час** и обрабатывает **до 26 машин за запуск** (`CELERY_BEAT_SYNC_INTERVAL_MINUTES=60`, `CELERY_BEAT_SYNC_LIMIT=26`).
|
||||||
|
|
||||||
Swagger-документация: `http://localhost:8000/docs`
|
Swagger-документация: `http://localhost:8000/docs`
|
||||||
|
|
||||||
|
Проверка состояния сервисов:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
- `api` имеет healthcheck на `GET /health`
|
||||||
|
- `worker` имеет healthcheck через `celery inspect ping`
|
||||||
|
- `beat` имеет healthcheck по файлу `celerybeat-schedule`
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
**Здоровье и статистика:**
|
**Здоровье и статистика:**
|
||||||
- `GET /health` — статус сервиса и подключения к БД
|
- `GET /health` — статус сервиса и подключения к БД
|
||||||
- `GET /api/v1/stats` — сколько машин/картинок в базе, топ брендов
|
- `GET /api/v1/stats` — сколько машин и картинок в базе, топ брендов
|
||||||
|
|
||||||
**Машины:**
|
**Машины:**
|
||||||
- `GET /api/v1/cars` — список с пагинацией
|
- `GET /api/v1/cars` — список с пагинацией
|
||||||
@@ -41,30 +53,30 @@ Swagger-документация: `http://localhost:8000/docs`
|
|||||||
**Задачи:**
|
**Задачи:**
|
||||||
- `POST /api/v1/tasks/sync-vehicle` — скрапнуть одну машину по URL
|
- `POST /api/v1/tasks/sync-vehicle` — скрапнуть одну машину по URL
|
||||||
- `POST /api/v1/tasks/sync-listing` — запустить полный обход листинга
|
- `POST /api/v1/tasks/sync-listing` — запустить полный обход листинга
|
||||||
- `GET /api/v1/tasks/{task_id}` — статус задачи
|
|
||||||
- `GET /api/v1/tasks` — все задачи
|
|
||||||
- `GET /api/v1/sync-runs` — история запусков
|
- `GET /api/v1/sync-runs` — история запусков
|
||||||
|
|
||||||
Пример — скрапнуть конкретную машину:
|
Пример — скрапнуть конкретную машину:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:8000/api/v1/tasks/sync-vehicle
|
curl -X POST http://localhost:8000/api/v1/tasks/sync-vehicle \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"vehicle_url": "https://www.iaai.com/VehicleDetail/45089484~US"}'
|
-d '{"vehicle_url": "https://www.iaai.com/VehicleDetail/45089484~US"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
Для отладки без API/Celery:
|
Для отладки без API и Celery:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python main.py init-db
|
iaai init-db
|
||||||
python main.py collect-listing --make Toyota
|
iaai collect-listing --make Toyota
|
||||||
python main.py sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US"
|
iaai sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US"
|
||||||
python main.py sync-listing --limit 10
|
iaai sync-listing --limit 10
|
||||||
```
|
```
|
||||||
|
|
||||||
## Структура
|
## Структура
|
||||||
|
|
||||||
```
|
```text
|
||||||
iaai_scraper/
|
iaai_scraper/
|
||||||
scraper.py — оркестратор: связывает browser → parser → storage
|
scraper.py — оркестратор: связывает browser → parser → storage
|
||||||
cli.py — CLI-команды (init-db, sync-vehicle, sync-listing, ...)
|
cli.py — CLI-команды (init-db, sync-vehicle, sync-listing, ...)
|
||||||
@@ -77,14 +89,14 @@ iaai_scraper/
|
|||||||
pace.py — рандомные паузы и движение мыши
|
pace.py — рандомные паузы и движение мыши
|
||||||
|
|
||||||
parsing/
|
parsing/
|
||||||
parser.py — VehicleParser: 3 канала (DOM + XHR JSON + embedded JSON)
|
parser.py — VehicleParser: DOM + XHR JSON + embedded JSON
|
||||||
mapper.py — CarMapper: нормализация → CarRecord, content hash
|
mapper.py — CarMapper: нормализация → CarRecord
|
||||||
|
|
||||||
storage/
|
storage/
|
||||||
models.py — SQLAlchemy: Car (30+ полей), Image, SyncRun, ScrapeTask
|
models.py — SQLAlchemy: Car, Image, SyncRun
|
||||||
schemas.py — Pydantic: CarRecord, ImageRecord, CarRead
|
schemas.py — Pydantic: CarRecord, ImageRecord, CarRead
|
||||||
enums.py — допустимые значения (drive, gearbox, body_type, ...)
|
enums.py — допустимые значения (drive, gearbox, body_type, ...)
|
||||||
db.py — PersistenceService: upsert (insert/update/skip), sync runs
|
db.py — PersistenceService: upsert, sync runs, статистика
|
||||||
|
|
||||||
api/
|
api/
|
||||||
app.py — FastAPI factory, lifespan, роутеры
|
app.py — FastAPI factory, lifespan, роутеры
|
||||||
@@ -92,38 +104,42 @@ iaai_scraper/
|
|||||||
routes/
|
routes/
|
||||||
health.py — GET /health
|
health.py — GET /health
|
||||||
cars.py — CRUD по машинам + GET /stats
|
cars.py — CRUD по машинам + GET /stats
|
||||||
tasks.py — управление Celery-задачами + sync-runs
|
tasks.py — запуск Celery-задач и история sync-runs
|
||||||
|
|
||||||
worker/
|
worker/
|
||||||
celery_app.py — конфиг Celery, beat-расписание
|
celery_app.py — конфиг Celery, beat-расписание
|
||||||
tasks.py — sync_vehicle_task, sync_listing_task
|
tasks.py — sync_vehicle_task, sync_listing_task
|
||||||
|
|
||||||
core/
|
core/
|
||||||
config.py — Settings (dataclass), все env-переменные
|
config.py — Settings (dataclass), env-переменные
|
||||||
logs.py — логирование с trace_id (ContextVar)
|
logs.py — логирование с trace_id (ContextVar)
|
||||||
retry.py — декоратор @retryable с exponential backoff
|
retry.py — декоратор @retryable с exponential backoff
|
||||||
|
runtime_config.py — чтение и нормализация runtime-конфига
|
||||||
utils.py — VIN_RE, deep_find_key, вспомогательные функции
|
utils.py — VIN_RE, deep_find_key, вспомогательные функции
|
||||||
|
|
||||||
alembic/ — миграции БД
|
alembic/ — миграции БД
|
||||||
tests/ — 30 тестов (SQLite in-memory)
|
tests/ — тесты
|
||||||
```
|
```
|
||||||
|
|
||||||
## Конфигурация
|
## Конфигурация
|
||||||
|
|
||||||
Всё через env-переменные (полный список в `.env.example`):
|
Всё через env-переменные (полный список в `.env.example`):
|
||||||
|
|
||||||
**БД:** `IAAI_DATABASE_URL`, `IAAI_DATABASE_POOL_SIZE`
|
- **БД:** `IAAI_DATABASE_URL`, `IAAI_DATABASE_POOL_SIZE`
|
||||||
**Redis:** `IAAI_REDIS_URL`
|
- **Redis:** `IAAI_REDIS_URL`
|
||||||
**Celery:** `CELERY_BROKER_URL`, `CELERY_BEAT_SYNC_INTERVAL_MINUTES`
|
- **Celery:** `CELERY_BROKER_URL`, `CELERY_BEAT_SYNC_INTERVAL_MINUTES`
|
||||||
**Скрапер:** `IAAI_HEADLESS`, `IAAI_SYNC_ONLY_NEW`, `IAAI_MAX_PAGES_PER_RUN`
|
- **Скрапер:** `IAAI_HEADLESS`, `IAAI_SYNC_ONLY_NEW`, `IAAI_MAX_PAGES_PER_RUN`
|
||||||
**Прокси:** `IAAI_PROXY_SERVER`, `IAAI_PROXY_USERNAME`, `IAAI_PROXY_PASSWORD`
|
- **Прокси:** `IAAI_PROXY_SERVER`, `IAAI_PROXY_USERNAME`, `IAAI_PROXY_PASSWORD`
|
||||||
**Паузы:** `IAAI_BETWEEN_VEHICLES_MIN_S`, `IAAI_AFTER_PAGE_CHANGE_MAX_S` и т.д.
|
- **Паузы:** `IAAI_BETWEEN_VEHICLES_MIN_S`, `IAAI_AFTER_PAGE_CHANGE_MAX_S` и т.д.
|
||||||
|
|
||||||
## Миграции
|
## Миграции
|
||||||
|
|
||||||
В Docker миграции накатываются автоматически при старте API-контейнера (`alembic upgrade head` в entrypoint).
|
В Docker миграции выполняются отдельным сервисом `migrate` (`alembic upgrade head`).
|
||||||
|
|
||||||
|
`api`, `worker`, `beat` стартуют после успешного завершения `migrate`.
|
||||||
|
|
||||||
Вручную:
|
Вручную:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
alembic upgrade head
|
alembic upgrade head
|
||||||
alembic revision --autogenerate -m "add_column_x"
|
alembic revision --autogenerate -m "add_column_x"
|
||||||
@@ -135,4 +151,110 @@ alembic revision --autogenerate -m "add_column_x"
|
|||||||
pytest -q
|
pytest -q
|
||||||
```
|
```
|
||||||
|
|
||||||
30 тестов, SQLite in-memory, без внешних зависимостей.
|
Тесты работают на SQLite in-memory, без внешних зависимостей.
|
||||||
|
|
||||||
|
## `pyproject.toml` + `uv.lock`
|
||||||
|
|
||||||
|
Проект переведён на современную схему зависимостей:
|
||||||
|
|
||||||
|
- `pyproject.toml` — декларация зависимостей и метаданных проекта
|
||||||
|
- `uv.lock` — зафиксированные версии для воспроизводимых установок
|
||||||
|
- `requirements.txt` удалён: Docker тоже собирается из `pyproject.toml` + `uv.lock`
|
||||||
|
|
||||||
|
Локально можно использовать:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
uv run pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime-фильтры
|
||||||
|
|
||||||
|
Файл `runtime_config.json` управляет runtime-поведением синка и фильтрацией автомобилей.
|
||||||
|
|
||||||
|
### Секция `sync`
|
||||||
|
|
||||||
|
Поддерживаются поля:
|
||||||
|
|
||||||
|
- `name`
|
||||||
|
- `ids_initial_size`
|
||||||
|
- `ids_next_size`
|
||||||
|
- `ids_max_pages`
|
||||||
|
- `condition_check_enabled`
|
||||||
|
- `lane`
|
||||||
|
- `only_new`
|
||||||
|
- `limit`
|
||||||
|
|
||||||
|
Часть полей сейчас служит заделом под более сложную стратегию синка. Рабочие поля уже используются: `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 и расширяемый фильтр.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user