Skip to content

Repository files navigation

ParsesUnix

CI Python 3.11–3.13 License: MIT

ParsesUnix — фреймворк для надёжного и экономного сбора данных из веба.

Он не «качает страницы». Он выбирает самый дешёвый маршрут, который доказанно отдаёт валидные данные: сначала структурированные источники, потом обычный HTTP, и только при доказанной блокировке — браузер. Каждый ответ проверяется на содержимое, каждый URL получает финальный вердикт, а рабочие маршруты запоминаются между прогонами.


Зачем он нужен

Обычный парсер ломается тихо. Он получает 200 OK, сохраняет пустую страницу, и вы узнаёте об этом через неделю по кривым отчётам. ParsesUnix построен вокруг трёх утверждений:

Утверждение Что это значит на практике
200 OK — это не успех Ответ проходит проверку содержимого. Челлендж-заглушка с кодом 200 — это SOFT_BLOCK, а не данные.
Ни один URL не теряется После прогона unaccounted == 0. Удалённая страница, упавший origin, стена авторизации — у каждого свой финальный вердикт, но никто не исчезает.
Деньги тратятся только по доказательству Платный провайдер включается лишь по вердикту BLOCKED/SOFT_BLOCK. Ошибки 404, 5xx и сломанные селекторы деньгами не лечатся.

Ключевые особенности

  • Cost-aware routing — лестница уровней от почти бесплатного к дорогому.
  • Adaptive route memory — система помнит, какая «дверь» реально открывается.
  • Site Profiles — декларативная настройка домена без правки кода.
  • Retry по причине429 ждёт Retry-After, 5xx откладывается, блок ведёт к другому маршруту.
  • Browser reconnaissance — браузер ищет скрытый JSON API и уходит, оставляя дешёвый маршрут.
  • Failure fingerprints — знакомая защита узнаётся даже на новом домене.
  • Quorum извлечения — критичное поле сверяется из независимых источников.
  • Atomic promote + LKG — полупрогона не существует; старые данные помечены как старые.
  • URL accounting — сводимый до нуля реестр.
  • Regression detection — «сайт изменился» видно до того, как испортятся данные.
  • Strict typing + CImypy --strict, ruff, более 1100 тестов.

Как это работает

                    URL
                     │
      ┌──────────────▼──────────────┐
      │  L0  JSON API / RSS / карта │   почти бесплатно
      │  L1  прямой HTTP-сеанс      │   дёшево
      │  L2  локальный браузер      │   дорого по CPU
      │  L3/L4  платный провайдер   │   деньги
      └──────────────┬──────────────┘
                     │  triage: вердикт после КАЖДОЙ попытки
                     ▼
              Валидация содержимого
                     │
                     ▼
      Извлечение (JSON-LD → app state → meta → CSS → эвристика)
                     │
                     ▼
         Staging → проверка целиком → атомарный promote
                     │                        │
                     ▼                        ▼
              Чистые данные          Отказ: сохраняется LKG

Это не тупая лестница сверху вниз. Роутер помнит статистику по каждому маршруту и может поставить вперёд тот, который на этом сайте реально работает — но только среди бесплатных уровней. Платный уровень роутеру недоступен: решение о деньгах принимает исключительно triage.


Быстрый старт

git clone https://github.com/Manacost-Labs/ParsesUnix.git
cd ParsesUnix
python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install -e .

Опционально — уровень L2 (рендеринг JavaScript и поиск скрытых API):

pip install -e '.[browser]'
playwright install chromium

Без браузера всё работает: маршруты L2 просто помечаются как пропущенные.


Первая разведка

ws-probe https://example.com/ --draft-profile draft.json

ws-probe безопасно смотрит на сайт и отвечает: SSR или CSR, есть ли RSS, sitemap, JSON-LD, скрытые API-подсказки, что говорит robots.txt и с какого уровня стоит начинать. Приватные и loopback-адреса заблокированы на каждом редиректе.


Site Profile

Профиль описывает домен декларативно — как проверять ответ, какими маршрутами ходить, что извлекать:

site: example.com
authorization:
  public_data_only: true

url_classes:
  article:
    match: "^https://example\\.com/articles/"
    validation:
      min_body_bytes: 500
      canary: "<article"            # без этой строки ответ не считается статьёй
      required_fields: [title, published_at]
    routes:
      primary:
        id: articles-api            # стабильная идентичность для статистики
        type: json_api
        level: L0
        url: "https://example.com/api/articles"
      alternatives:
        - {type: direct_http, level: L1}
        - {type: dynamic, level: L2}
    extractors:
      - {kind: json_ld, schema_type: Article}
      - {kind: css, fields: {title: "h1::text", published_at: "time::attr(datetime)"}}
    quorum_fields: [title]

