improve batch sync add postgres upsert fix sync locking improve listing sync speed up scraper clean up project prepare for github update docker setup
12 KiB
IAAI Scraper
Парсер аукционных автомобилей с 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 в корне проекта.
Быстрый старт
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 ничего менять не нужно.
Запуск парсера
Скрапинг одной машины:
iaai sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US"
Сбор листинга + скрапинг (например Toyota, 10 штук):
iaai sync-listing --make Toyota --limit 10
Только ссылки с листинга (без скрапинга):
iaai collect-listing --make Toyota
С видимым браузером (для отладки):
iaai --headless false sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US"
Проверка, что записалось в базу (iaai_scraper.db):
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:
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
Применить миграции и запустить в трёх терминалах:
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 (все сервисы в контейнерах)
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
docker compose ps
apiимеет healthcheck наGET /healthworkerимеет healthcheck черезcelery inspect pingbeatимеет 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— скрапнуть одну машину по URLPOST /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.iaai.com/VehicleDetail/45089484~US"}'
CLI
Для отладки без API и Celery:
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.
Структура
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.
Вручную:
alembic upgrade head
alembic revision --autogenerate -m "add_column_x"
Тесты
pytest -q
Тесты работают на SQLite in-memory, без внешних зависимостей.
pyproject.toml + uv.lock
Проект переведён на современную схему зависимостей:
pyproject.toml— декларация зависимостей и метаданных проектаuv.lock— зафиксированные версии для воспроизводимых установок
Локально можно использовать:
uv sync
uv run pytest -q
Runtime-фильтры
Файл runtime_config.json управляет runtime-поведением sync и фильтрацией автомобилей.
Секция sync
Поддерживаются поля:
nameids_initial_sizeids_next_sizeids_max_pagescondition_check_enabledlaneonly_newlimit
Секция filters
Поддерживаются поля:
brandsmodelsyearsbody_typescolorsdrivesgearboxeslocationsexclude_brandsexclude_modelsexclude_yearsexclude_body_typesexclude_colorsexclude_drivesexclude_gearboxesexclude_locationsprice.min,price.maxmileage.min,mileage.maxflags.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": "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 и расширяемый фильтр.