Metadata-Version: 2.5
Name: oniscrape
Version: 0.2.1
Summary: High-performance async scraping engine powered by curl_cffi, selectolax, orjson, and Pydantic v2. Extracts Next.js SSR, RSC Flight streams, and Meta Relay states without headless browsers.
Author: FastScraper Team
License-Expression: MIT
License-File: LICENSE
Keywords: anti-bot,crawler,curl-cffi,ja3,nextjs,pydantic,react-server-components,rsc,scraper,selectolax,tls-fingerprint
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: curl-cffi>=0.7.4
Requires-Dist: jmespath>=1.0.1
Requires-Dist: orjson>=3.10.0
Requires-Dist: pydantic>=2.7.0
Requires-Dist: selectolax>=0.3.21
Requires-Dist: tenacity>=8.3.0
Provides-Extra: dev
Requires-Dist: pyright>=1.1.350; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Provides-Extra: redis
Requires-Dist: redis>=5.0.0; extra == 'redis'
Description-Content-Type: text/markdown

# oniscrape 🚀

[![PyPI version](https://img.shields.io/pypi/v/oniscrape.svg)](https://pypi.org/project/oniscrape/)
[![Python versions](https://img.shields.io/pypi/pyversions/oniscrape.svg)](https://pypi.org/project/oniscrape/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)

> **Высокопроизводительный асинхронный скрапинг-движок для Python на базе `curl_cffi`, `selectolax` (Lexbor), `orjson` и `Pydantic v2`.**

Специализированная библиотека для сверхбыстрого парсинга современных веб-приложений (Next.js, React Server Components, Threads/Instagram, Nuxt) и статических сайтов **без запуска тяжелых headless-браузеров**.

---

## ⚡ Почему не Playwright / Selenium?

| Параметр | Headless Браузер (Playwright / Puppeteer) | `oniscrape` (No-JS / State Engine) |
| :--- | :--- | :--- |
| **Потребление RAM** | ~300 - 600 МБ на вкладку | **~15 - 30 МБ на процесс** |
| **Время ответа** | 3.0 - 8.0 секунд (DOM + JS) | **0.1 - 0.4 секунды** (чистый сетевой I/O) |
| **Устойчивость к редизайнам** | Низкая (CSS-классы меняются при каждом билде) | **Высокая** (структура данных в State/API стабильна годами) |
| **Обход TLS-проверок** | Зависит от stealth-патчей Chromium | **Нативный JA3/JA4 / HTTP2 спуфинг через `curl_cffi`** |
| **Утечки IP при смене прокси** | Залипание сокетов в пуле браузера | **Гарантированная изоляция сокетов на уровне сессий** |

---

## 📦 Установка

### На любом компьютере / сервере в мире:

```bash
pip install oniscrape
```

или через **`uv`**:
```bash
uv add oniscrape
```

### С поддержкой Redis (для распределенного хранения сессий/куки):
```bash
pip install oniscrape[redis]
```

---

## ⚙️ Настройка и Конфигурация

Все настройки валидируются строго через **Pydantic v2**:

```python
from oniscrape import ScraperClient, ScraperConfig, ProxyConfig, RetryConfig

config = ScraperConfig(
    # TLS-фингерпринт по умолчанию
    default_impersonate="chrome124",
    
    # Пул TLS-фингерпринтов для авто-ротации при блокировках (403/429)
    impersonate_pool=["chrome124", "chrome120", "safari17_0", "edge122"],
    auto_rotate_impersonate_on_retry=True,
    
    # Сетевой таймаут в секундах
    timeout_seconds=20.0,
    
    # Настройки пула прокси
    proxy=ProxyConfig(
        urls=[
            "socks5://127.0.0.1:40000", # Пример: локальный Cloudflare WARP
            "socks5://127.0.0.1:40001",
            "http://user:pass@proxy.example.com:8080",
        ],
        strategy="least_failed",        # "round_robin" | "random" | "least_failed"
        cooldown_seconds=120.0,         # Время отстоя прокси при получении 403/429
        max_fails_before_cooldown=2,    # Число ошибок до отправки в кулдаун
    ),
    
    # Настройки умных ретраев
    retry=RetryConfig(
        max_attempts=3,
        min_backoff_seconds=0.5,
        max_backoff_seconds=5.0,
        retry_status_codes={403, 429, 500, 502, 503, 504},
    ),
)
```

---

## 🎯 Высокоуровневый метод `client.scrape()` и контейнер `ScrapeResult`

Вместо ручной склейки запросов и разбора HTML, `client.scrape()` выполняет полный пайплайн и возвращает чистый структурированный объект **`ScrapeResult`**:

```python
import asyncio
from pydantic import BaseModel, Field
from oniscrape import ScraperClient

class CryptoItem(BaseModel):
    id: str
    title: str = Field(alias="name")
    price: float = Field(alias="cost")

async def main():
    async with ScraperClient() as client:
        # Автоматический пайплайн: HTTP/2 GET -> авто-экстракция -> Pydantic валидация
        result = await client.scrape(
            url="https://example-store.com/catalog",
            model_cls=CryptoItem,
            query="props.pageProps.items[*].{id: item_id, name: name, cost: price}",
            strict=False,
        )
        
        # 1. Метаданные запроса
        print(f"Статус: {result.status_code}, Время: {result.response_time_ms} ms")
        print(f"Извлечено элементов: {result.items_count}")
        
        # 2. Чистый словарь без сырого HTML
        clean_dict = result.to_dict()
        
        # 3. Встроенное сохранение в JSON / NDJSON / Polars
        result.save_json("output_clean.json", indent=True)
        result.append_ndjson("stream_data.ndjson")

asyncio.run(main())
```

---

## 🚀 Пошаговые рецепты использования

### Рецепт 1: Парсинг Next.js SSR (`__NEXT_DATA__`) в Pydantic v2

```python
import asyncio
from pydantic import BaseModel, Field
from oniscrape import ScraperClient, extract_next_data, map_state_to_model

class CryptoMetric(BaseModel):
    num_cryptos: int = Field(alias="numCryptocurrencies")
    num_markets: int = Field(alias="numMarkets")
    active_exchanges: int = Field(alias="activeExchanges")

async def main():
    async with ScraperClient() as client:
        response = await client.get("https://coinmarketcap.com/")
        
        # 1. Мгновенно достаем JSON-дерево гидратации (без выполнения JS)
        state = extract_next_data(response.text)
        
        # 2. Ищем данные по JMESPath и валидируем через Pydantic v2
        metrics = map_state_to_model(
            state=state,
            query="props.dehydratedState.queries[?queryKey[0]=='global-metric'].state.data | [0]",
            model_cls=CryptoMetric,
            strict=False,
        )
        print(f"Всего криптовалют в мире: {metrics.num_cryptos}")

asyncio.run(main())
```

---

### Рецепт 2: Парсинг React Server Components (RSC Flight stream) в Next.js 14 / 15 / 19

Next.js App Router не использует `__NEXT_DATA__`, а отдает потоковые чанки `self.__next_f.push` по протоколу React Flight (`id:tag:payload`). `oniscrape` построчно декодирует этот протокол и предоставляет хелпер поиска `find_rsc_payload`:

```python
import asyncio
from oniscrape import ScraperClient, extract_rsc_flight, find_rsc_payload

async def main():
    async with ScraperClient() as client:
        response = await client.get("https://nextjs.org/")
        
        # Построчно декодирует все слоты wire-протокола
        flight_tree = extract_rsc_flight(response.text)
        print(f"Декодировано слотов RSC: {len(flight_tree)}")
        
        # Рекурсивный поиск полезных блоков данных по ключу
        products = find_rsc_payload(flight_tree, target_key="products")
        print(f"Найденные продукты: {products}")

asyncio.run(main())
```

---

### Рецепт 3: Smart Relay Unwrapper для Threads / Instagram

`extract_relay_cache` автоматически разворачивает глубоко вложенные структуры `RelayPrefetchedStreamCache` и `__bbox` в чистый словарь по именам запросов:

```python
import asyncio
from oniscrape import ScraperClient, extract_relay_cache, extract_meta_tokens

async def main():
    async with ScraperClient() as client:
        response = await client.get("https://www.threads.net/@zuck")
        
        # 1. Извлечение токенов для прямых POST-запросов к /api/graphql
        tokens = extract_meta_tokens(response.text)
        print("LSD Token:", tokens["lsd"])
        
        # 2. Умный распаковщик Relay Cache -> плоский словарь {QueryName: data}
        cache = extract_relay_cache(response.text)
        for query_name, data in cache.items():
            print(f" -> Запрос [{query_name}]: {len(data)} полей")

asyncio.run(main())
```

---

### Рецепт 4: Быстрый C-уровень DOM-парсинга через Selectolax Lexbor

Для сайтов с обычным HTML используется движок Lexbor на чистом Си (в 20 раз быстрее BeautifulSoup и в 10 раз меньше памяти):

```python
import asyncio
from oniscrape import ScraperClient, parse_html, css_all_text, css_all_attr

async def main():
    async with ScraperClient() as client:
        response = await client.get("https://news.ycombinator.com/")
        
        tree = parse_html(response.text)
        titles = css_all_text(tree, ".titleline > a")
        links = css_all_attr(tree, ".titleline > a", "href")
        
        for title, link in zip(titles[:5], links[:5], strict=False):
            print(f"{title} -> {link}")

asyncio.run(main())
```

---

## 💾 Сохранение данных (Data Persistence 2026)

### 1. PostgreSQL + SQLAlchemy 2.0 Async (UPSERT)
Атомарное обновление цен и товаров без дубликатов через `on_conflict_do_update`:

```python
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.ext.asyncio import AsyncSession

async def upsert_items(session: AsyncSession, model_cls, items: list[dict]):
    if not items:
        return
    stmt = insert(model_cls).values(items)
    stmt = stmt.on_conflict_do_update(
        index_elements=[model_cls.id],
        set_={
            "price": stmt.excluded.price,
            "updated_at": func.now(),
        }
    )
    await session.execute(stmt)
    await session.commit()
```

### 2. Polars + Apache Parquet (Для аналитики и больших объемов)
Сжатие до 80% через ZSTD, сохранение типов и мгновенное чтение:

```python
df = result.to_polars()
df.write_parquet("scraped_data.parquet", compression="zstd")
```

### 3. Потоковый NDJSON (JSON Lines) через `orjson`
Не держит миллионы записей в памяти:

```python
result.append_ndjson("stream_output.ndjson")
```

---

## 🏛️ Архитектура пакета

```text
src/oniscrape/
├── __init__.py           # Единый публичный API
├── client.py             # ScraperClient с изоляцией сессий, client.scrape() и ретраями
├── result.py             # ScrapeResult контейнер (.to_dict(), .save_json(), .append_ndjson())
├── config.py             # Pydantic v2 конфигурация (ScraperConfig, ProxyConfig, RetryConfig)
├── py.typed              # Маркер типизации PEP 561
├── extractors/           # Движки извлечения состояний гидратации
│   ├── nextjs.py         # __NEXT_DATA__, extract_rsc_flight, find_rsc_payload
│   ├── meta.py           # extract_relay_cache, extract_meta_tokens, extract_meta_relay
│   ├── nuxt.py           # Nuxt 2 / 3 (__NUXT_DATA__)
│   └── generic.py        # Schema.org JSON-LD и <script type="application/json">
├── dom/                  # C-уровень DOM-парсинга
│   └── parser.py         # Обертки над Selectolax Lexbor
├── proxy/                # Ротация и мониторинг прокси
│   ├── manager.py        # ProxyManager (Round-Robin, Random, Least-Failed)
│   └── health.py         # Отслеживание сбоев и таймеры кулдауна
├── cookies/              # Управление сессиями и куки
│   ├── memory.py         # InMemoryCookieStorage
│   └── redis_storage.py  # RedisCookieStorage
└── pipeline/             # Трансформация данных
    └── schema_mapper.py  # JMESPath -> Pydantic v2 TypeAdapter
```

---

## 🏎️ Производительность Rust и C-расширений

Все ресурсоемкие операции вынесены за пределы интерпретатора Python:
- **JSON парсинг:** `orjson` на **Rust** (в 6-10 раз быстрее встроенного `json`).
- **DOM парсинг:** `selectolax` на движке Lexbor (**C**, в 20 раз быстрее BeautifulSoup).
- **Валидация данных:** `pydantic-core` на **Rust** (в 5-20 раз быстрее Pydantic v1).
- **Сетевой уровень:** `curl_cffi` на **C/libcurl** с аппаратной поддержкой TLS/HTTP2.

---

## 🔒 Безопасность сокетов и прокси

При смене прокси в стандартных клиентах соединения могут удерживаться в HTTP Keep-Alive пуле, из-за чего часть запросов уходит со старого IP.

`oniscrape` решает это на уровне архитектуры:
1. Метод `create_session` гарантированно закрывает `libcurl` сессию при смене прокси.
2. Автоматический трекер ошибок при получении `403` или `429` временно блокирует скомпрометированный IP, переключает TLS-фингерпринт из `impersonate_pool` и повторяет запрос через следующий живой прокси.

---

## 🧪 Тестирование

```bash
# Запуск unit-тестов
uv run --with pytest --with pytest-asyncio pytest -v

# Проверка типизации (pyright)
uv run --with pyright --with pytest pyright src tests

# Линтер (ruff)
uv run --with ruff ruff check src tests
```

---

## 📄 Лицензия

MIT License (c) 2026 Oniscrape Authors.