Профиль проверяется до обращения к сети:

ws-profile validate profiles/example.yaml

Секреты, cookies и токены в профиле запрещены — валидатор их отклоняет.


Встраивание в существующий парсер

Если приложение уже само управляет очередями и публикацией, оно может использовать ParsesUnix только как строгий транспортный слой. В таком режиме ожидаемый формат и доказательство полезного содержимого задаются явно:

from web_scraper import ResponseContract, fetch_validated
from web_scraper.fetchers import UrllibTransport

contract = ResponseContract.json(
    required_json_paths=("decks.0.archetypeId", "decks.0.games"),
)
result = fetch_validated(
    UrllibTransport(),
    "https://data.example/decks.json",
    contract,
    headers={"Accept": "application/json"},
)

if result.transport_validated:
    candidate = result.response.body  # затем прикладной parser + publication gate
else:
    diagnostics = result.telemetry()  # без URL query, headers и body

Для SSR HTML вместо JSON path требуется канарейка, подтверждающая, что пришли именно данные страницы, а не общая оболочка:

contract = ResponseContract.html(
    canaries=("data-meta-table",),
    min_body_bytes=500,
)

Контракты fail-closed: JSON без обязательного path, HTML/Text без canary, несовпавший формат и усечённый ответ не могут получить OK. Клиентская оболочка остаётся CSR_REQUIRED; это разрешает бесплатный браузерный маршрут, но не платного провайдера.

transport_validated означает только «получен полный ответ нужного типа с заданным доказательством». Число строк, обязательные поля, свежесть патча, регрессия и атомарная публикация проверяются прикладным парсером отдельно. Это не позволяет резервным или старым данным выглядеть как свежий успех.

На этой границе проверены формы ответов, характерные для наших источников:

Семейство источника Контракт ParsesUnix Что остаётся приложению
HSReplay / Firestone API JSON + обязательные schema paths число сущностей, patch, completeness
HSGuru SSR HTML + source-specific canary deck-коды и семантика строк
CSR-страницы HTML + canary → CSR_REQUIRED discovery JSON API или L2 rendering

Детальная последовательность внедрения в Hearthstone pipeline описана в плане интеграции.


Первый прогон

run.json:

{
  "profile": "profiles/example.yaml",
  "state_dir": "state",
  "seed_urls": ["https://example.com/articles/first"],
  "deadline_seconds": 14400,
  "batch_size": 20,
  "allowed_providers": []
}
ws-run run.json --report state/last-report.json

В отчёте — покрытие, вердикты по каждому URL, здоровье маршрутов, свежесть данных и нулевой unaccounted.

Платные провайдеры включаются только явным массивом allowed_providers в этом же файле. Одних переменных окружения с ключами недостаточно. Порядок массива задаёт разрешённый оператором порядок провайдеров; для бесплатного запуска он остаётся пустым.


Уровни L0–L4

Уровень Что используется Цена Когда
L0 JSON API, RSS/Atom, sitemap, JSON-LD почти ноль всегда пробовать первым
L1 Прямой HTTP-сеанс с прогревом и куками дёшево обычный SSR-сайт
L2 Локальный браузер (Playwright) дорого по CPU/RAM нужен JS-рендеринг или доказан челлендж
L3 Платный провайдер деньги L2 устойчиво не проходит
L4 Residential / Web Unlocker больше денег исчерпано всё дешёвое, причина задокументирована

Важное наблюдение из реальной приёмки: L2 не «сильнее» L1. Headless-браузер легче распознаётся — на hsreplay.net обычный HTTP отдаёт страницу, а браузер получает Turnstile. Поэтому лестница начинается снизу, а альтернативные маршруты на том же уровне важнее подъёма.


Надёжность

Triage — единственный источник решений. Любой ответ, включая 200, получает вердикт до того, как принято решение о повторе:

Вердикт Что делает система Платить?
OK сохранить не нужно
DEAD_URL (404/410) карантин нет
ORIGIN_DOWN (5xx) отложенный повтор нет
RATE_LIMITED (429) ждать Retry-After, снизить темп нет
BLOCKED / SOFT_BLOCK другой маршрут → браузер да, в рамках бюджета
CSR_REQUIRED пустая оболочка — рендерить браузером нет
THIN_CONTENT 2xx короче порога — проблема качества нет
PARSE_FAIL чинить профиль, не сеть нет
ACCESS_DENIED / AUTH_REQUIRED остановиться, не обходить нет

