# 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 cd iaai_scraper_project pip install -e . playwright install chromium iaai init-db ``` Готовый `.env` уже в репозитории и настроен на SQLite ничего менять не нужно. ### Команды для локального запуска и проверки ```bash pip install -e . playwright install chromium iaai init-db iaai sync-listing --limit 10 # С видимым браузером iaai --headless false sync-listing --limit 5 # Для просмотра того что записалось 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]" ``` ### Запуск парсера **Скрапинг одной машины:** ```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" ``` Результаты сохраняются в `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 ``` ## Структура ```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` Так же используются: `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 и расширяемый фильтр.