rewrite readme
This commit is contained in:
283
README.md
283
README.md
@@ -1,189 +1,136 @@
|
||||
# IAAI Scraper
|
||||
|
||||
Скрапер листинга автомобилей с сайта IAAI.
|
||||
Собирает данные карточек через Playwright, парсит HTML и перехваченные JSON ответы,
|
||||
нормализует и сохраняет в PostgreSQL или SQLite через SQLAlchemy.
|
||||
Парсер аукционных автомобилей с [iaai.com](https://www.iaai.com). Ходит по листингу, собирает карточки машин, вытаскивает данные из DOM и перехваченных XHR-ответов, складывает всё в PostgreSQL. Работает через Playwright (headless Chromium), крутится в Docker.
|
||||
|
||||
По умолчанию работает последовательно одна машина за раз, с паузами между запросами.
|
||||
|
||||
JSON-результаты CLI по умолчанию сохраняются в `artifacts/json/`, чтобы не засорять корень проекта.
|
||||
## Как устроен сайт и его защита
|
||||
|
||||
## Что делает
|
||||
IAAI не использует Cloudflare или Akamai, но у них своя защита:
|
||||
|
||||
1. Открывает страницу листинга `Vehiclelisting/Cars`, собирает ссылки на карточки.
|
||||
2. Переходит на каждую карточку, перехватывает XHR/fetch JSON-ответы.
|
||||
3. Парсит DOM-текст, `<title>`, встроенные `<script>` с JSON, сетевые payload'ы.
|
||||
4. Маппит всё в единую структуру `CarRecord` (pydantic) с нормализацией полей.
|
||||
5. Делает upsert в БД по `origin_id`, сравнивая `content_hash`, чтобы пропускать неизменившиеся записи.
|
||||
6. Защищён от дублей: `origin_id` уникален, одинаковые записи пропускаются, а повторные картинки заменяются безопасно.
|
||||
- **Детект автоматизации** — проверяют `navigator.webdriver`, `chrome.runtime`, WebGL-рендерер, `hardwareConcurrency` и прочие browser fingerprint параметры. Если видят Playwright/Puppeteer — блокируют.
|
||||
- **Rate limiting** — после нескольких быстрых запросов подряд начинают отдавать пустые страницы или редиректить. Нет явного 429, просто перестают отдавать данные.
|
||||
- **Geo-блокировка** — часть контента доступна только с US/CA IP. С европейских адресов листинг может быть пустым.
|
||||
- **Динамическая подгрузка** — карточки машин подгружаются через XHR (`/Search`, `/VehicleDetail`), часть данных приходит в JSON, часть рендерится на сервере. Нельзя просто дёрнуть HTML нужен полноценный браузер с JS.
|
||||
- **Cookie consent** — при первом заходе показывают баннер, без принятия кук часть функционала не работает.
|
||||
|
||||
## Структура проекта
|
||||
|
||||
## Запуск
|
||||
|
||||
```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/
|
||||
├── browser/
|
||||
│ ├── factory.py # запуск Chrome/Chromium с desktop-фингерпринтом
|
||||
│ ├── network.py # перехват XHR/fetch, фильтрация и категоризация JSON
|
||||
│ └── pace.py # паузы между действиями
|
||||
├── core/
|
||||
│ ├── config.py # настройки из .env
|
||||
│ ├── logs.py # setup logging
|
||||
│ ├── retry.py # retry-декоратор
|
||||
│ └── utils.py # regex, deep_find_key, save_to_json
|
||||
├── parsing/
|
||||
│ ├── parser.py # DOM + JSON парсинг
|
||||
│ └── mapper.py # нормализация в CarRecord
|
||||
├── storage/
|
||||
│ ├── models.py # ORM: cars, images, sync_runs
|
||||
│ ├── schemas.py # pydantic-схемы
|
||||
│ ├── db.py # upsert с content_hash
|
||||
│ ├── listing.py # сбор ссылок из листинга
|
||||
│ └── enums.py # enum-значения для БД
|
||||
├── scraper.py # главный модуль
|
||||
└── cli.py # CLI (argparse)
|
||||
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: 3 канала (DOM + XHR JSON + embedded JSON)
|
||||
mapper.py — CarMapper: нормализация → CarRecord, content hash
|
||||
|
||||
storage/
|
||||
models.py — SQLAlchemy: Car (30+ полей), Image, SyncRun, ScrapeTask
|
||||
schemas.py — Pydantic: CarRecord, ImageRecord, CarRead
|
||||
enums.py — допустимые значения (drive, gearbox, body_type, ...)
|
||||
db.py — PersistenceService: upsert (insert/update/skip), 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
|
||||
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
|
||||
pip install -r requirements.txt
|
||||
python -m playwright install chromium
|
||||
```
|
||||
|
||||
## Настройка
|
||||
|
||||
Создайте `.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
|
||||
alembic upgrade head
|
||||
alembic revision --autogenerate -m "add_column_x"
|
||||
```
|
||||
|
||||
## Тесты
|
||||
|
||||
```bash
|
||||
pytest tests -q
|
||||
pytest -q
|
||||
```
|
||||
|
||||
## Docker
|
||||
|
||||
```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`)
|
||||
- при обновлении запись не дублируется, а обновляется
|
||||
- изображения пересобираются без накопления дублей
|
||||
|
||||
|
||||
|
||||
30 тестов, SQLite in-memory, без внешних зависимостей.
|
||||
|
||||
Reference in New Issue
Block a user