URL accounting. Прогон завершается сведённым реестром: каждый входной URL находится ровно в одной корзине — обработан, перенесён на следующий прогон, в карантине или в мёртвой зоне. unaccounted != 0 означает дефект системы.

LKG. Если свежий прогон не удался, потребитель получает последние валидные данные — но явно помеченными: STALE_LKG, возраст и вердикт, из-за которого обновление не состоялось. Молча выдать старое за свежее нельзя.

Regression. ws-regress сравнивает сохранённый эталон с текущим ответом: потерянные поля, переехавшие JSON-пути (с подсказкой замены), переход SSR→CSR, дрейф источника извлечения. При критичной регрессии — ненулевой код возврата.


Adaptive routing

Простыми словами: система запоминает, какая дверь открывается.

Для каждой пары «класс страниц + маршрут» копится статистика: сколько было попыток, сколько закончилось валидированным успехом, какая задержка. Дальше маршруты переупорядочиваются по нижней границе доверительного интервала (Wilson), а не по сырой доле успехов — один удачный запрос из одного не обгонит двести из двухсот пяти.

Три правила не дают этому сломаться:

  • Отказ origin — нейтрален. Если сайт лежит, маршрут в этом не виноват: такие попытки не портят его оценку и не толкают лестницу вверх.
  • Гистерезис. Маршруты не меняются местами от шума.
  • Shadow probes. Изредка перепроверяется дешёвый маршрут, который история считает мёртвым — так система возвращается вниз, когда сайт ослабил защиту.

Failure fingerprints дополняют это: нормализованная «форма» отказа (вердикт, код, размер, маркеры челленджа, заголовки защиты) не зависит от домена. Если знакомая защита встречается на новом сайте, система сразу пробует маршрут, который уже помогал против неё.


Сложные сайты

«Сложный» — это когда данных нет в первом HTML-ответе: страница рендерится в браузере, стоит защита от ботов, или контент приходит отдельным XHR.

ParsesUnix не «пробивает» такие сайты. Он ищет наиболее устойчивый разрешённый способ получить публичные данные:

обычный HTTP
  ↓ если пришла пустая оболочка — вердикт CSR_REQUIRED
браузер (L2)
  ↓ перехват сети: какой JSON запрашивает сама страница
кандидат маршрута L0
  ↓ проверка и решение оператора
дальше — дёшево, без браузера

Ключевые детали:

  • пустая оболочка не считается успехом<div id="root"></div> с кодом 200 получает CSR_REQUIRED, который разблокирует браузер, но никогда не разрешает платного провайдера: рендеринг — наша работа;
  • канарейка не ищется внутри <script> — маркер, найденный только в JS-массиве, не доказывает, что страница отрендерилась;
  • браузер — разведчик: один Chromium на прогон с контекстом на домен (измерено 4.9× против запуска на каждую страницу), а найденный JSON-эндпоинт переводит домен на дешёвый уровень;
  • если сайт запрещает такой доступ — это фиксируется в отчёте приёмки, и механика обхода не строится.

Подробности — в docs/concepts/hard-sites.md, измерения — в docs/acceptance/hard-sites/.

HTML и JSON — оба первого класса

Раньше JSON обрабатывался как HTML, который случайно разбирается: тело декодировалось, из него строилось DOM-дерево, и в нём искался точечный путь. Это работало достаточно часто, чтобы скрыть, что форма работы неверна.

ContentKind решает, чем ответ является, а не чем он назвался:

HTML  JSON  TEXT  BINARY  UNKNOWN

Заголовок — первый сигнал, а не единственный. Ошибка здесь беззвучна: HTML-экстрактор на JSON не находит элементов и возвращает пустоту, JSON-парсер на HTML падает и падение перехватывается. В обоих случаях прогон завершается, поле отсутствует, и никто не знает почему.

application/json          → JSON
text/plain + {"a": 1}     → JSON      (частый случай, заголовок врёт)
application/json + <html> → HTML      (страница ошибки JSON-эндпоинта)
image/png                 → BINARY    (экстрактору не отдаётся вовсе)

Порядок проверок намеренный: сначала бинарные сигнатуры, чтобы не декодировать видео ради выяснения, что это видео; потом собственное начало тела, которое не может врать о себе; и только потом заголовок.

