update readme
This commit is contained in:
402
README.md
402
README.md
@@ -1,344 +1,114 @@
|
|||||||
# IAAI Scraper
|
# Encar Scraper
|
||||||
|
|
||||||
Парсер аукционных автомобилей с [iaai.com](https://www.iaai.com). Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright, FastAPI, Celery и Docker.
|
Парсер автомобилей с [encar.com](https://www.encar.com) — крупнейшей корейской площадки продажи авто.
|
||||||
|
|
||||||
## Как устроен сайт и его защита
|
## Как это работает
|
||||||
|
|
||||||
У IAAI стоит **Imperva Incapsula** — внешний WAF и anti-bot.
|
Скрапер работает через публичный JSON API Encar (без браузера, без Selenium):
|
||||||
|
|
||||||
- **Anti-bot** — headless Chromium без прокси часто режется.
|
1. **Листинг** — `api.encar.com/search/car/list/general` отдаёт список авто с пагинацией (до 1000 на страницу)
|
||||||
- **Динамическая подгрузка** — часть данных приходит через XHR (`/Search`, `/VehicleDetail`), часть остаётся в HTML.
|
2. **Шардирование** — API лимитирует выдачу ~10k результатов, поэтому весь каталог (~230k авто) разбивается на 36 шардов по типу (domestic/import) и диапазонам годов
|
||||||
- **Cookie consent** — при первом заходе показывают баннер.
|
3. **Фото** — batch endpoint `api.encar.com/v1/readside/vehicles` (до 20 ID за запрос) возвращает все фото с CDN `ci.encar.com`
|
||||||
|
4. **Переводы** — корейские названия брендов, моделей и цветов автоматически переводятся в русские/английские
|
||||||
|
5. **Sold-трекинг** — авто, пропавшие из листинга, помечаются как проданные
|
||||||
|
|
||||||
Поэтому в проекте используются Playwright, паузы между действиями, прокси и сохранение браузерного состояния.
|
Celery Beat запускает полную синхронизацию каждые 60 минут. Worker обходит все шарды, парсит данные и пишет батчами в PostgreSQL.
|
||||||
|
|
||||||
## Локальный запуск (без Docker)
|
## Особенности
|
||||||
|
|
||||||
### Требования
|
- **Чистые HTTP запросы** — без браузера, через публичный API Encar
|
||||||
|
- **~100 авто/сек** — полный sync ~230k авто за ~35 мин
|
||||||
|
- **PostgreSQL** для хранения данных (авто + фото)
|
||||||
|
- **Celery + Redis** для фоновых задач и периодической синхронизации
|
||||||
|
- **FastAPI** REST API для управления задачами и просмотра данных
|
||||||
|
|
||||||
- **Python 3.11+**
|
## Быстрый старт (Docker)
|
||||||
- **Git**
|
|
||||||
|
|
||||||
Docker **не нужен**. Данные хранятся в SQLite-файле `iaai_scraper.db` в корне проекта.
|
|
||||||
|
|
||||||
### Быстрый старт
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repo-url>
|
docker compose up -d --build
|
||||||
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).
|
Поднимутся сервисы:
|
||||||
|
- `encar-postgres` — PostgreSQL
|
||||||
Готовый `.env` уже в репозитории и настроен на SQLite ничего менять не нужно.
|
- `encar-redis` — Redis
|
||||||
|
- `encar-api` — FastAPI на порту 8000
|
||||||
### Запуск парсера
|
- `encar-worker` — Celery worker
|
||||||
|
- `encar-beat` — периодическая синхронизация (каждые 60 мин)
|
||||||
**Скрапинг одной машины:**
|
|
||||||
|
|
||||||
```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
|
## CLI
|
||||||
|
|
||||||
Для отладки без API и Celery:
|
```bash
|
||||||
|
# Инициализация БД
|
||||||
|
encar init-db
|
||||||
|
|
||||||
|
# Собрать листинг
|
||||||
|
encar collect-listing --limit 100
|
||||||
|
|
||||||
|
# Синхронизировать листинг в БД
|
||||||
|
encar sync-listing --limit 100 --car-type all --only-new true
|
||||||
|
|
||||||
|
# Синхронизировать одно авто
|
||||||
|
encar sync-vehicle "https://www.encar.com/dc/dc_cardetailview.do?carid=41421262"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Фильтры
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
iaai init-db
|
encar sync-listing --car-type import --manufacturer BMW --year-from 2020 --limit 50
|
||||||
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.
|
## API endpoints
|
||||||
|
|
||||||
## Структура
|
- `GET /health` — проверка доступности
|
||||||
|
- `GET /api/v1/cars` — список авто с пагинацией и фильтрами
|
||||||
|
- `GET /api/v1/cars/{car_id}` — детали авто
|
||||||
|
- `GET /api/v1/cars/by-origin/{origin_id}` — поиск по origin_id
|
||||||
|
- `POST /api/v1/tasks/sync-vehicle` — синхронизация одного авто
|
||||||
|
- `POST /api/v1/tasks/sync-listing` — синхронизация листинга
|
||||||
|
- `GET /api/v1/tasks/{task_id}` — статус задачи
|
||||||
|
- `GET /api/v1/sync-runs` — история синхронизаций
|
||||||
|
|
||||||
```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
|
```bash
|
||||||
alembic upgrade head
|
# Запуск синхронизации
|
||||||
alembic revision --autogenerate -m "add_column_x"
|
curl -X POST http://localhost:8000/api/v1/tasks/sync-listing \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"car_type": "all", "limit": 100, "only_new": true}'
|
||||||
|
|
||||||
|
# Статус задачи
|
||||||
|
curl http://localhost:8000/api/v1/tasks/<task_id>
|
||||||
|
|
||||||
|
# Список авто
|
||||||
|
curl "http://localhost:8000/api/v1/cars?page=1&per_page=20"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Тесты
|
## Переменные окружения
|
||||||
|
|
||||||
```bash
|
- **БД:** `ENCAR_DATABASE_URL`
|
||||||
pytest -q
|
- **Redis:** `ENCAR_REDIS_URL`
|
||||||
|
- **Celery:** `CELERY_BROKER_URL`, `CELERY_RESULT_BACKEND`, `CELERY_TASK_TIME_LIMIT`
|
||||||
|
- **Beat:** `ENCAR_BEAT_INTERVAL_MINUTES` (по умолчанию 60), `ENCAR_BEAT_LIMIT` (0 = без лимита)
|
||||||
|
|
||||||
|
## Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
encar_scraper/
|
||||||
|
├── encar.py — скрапер Encar (HTTP API), маппер, переводы
|
||||||
|
├── cli.py — CLI интерфейс
|
||||||
|
├── api/
|
||||||
|
│ ├── app.py — FastAPI приложение
|
||||||
|
│ └── routes/ — health, cars, tasks
|
||||||
|
├── core/
|
||||||
|
│ ├── config.py — настройки из env vars
|
||||||
|
│ ├── utils.py — утилиты (save_to_json, deep_find_key)
|
||||||
|
│ └── logs.py — логирование с trace_id
|
||||||
|
├── storage/
|
||||||
|
│ ├── db.py — PersistenceService (upsert, session_scope)
|
||||||
|
│ ├── models.py — SQLAlchemy модели (Car, Image, SyncRun)
|
||||||
|
│ └── schemas.py — Pydantic схемы (CarRecord, ImageRecord)
|
||||||
|
└── worker/
|
||||||
|
├── celery_app.py — Celery app + beat schedule
|
||||||
|
└── tasks.py — encar_sync_listing_task, encar_sync_vehicle_task
|
||||||
```
|
```
|
||||||
|
|
||||||
Тесты работают на 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 и расширяемый фильтр.
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user