rewrite readme
This commit is contained in:
283
README.md
283
README.md
@@ -1,189 +1,136 @@
|
|||||||
# IAAI Scraper
|
# IAAI Scraper
|
||||||
|
|
||||||
Скрапер листинга автомобилей с сайта IAAI.
|
Парсер аукционных автомобилей с [iaai.com](https://www.iaai.com). Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright (headless Chromium), крутится в Docker.
|
||||||
Собирает данные карточек через Playwright, парсит HTML и перехваченные JSON ответы,
|
|
||||||
нормализует и сохраняет в PostgreSQL или SQLite через SQLAlchemy.
|
|
||||||
|
|
||||||
По умолчанию работает последовательно одна машина за раз, с паузами между запросами.
|
|
||||||
|
|
||||||
JSON-результаты CLI по умолчанию сохраняются в `artifacts/json/`, чтобы не засорять корень проекта.
|
## Как устроен сайт и его защита
|
||||||
|
|
||||||
## Что делает
|
IAAI не использует Cloudflare или Akamai, но у них своя защита:
|
||||||
|
|
||||||
1. Открывает страницу листинга `Vehiclelisting/Cars`, собирает ссылки на карточки.
|
- **Детект автоматизации** — проверяют `navigator.webdriver`, `chrome.runtime`, WebGL-рендерер, `hardwareConcurrency` и прочие browser fingerprint параметры. Если видят Playwright/Puppeteer — блокируют.
|
||||||
2. Переходит на каждую карточку, перехватывает XHR/fetch JSON-ответы.
|
- **Rate limiting** — после нескольких быстрых запросов подряд начинают отдавать пустые страницы или редиректить. Нет явного 429, просто перестают отдавать данные.
|
||||||
3. Парсит DOM-текст, `<title>`, встроенные `<script>` с JSON, сетевые payload'ы.
|
- **Geo-блокировка** — часть контента доступна только с US/CA IP. С европейских адресов листинг может быть пустым.
|
||||||
4. Маппит всё в единую структуру `CarRecord` (pydantic) с нормализацией полей.
|
- **Динамическая подгрузка** — карточки машин подгружаются через XHR (`/Search`, `/VehicleDetail`), часть данных приходит в JSON, часть рендерится на сервере. Нельзя просто дёрнуть HTML нужен полноценный браузер с JS.
|
||||||
5. Делает upsert в БД по `origin_id`, сравнивая `content_hash`, чтобы пропускать неизменившиеся записи.
|
- **Cookie consent** — при первом заходе показывают баннер, без принятия кук часть функционала не работает.
|
||||||
6. Защищён от дублей: `origin_id` уникален, одинаковые записи пропускаются, а повторные картинки заменяются безопасно.
|
|
||||||
|
|
||||||
## Структура проекта
|
|
||||||
|
## Запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # подправить под себя
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Поднимутся 5 контейнеров: postgres, redis, api, worker, beat. API на `http://localhost:8000`.
|
||||||
|
|
||||||
|
Swagger-документация: `http://localhost:8000/docs`
|
||||||
|
|
||||||
|
## 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/tasks/{task_id}` — статус задачи
|
||||||
|
- `GET /api/v1/tasks` — все задачи
|
||||||
|
- `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
|
||||||
|
python main.py init-db
|
||||||
|
python main.py collect-listing --make Toyota
|
||||||
|
python main.py sync-vehicle "https://www.iaai.com/VehicleDetail/45089484~US"
|
||||||
|
python main.py sync-listing --limit 10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
```
|
```
|
||||||
iaai_scraper/
|
iaai_scraper/
|
||||||
├── browser/
|
scraper.py — оркестратор: связывает browser → parser → storage
|
||||||
│ ├── factory.py # запуск Chrome/Chromium с desktop-фингерпринтом
|
cli.py — CLI-команды (init-db, sync-vehicle, sync-listing, ...)
|
||||||
│ ├── network.py # перехват XHR/fetch, фильтрация и категоризация JSON
|
proxy_bridge.py — HTTP→SOCKS5 мост (Chromium не умеет SOCKS5 с авторизацией)
|
||||||
│ └── pace.py # паузы между действиями
|
|
||||||
├── core/
|
browser/
|
||||||
│ ├── config.py # настройки из .env
|
factory.py — создание браузера, stealth-инъекции, fingerprint
|
||||||
│ ├── logs.py # setup logging
|
listing.py — сбор ссылок на машины с листинга, пагинация
|
||||||
│ ├── retry.py # retry-декоратор
|
network.py — перехват XHR/fetch ответов через Playwright events
|
||||||
│ └── utils.py # regex, deep_find_key, save_to_json
|
pace.py — рандомные паузы и движение мыши
|
||||||
├── parsing/
|
|
||||||
│ ├── parser.py # DOM + JSON парсинг
|
parsing/
|
||||||
│ └── mapper.py # нормализация в CarRecord
|
parser.py — VehicleParser: 3 канала (DOM + XHR JSON + embedded JSON)
|
||||||
├── storage/
|
mapper.py — CarMapper: нормализация → CarRecord, content hash
|
||||||
│ ├── models.py # ORM: cars, images, sync_runs
|
|
||||||
│ ├── schemas.py # pydantic-схемы
|
storage/
|
||||||
│ ├── db.py # upsert с content_hash
|
models.py — SQLAlchemy: Car (30+ полей), Image, SyncRun, ScrapeTask
|
||||||
│ ├── listing.py # сбор ссылок из листинга
|
schemas.py — Pydantic: CarRecord, ImageRecord, CarRead
|
||||||
│ └── enums.py # enum-значения для БД
|
enums.py — допустимые значения (drive, gearbox, body_type, ...)
|
||||||
├── scraper.py # главный модуль
|
db.py — PersistenceService: upsert (insert/update/skip), sync runs
|
||||||
└── cli.py # CLI (argparse)
|
|
||||||
|
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
|
||||||
|
utils.py — VIN_RE, deep_find_key, вспомогательные функции
|
||||||
|
|
||||||
|
alembic/ — миграции БД
|
||||||
|
tests/ — 30 тестов (SQLite in-memory)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Установка
|
## Конфигурация
|
||||||
|
|
||||||
|
Всё через 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 миграции накатываются автоматически при старте API-контейнера (`alembic upgrade head` в entrypoint).
|
||||||
|
|
||||||
|
Вручную:
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
alembic upgrade head
|
||||||
python -m playwright install chromium
|
alembic revision --autogenerate -m "add_column_x"
|
||||||
```
|
|
||||||
|
|
||||||
## Настройка
|
|
||||||
|
|
||||||
Создайте `.env` на основе `.env.example`:
|
|
||||||
|
|
||||||
```env
|
|
||||||
IAAI_HEADLESS=true
|
|
||||||
IAAI_DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/iaai_scraper
|
|
||||||
```
|
|
||||||
|
|
||||||
Все настройки (pacing, лимиты, gentle mode, retry/backoff, scheduler) задаются через переменные окружения в `.env.example`.
|
|
||||||
|
|
||||||
### БД для рабочего сайта
|
|
||||||
|
|
||||||
Для рабочего запуска нужно **PostgreSQL**.
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```env
|
|
||||||
IAAI_DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/iaai_scraper
|
|
||||||
```
|
|
||||||
|
|
||||||
SQLite подходит для локальной отладки, но не для production-сценария сайта.
|
|
||||||
|
|
||||||
### Scheduler по умолчанию
|
|
||||||
|
|
||||||
Режим :
|
|
||||||
|
|
||||||
- запуск раз в **1 час**
|
|
||||||
- лимит **30 машин за цикл**
|
|
||||||
- обрабатываются только **новые авто** (по умолчанию `IAAI_SYNC_ONLY_NEW=true`)
|
|
||||||
|
|
||||||
Это уже отражено в актуальных env-настройках.
|
|
||||||
|
|
||||||
### Прокси и anti-bot
|
|
||||||
|
|
||||||
IAAI использует anti-bot / fraud protection.
|
|
||||||
|
|
||||||
Что важно:
|
|
||||||
|
|
||||||
- предпочтительно использовать **USA residential** или **USA mobile** прокси
|
|
||||||
- для Playwright лучше использовать **HTTP/HTTPS proxy**
|
|
||||||
- Chromium **не поддерживает SOCKS5 с аутентификацией напрямую**
|
|
||||||
- поэтому для production желательно покупать прокси, который отдаёт именно HTTP/HTTPS доступ
|
|
||||||
|
|
||||||
Если используется встроенный bridge `iaai_scraper/proxy_bridge.py` (HTTP/HTTPS → SOCKS5),
|
|
||||||
в нём добавлены базовые меры стабильности:
|
|
||||||
|
|
||||||
- корректное чтение request body через `rfile`
|
|
||||||
- поддержка `Transfer-Encoding: chunked` для request body
|
|
||||||
- базовое логирование запросов и ошибок
|
|
||||||
- таймауты relay-соединений
|
|
||||||
- ограничение числа рабочих потоков (`PROXY_BRIDGE_MAX_WORKERS`)
|
|
||||||
- безопасный ответ `502 Bad Gateway` без утечки внутренних исключений
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
|
|
||||||
```env
|
|
||||||
IAAI_PROXY_SERVER=http://proxy.example.com:8080
|
|
||||||
IAAI_PROXY_USERNAME=username
|
|
||||||
IAAI_PROXY_PASSWORD=password
|
|
||||||
```
|
|
||||||
|
|
||||||
### Captcha / anti-bot detection
|
|
||||||
|
|
||||||
В парсере добавлены признаки для определения возможной captcha / anti-bot страницы:
|
|
||||||
|
|
||||||
- `possible_captcha`
|
|
||||||
- `possible_antibot`
|
|
||||||
- `dom_hints.has_captcha_text`
|
|
||||||
- `dom_hints.has_antibot_text`
|
|
||||||
|
|
||||||
Если сайт начнёт отдавать защитную страницу, это можно увидеть в результате scrape.
|
|
||||||
|
|
||||||
## Команды
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# создать таблицы
|
|
||||||
python main.py init-db
|
|
||||||
|
|
||||||
# собрать ссылки из листинга
|
|
||||||
python main.py collect-listing --make Toyota --model Camry --output artifacts/json/listing.json
|
|
||||||
|
|
||||||
# scrape одной карточки
|
|
||||||
python main.py scrape-vehicle "https://www.iaai.com/VehicleDetail/41180634~US" --output artifacts/json/result.json
|
|
||||||
|
|
||||||
# scrape + запись в БД
|
|
||||||
python main.py sync-vehicle "https://www.iaai.com/VehicleDetail/41180634~US" --lane iaai
|
|
||||||
|
|
||||||
# массовая синхронизация листинга
|
|
||||||
python main.py sync-listing --make Toyota --model Camry --lane iaai_cars --limit 30
|
|
||||||
|
|
||||||
# при необходимости можно принудительно отключить фильтр only-new
|
|
||||||
python main.py sync-listing --limit 30 --only-new false
|
|
||||||
|
|
||||||
# daemon-режим (цикл каждые N минут)
|
|
||||||
python main.py run-daemon --interval 60
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Тесты
|
## Тесты
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pytest tests -q
|
pytest -q
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker
|
30 тестов, SQLite in-memory, без внешних зависимостей.
|
||||||
|
|
||||||
```bash
|
|
||||||
# собрать образ
|
|
||||||
docker compose build
|
|
||||||
|
|
||||||
# запустить daemon (по умолчанию run-daemon)
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# посмотреть логи
|
|
||||||
docker compose logs -f
|
|
||||||
|
|
||||||
# одноразовая команда
|
|
||||||
docker compose run --rm iaai-scraper python main.py sync-listing --limit 5
|
|
||||||
|
|
||||||
# остановить (graceful shutdown)
|
|
||||||
docker compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
Контейнер автоматически перезапускается при крашах (`restart: unless-stopped`).
|
|
||||||
Для Docker прокси также задаются через `.env`.
|
|
||||||
|
|
||||||
## Защита от дублей
|
|
||||||
|
|
||||||
Система защищена от дублей на нескольких уровнях:
|
|
||||||
|
|
||||||
- `origin_id` уникален в БД
|
|
||||||
- при совпадении `content_hash` запись **пропускается** (`skipped`)
|
|
||||||
- при обновлении запись не дублируется, а обновляется
|
|
||||||
- изображения пересобираются без накопления дублей
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user