JSON-экстрактор

extractors:
  - kind: json
    fields:
      name: data.character.name
      score: data.character.score
      players: data.players[*].name

Поддерживается сознательно подмножество, а не JSONPath: вложенные ключи, индексы массивов, целые списки и один ограниченный wildcard. Фильтры и рекурсивный спуск — это способы записать в профиль выражение, стоимость и результат которого никто не предскажет.

Типы сохраняются: 93 остаётся int, false остаётся False, список остаётся списком. Приведение к строке при извлечении — это то, как числовое поле тремя слоями ниже начинают сравнивать как текст.

Для JSON не строится DOM. Это и есть практический смысл всего изменения, и на это есть тест, который падает, если parse_html был вызван.


Обнаружение внутренних API

Страница, требующая браузера, остаётся дорогой навсегда — если ничего не изменить. Изменение почти всегда одно и то же: страница взяла данные из эндпоинта, и этот эндпоинт дешевле, быстрее и стабильнее, чем разметка вокруг.

CSR-страница
  ↓
BrowserWorker
  ↓
наблюдение XHR / fetch / GraphQL
  ↓
кандидаты: схемная сигнатура, пагинация, операция
  ↓
валидация на нескольких страницах
  ↓
черновик L0 JSON-маршрута → человек решает

Три отказа

Обнаружение — не разрешение. Запрос с Authorization, куки или CSRF-токеном был авторизован сессией, которую нам дали для рендеринга, а не лицензией вызывать его напрямую → REJECTED_AUTH.

Найденный URL — всё ещё вектор SSRF. Отрендеренная страница может попросить браузер о чём угодно, включая metadata-сервис → REJECTED_PRIVATE.

Аналитика тоже отвечает JSON. Google Analytics, Segment, Sentry, пиксели → REJECTED_NOISE.

Отказ записывается, а не выбрасывается: на вопрос «почему не нашёл API?» ответ «нашёл и отказался, потому что нужна кука» полезнее молчания.

Кандидат — не маршрут

Ничего не пишется в профиль автоматически. Эндпоинт, ответивший один раз при одном рендере, — совпадение. VALIDATED требует, чтобы тот же эндпоинт отдал ту же схему на нескольких разных страницах, а результат — черновик, который читает оператор.

ws-probe https://example.com/ --discover-api --target-field score

Внутри прогона обнаружение включено по умолчанию (discover_api) и ничего не стоит: оно едет пассажиром на рендерах, которые и так происходят. Разница принципиальная — probe рендерит одну страницу и может дать только PROMISING, а прогон рендерит много, и порог, отделяющий совпадение от закономерности, достижим только там.

Найденные маршруты попадают в отчёт прогона готовым черновиком:

{
  "suggested_route": {"id": "stats-api", "type": "json_api", "level": "L0", "url": "..."},
  "extractor": {"kind": "json", "fields": {"title": "data.player.title"}},
  "evidence": {"observed_count": 8, "confidence": "HIGH", "pagination": {"strategy": "CURSOR"}}
}

Пути в черновике взяты оттуда, где поля реально видели, а не подставлены по шаблону — поэтому черновик можно проверить против эндпоинта.

Экономия заявляется только там, где посчитана: валидированный эндпоинт покрывает рендеры, которые этот прогон уже сделал. Прогноз на будущие прогоны не выдаётся — они ещё не состоялись.

Измерено вживую

На разрешённой цели wowmeta.com (robots: User-agent: *, запретов нет):

Классификация CSR-оболочка — 0 видимого текста, 2 скрипта
Найдено эндпоинтов 2 JSON
С искомым полем data.wowmeta.com/rankings/top-specs/…
Вердикт PROMISING, 0 validated

Ноль validated — правильный результат: отрендерена одна страница, а валидация требует нескольких. Система отказалась продвигать маршрут по одному наблюдению.

hsguru.com — SKIPPED BY POLICY. Его robots.txt содержит User-agent: ClaudeBot / Disallow: / и Content-Signal: ai-train=no. Секция * разрешает, но использовать нейтральный агент, чтобы обойти адресный запрет для AI, — это и есть обход.


Платный слой: три провайдера, один бюджет

Платить система может только по вердикту BLOCKED или SOFT_BLOCK. Всё остальное — мёртвый URL, лежащий origin, непрошедший парсинг — не оплачивается никогда. Это правило про деньги, а не про стиль: измерено, что 404 через scrape.do всё равно стоит кредит.

