diff --git a/README.md b/README.md index 1d418ad..9b270aa 100644 --- a/README.md +++ b/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-текст, ``, встроенные `<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, без внешних зависимостей.