Провайдер Роль Стратегии Проверено
Scrape.do основной normal / render / super / super_render живыми вызовами
Firecrawl управляемый рендеринг basic / cached / auto / enhanced по документации
Bright Data резерв высокой надёжности unlocker / unlocker_render / browser живыми вызовами
ZenRows широкий диапазон цены за вызов basic / js / premium / js_premium / auto по документации
Zyte API чистое разделение статусов, обучение маршрутам http / browser / browser_capture по документации

Firecrawl и Bright Data проверены живыми вызовами — и проверка нашла в них пять настоящих дефектов, невидимых документации и тестам. ZenRows и Zyte написаны по сверенной документации (docs_verified_at в каждом файле), но ключей нет, поэтому они помечены NOT LIVE VERIFIED.

Выбирается не самый дешёвый прайс, а самый дешёвый результат

provider A:  1 кредит/запрос, валидны 50%  →  2 кредита за результат
provider B:  1.5 кредита/запрос, валидны 99%  →  ~1.52 за результат

По прайсу A дешевле на треть. На деле — дороже на треть. Роутер ранжирует по cost_per_valid_result, поэтому выбирает B.

Нижняя граница Уилсона и точечная оценка делают разную работу: граница решает, допустима ли стратегия вообще, точечная оценка — во сколько она обходится.

Определённость стоимости: EXACT / PROVISIONAL / UNKNOWN

Cost.free()             — платного вызова не было. Измеренный ноль.
Cost.of("5")            — провайдер назвал цену.            EXACT
Cost.provisional("1")   — не назвал, но есть документированный потолок. PROVISIONAL
Cost.unknown()          — безопасный потолок установить нельзя. UNKNOWN

PROVISIONAL — единственный уровень, при котором батч продолжает работу, ничего не зная о фактической цене. Поэтому он допустим только при опубликованном тарифе, и решение принимается в одном месте — providers/pricing.py.

Применение к трём вендорам несимметрично, и в этом суть:

Вендор Молчащий ответ Почему
Scrape.do не бывает стоимость приходит в каждом ответе
Firecrawl basic/cached PROVISIONAL 1 кредит/страница опубликован, множителей нет
Firecrawl auto/enhanced UNKNOWN документирован повтор на другом пуле, цена — нет
Bright Data UNKNOWN CPM + премиум-домены без опубликованного множителя

Provisional расчитывается по потолку, а не по прайсу: запись меньшей цифры позволила бы серии молчащих вызовов выйти за лимит, пройдя каждую проверку.

Сравнение в деньгах, а не в кредитах

Один кредит Scrape.do, один кредит Firecrawl и один запрос Bright Data — три разные вещи. Ранжирование «1 против 1» сравнивает ничто. Роутер сравнивает в USD через снимки тарифов, и неоценённая стратегия сортируется последней: «мы не знаем, сколько это стоит» не должно выигрывать ценовое сравнение.

Сколько это будет стоить

ws-run run.json --estimate-cost

Ни одного платного вызова. Роутер спрашивается ровно так же, как во время прогона, поэтому оценка не может разойтись с реальным решением:

unresolved URLs: 40
expected cost:   42.00
reserved (held): 120
budget remaining: 500 (fits)

Три числа, которые часто путают: expected — плановая цифра, reserved — то, что будет удержано (всегда больше), worst case — насколько плохо может быть. Влезает ли прогон в бюджет, решается по удержанию: именно на нём реестр откажет. Прогон, одобренный по expected, встаёт на 60% с бюджетом, съеденным холдами.

Прогон идёт фазами

A  free            все URL, L0–L2, платный вызов недостижим
B  free retry      только то, что чинится само
C  cheap paid      остаток, дешёвые стратегии
D  expensive paid  только то, что дешевле не берётся

Фаза B — где основная экономия. 10 000 URL на пятиминутном сбое origin дают 800 отказов; поштучная эскалация купила бы 800 платных вызовов за страницы, которые бесплатный повтор через двадцать минут отдаёт даром.

Состояние фаз переживает рестарт: процесс, умерший в фазе C, продолжает с фазы C. Перезапуск платных фаз оплатил бы те же URL дважды.

Возраст доказательств

Стратегия, безупречно работавшая год назад и с тех пор не проверявшаяся, — не то же самое, что работавшая вчера: у сайта был год на изменения. Наблюдения теряют половину веса за период полураспада (по умолчанию 30 дней). Масштабируются и успехи, и попытки, поэтому наблюдаемая доля не меняется, а уверенность падает — мы по-прежнему верим, что оно работало, просто менее уверены, что работает сейчас.

Предохранители восстанавливаются

CLOSED → OPEN → (остывание) → HALF_OPEN → один пробный вызов → CLOSED или OPEN

HALF_OPEN пропускает ровно один вызов: иначе истёкшее остывание выпустило бы весь ждущий батч по тарифам провайдера, и восстановление стало бы вторым инцидентом. AUTH таймером не лечится и ждёт человека; QUOTA возвращается по биллинговому окну. Состояние переживает рестарт — иначе новый процесс первым делом платит за то, чтобы заново узнать про неверный ключ.

Канарейка перед батчем

Перед большим платным прогоном 3–10 представительных URL проходят тот же путь, что и батч. BLOCK_PAID_PHASE останавливает трату. Нейтральные вердикты (мёртвый URL, лежащий origin) из подсчёта исключаются: остановить платную фазу из-за чужого сбоя значило бы добавить к нему свой.

Разбор инцидентов — docs/operations/provider-incidents.md.


Безопасность

  • SSRF-защита на каждом хопе: приватные, loopback, link-local, CGNAT и metadata-адреса заблокированы; адрес пиннится, чтобы исключить DNS-rebinding.
  • Заголовки не утекают: Authorization и Cookie вырезаются при редиректе на другой хост.
  • Никакого обхода авторизации и контроля доступа — это терминальные вердикты.
  • Редакция секретов: тело снапшота, query-строка и заголовки чистятся перед записью на диск.
  • Бюджет — обязательный шлюз для любого платного запроса.
  • Профили без секретов — валидатор отклоняет ключи, cookies и токены.

Качество кода

make check

Команда использует .venv, если окружение создано, и запускает тот же набор Ruff, mypy и stdlib unittest, что используется в CI.

Python 3.11–3.13. Рантайм — только стандартная библиотека; браузер, PyYAML и Scrapling остаются опциональными зависимостями. Тесты, требующие браузера, корректно пропускаются, если Playwright не установлен.


Архитектура

src/web_scraper/
├── contracts.py      единые типы: Verdict, Level, Route, Attempt, Result
├── triage.py         классификатор ответов (единственный источник вердиктов)
├── profiles/         модель и валидатор Site Profile
├── probe/            разведка: статическая + браузерная, SSRF-защита
├── fetchers/         шлюз L0–L2, транспорты, сессии, пейсинг, circuit breaker
├── routing/          статистика маршрутов и адаптивный роутер
├── fingerprints/     нормализованные сигнатуры отказов и их восстановление
├── queue/            SQLite-очередь: дедуп, checkpoint/resume, карантин
├── extract/          цепочка экстракторов, кворум, нормализация
├── publish/          staging → атомарный promote → LKG, статусы свежести
├── freshness/        условные запросы и адаптивные интервалы
├── observability/    метрики, учёт URL, отчёты, алерты
├── regression/       диффы «эталон vs сейчас»
├── diagnose/         группировка отказов и корректные средства
└── run/              раннер прогона и CLI

Deployment

Целевая среда — один сервер Debian с systemd-таймером. Юниты и инструкция — в deploy/:

git pull && pip install --user -e .
systemctl --user start web-scraper.service
journalctl --user -u web-scraper.service -n 100

Состояние (очередь, датасет, свежесть, статистика маршрутов) переживает деплой; прогон, прерванный обновлением, продолжается с очереди без дублей.


CLI

Команда Назначение
ws-probe разведка нового домена, черновик профиля
ws-probe --discover-api найти внутренние JSON/GraphQL-эндпоинты страницы
ws-profile валидация профиля (без сети)
ws-triage классификация сохранённого ответа
ws-run прогон по расписанию
ws-run --estimate-cost сколько будет стоить платная часть (без единого платного вызова)
ws-regress «сайт изменился?» — гейт для CI
ws-diagnose «почему прогон недобирает?»
ws-budget учёт платных запросов

Roadmap

Реально незавершённое:

  • источники Reddit и X — контракт и нормализованная модель не написаны;
  • живая проверка Firecrawl и Bright Data: адаптеры написаны по документации, ключей нет, поэтому разбор реального ответа не измерен;
  • подключение PhaseController к Runner: контроллер фаз готов и покрыт тестами, но Runner пока проходит очередь одним проходом;
  • бенчмарки под нагрузкой до решения о Rust-воркере.

Подробности — в docs/IMPLEMENTATION_PLAN.md.


Лицензия

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages