# Каталог по базе знаний

> Режим catalog: разведка словарём, обход по дереву, поля по микроразметке, что и когда уходит языковой модели, единый тариф и отказы

Источник: https://docs.parreq.com/crawl-catalog/

`"mode": "catalog"` — обход, которому не нужны селекторы. Вы называете адрес
каталога, число страниц и поля, которые хотите видеть в таблице. Как устроен
сайт — где разделы, где списки, где карточки и откуда на них берётся цена, —
выясняет сам сервис, и делает это по **базе знаний**, а не по вашему правилу.

```json
{
  "mode": "catalog",
  "url": "https://shop.example/catalog/",
  "pages": 25,
  "fields": ["name", "price", "old_price", "currency", "in_stock", "sku", "brand", "url"],
  "cards": true
}
```

| параметр | обязателен | по умолчанию | значение |
|---|---|---|---|
| `mode` | да | — | `catalog` |
| `url` | да | — | адрес каталога или его раздела: обход не выйдет за пределы этого пути |
| `pages` | нет | 25 | сколько страниц каталога взять, 1…200 |
| `fields` | нет | все восемь | какие поля снимать; `name` и `price` включаются всегда |
| `cards` | нет | `true` | заходить ли в карточки товаров; `false` — только списки, поля с плиток |
| `fetch.wait_ms` | нет | 2500 | сколько ждать догрузки каждой страницы |

Селекторов, `follow`, `next`, `allow` и `deny` в этом режиме нет — и если они
переданы, они не нужны. Ответ всегда `202` с идентификатором: перед обходом
идёт разведка, и держать соединение ради неё незачем.

```json
{
  "crawl_metadata": {
    "id": "cr_8f31a0c94e2b",
    "status": "queued",
    "pages_planned": 25,
    "credits": 0,
    "credits_max": 75
  }
}
```

Дальше всё как у обычного задания: `GET /v1/crawl/{id}` — ход и срез
результата, `/pages` — страницы, `/tree` — каталог деревом, `/cancel` —
остановить. См. [Задания](https://docs.parreq.com/crawl-jobs) и [Дерево каталога](https://docs.parreq.com/crawl-tree).

## Поля

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

| поле | вид | что это |
|---|---|---|
| `name` | текст | название товара как оно написано на странице |
| `price` | число | цена без валюты и пробелов |
| `old_price` | число | перечёркнутая цена до скидки, если есть |
| `currency` | производное | валюта цены: `RUB`, `BYN`, `USD`, `KZT` |
| `in_stock` | да/нет | есть ли товар в наличии |
| `sku` | текст | артикул или код товара |
| `brand` | текст | производитель или бренд |
| `url` | производное | адрес страницы товара |

Неизвестные имена в `fields` молча отбрасываются. Если после этого список
пуст, берутся все восемь.

## Как проходит задание

**1. Разведка**

    Стартовая страница читается словарём: оглавление это, список товаров или
    карточка. Если оглавление — обход спускается по средней ссылке на этаж
    ниже и смотрит снова, до трёх этажей, пока не найдёт список с ценами.
    Отсюда берутся формы адресов разделов и карточек и число этажей до
    списка. Три-четыре захода, бесплатно, языковая модель не участвует.

**2. Обход по дереву**

    Обход идёт вширь и внутри стартового пути: раздел → его подразделы →
    список товаров со всеми страницами пагинации → карточки. Адреса с
    параметрами, кроме пагинации, не берутся: это фильтры и сортировки.
    Вид каждой страницы читает словарь; модель зовут только там, где он не
    уверен.

**3. Разбор**

    Поля снимаются с сохранённой разметки. Сначала — память сайта, потом
    словарь по микроразметке и подписям, и только потом, для оставшихся
    страниц, модель. Одна сверка цены на сайт.

**4. Сводка**

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

Ход виден в `note` задания: «смотрим стартовую страницу», «смотрим раздел:
…», «обход по ссылкам, глубина 3», «словарём разобрано 21 страниц без
обращения к модели», «сверяем цену словаря с моделью: попытка 1».

### Знакомый сайт

Разведка не повторяется, если сайт уже есть в базе знаний и отпечаток вёрстки
стартовой страницы не изменился: устройство берётся из карточки сайта, и
задание идёт сразу в обход. Разведать сайт заранее, без обхода, можно
бесплатной ручкой [`/v1/crawl/probe`](https://docs.parreq.com/crawl-probe).

### Если словарь не понял стартовую страницу

Тогда обход ищет карту сайта и идёт по ней — подробнее на странице
[Карта сайта](https://docs.parreq.com/crawl-sitemap). Нет и карты — устройство сайта разбирает
модель. Не вышло и у неё — задание завершается отказом `recon_failed`, и
денег это не стоит.

## Что читает словарь

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

**Что это за страница.** По числу плиток с ценой и форме ссылок с них,
по ссылкам оглавления, по хлебным крошкам и заголовку. Список отличается от
карточки количеством товаров с ценой на странице, оглавление — обилием
ссылок одной формы без цен, подборка и служебная страница — тем и другим
вместе с тем, что описано на странице [Что не каталог](https://docs.parreq.com/crawl-not-catalog).

**Откуда взять поля.** Порядок источников — от самого надёжного:

1. `ld+json` с типом `Product`: название, цена, валюта, артикул, бренд,
   наличие — всё сразу;
2. `itemprop` — `price`, `priceCurrency`, `name`, `sku`, `brand`,
   `availability`;
3. Open Graph — `product:price:amount` и `product:price:currency`;
4. подписи: блоки, чьи классы или текст содержат ценовые слова, слова
   «старая цена», признаки наличия.

Микроразметка есть у большинства магазинов, и на них модель не нужна вовсе.
В ответе каждой такой страницы `extract_note` начинается со слов «по словарю:
ld+json» или «по словарю: itemprop» — видно, откуда пришло значение.

## Когда зовут языковую модель

Модель — запасной судья, а не основной механизм. К ней обращаются в трёх
случаях, и только в них:

### Словарь не уверен, что это за страница

    Плиток с ценой мало, оглавление неявное, крошки не читаются. Модель
    получает текст страницы и говорит: раздел, список, карточка или не
    каталог. Если и она не ответила со второй попытки, страница помечается
    `failed` с кодом `unread`, не оплачивается и по её ссылкам обход не идёт:
    гадать по форме адреса здесь нельзя, именно так страницы-метки когда-то
    уезжали в отчёт карточками товара.

### Кандидатов на цену два и больше, а микроразметки нет

    Цена в рассрочку рядом с обычной, цена за штуку и за упаковку, «от … до».
    Словарь честно помечает страницу спорной (`ambiguous`), и её поля
    разбирает модель.

### Сверка цены — одна на сайт

    Первой карточке, разобранной словарём, модель показывают вместе с
    найденной ценой и спрашивают, та ли это цена. Согласилась — остальным
    страницам словаря верят без сверки. Не согласилась — спорная страница
    уходит модели целиком, а сверка повторяется на следующей. После **пяти
    несогласий подряд** задание завершается отказом `price_unconfirmed`:
    сайт подменяет цену скриптом или защитой, и гадать за деньги нельзя.
    Таблица при этом не отдаётся и не оплачивается.

Страницы, которые словарь не осилил вовсе, группируются по шаблону вёрстки:
модели показывают несколько образцов группы, получают правило вроде «цена
лежит в `meta[itemprop=price]`» и применяют его к остальным страницам группы
без обращений. Двести обращений превращаются в десяток.

### Что именно уходит модели

Не разметка. Модель получает **текст страницы** — тот же, что отдаёт
`parreq_fetch` с `text: true` и `max_chars`, — без шапки, меню и подвала, с
краткой сводкой микроразметки в начале и, если в тексте цены нет, с кусками
скриптов, где она лежит. По умолчанию это 6 000 знаков. Ровно эту выжимку
можно посмотреть самому: [`/v1/parse`](https://docs.parreq.com/parse) с `text: true` возвращает её
в поле `text`.

> **К сведению.**
>   Так дешевле и точнее одновременно. Страница магазина весит сотни тысяч
>   знаков разметки, из которых товар — несколько сотен; всё остальное только
>   мешает модели и стоит денег. А сводка микроразметки в начале выжимки даёт
>   ей тот же источник, которым пользуется словарь.

## База знаний

То, что обход узнал о сайте, не пропадает с окончанием задания. База знаний —
три слоя, и все три — данные, а не код.

**Словарь**
    Общий для всех сайтов: ценовые слова, шум, служебные блоки, подписи
    наличия, отпечатки CMS, пороги. Обновляется пересчётом по выборке и
    утверждается человеком.

**Знание о сайте**
    По хосту: устройство каталога, подтверждённые источники полей, формы
    адресов не каталога, спорные страницы, статистика прогонов и ручные
    правки.

**Память страниц**
    Отпечаток товарного блока каждой собранной страницы и её поля. Повторный
    обход платит только за то, что изменилось.

Карточка сайта проходит состояния `new` → `warmed` → `learned`. **Прогрев** —
разведка без обхода: сайт открыт, устройство и источники полей записаны,
статус `warmed`. Первый прогон дописывает то, что подтвердил на деле, — с
какого источника пришла цена и согласилась ли с ней модель, — и переводит
карточку в `learned`. Разведка повторяется, только когда меняется отпечаток
вёрстки сайта.

Правки, сделанные человеком в админке, сильнее выученного: словарь и обходы
не перезаписывают их, пока сайт не сменил вёрстку. Сайт, закрытый по решению
модерации, получает статус `blocked`, и обход по нему отклоняется до старта —
`403 site_blocked`.

> **Примечание.**
>   Память страниц общая для хоста, а не для ключа: если вёрстка карточки не
>   изменилась с прошлого прогона, поля берутся из памяти без единого
>   обращения — и такая строка **не оплачивается**. Сколько строк пришло из
>   памяти, видно в `note` задания: «из памяти 40».

## Что приходит

Каждая страница — в форме ответа Fetch. В `fetch_metadata` к обычным полям
добавляются пометки дерева:

```json
{
  "fetch_metadata": {
    "n": 7,
    "status": "ok",
    "final_url": "https://shop.example/catalog/kraska/emal-pf-115/",
    "title": "Эмаль ПФ-115 белая, 2.7 кг",
    "kind": "card",
    "depth": 2,
    "parent_url": "https://shop.example/catalog/kraska/",
    "prices_seen": 1,
    "pages_total": 0,
    "offers": 3,
    "credits": 3,
    "extract_note": "по словарю: ld+json"
  },
  "parsed": {
    "name": "Эмаль ПФ-115 белая, 2.7 кг",
    "price": 24.9,
    "old_price": null,
    "currency": "BYN",
    "in_stock": true,
    "sku": "ПФ-115-27",
    "brand": "Лакокраска",
    "url": "https://shop.example/catalog/kraska/emal-pf-115/"
  }
}
```

- **`kind`** (`string`):
  `sections` — оглавление каталога, `subsections` — оглавление внутри
  раздела, `listing` — список товаров, `card` — карточка, `not_catalog` —
  подборка, фильтр или служебная страница.

- **`parent_url`** (`string`):
  С какой страницы сюда пришли. По этому полю строки складываются в дерево.

- **`pages_total`** (`integer`):
  У списка — сколько всего страниц у его пагинации. По нему считается,
  сколько каталога осталось необойдённым.

- **`offers`** (`integer`):
  У карточки — сколько на ней торговых предложений: размеров, цветов, фасовок.

- **`parsed`** (`object | null`):
  У карточки — объект с заказанными полями. У списка при `cards: false` —
  `{"товары": [ … ]}`, по объекту на плитку. У раздела и не каталога —
  `null`.

Строки идут **по дереву**, а не в порядке сбора: раздел, под ним подразделы,
списки и товары, потом следующий раздел. В выгрузке CSV из кабинета для
этого есть колонка «уровень» — 1 раздел, 2 подраздел, 3 список, 4 товар, —
по которой в Excel можно свернуть всё, кроме товаров.

## Тариф

Тариф единый, и позиция в прайсе одна:

| позиция | кредитов | за что |
|---|---|---|
| `catalog_page` | 3 за страницу каталога | сбор, разбор по базе знаний и, где понадобилось, модель |

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

**Не оплачивается:**

- разведка и прогрев, в том числе [`/v1/crawl/probe`](https://docs.parreq.com/crawl-probe);
- сверка цены и сводка таблицы — они не считаются страницами;
- страницы, признанные не каталогом: метки, фильтры, сортировки, служебные;
- страницы, отданные из памяти сайта: вёрстка не менялась, модель не звалась;
- страницы, которые не взялись или не прочитались (`failed`);
- задание, завершившееся отказом `price_unconfirmed` или `recon_failed`.

Проверка баланса делается до старта по верхней оценке — `pages × 3`, это
`credits_max` в ответе. Списание — в конце, по числу вправду доставленных
страниц каталога. Прогон, запущенный из кабинета в ночное окно, дешевле на
20% — см. [Crawler в кабинете](https://docs.parreq.com/crawler-cabinet).

> **Примечание.**
>   Свой обход считается иначе — надбавкой сверх цены страницы. Сравнение и
>   арифметика с примерами — на странице [Цена обхода](https://docs.parreq.com/crawl-pricing).

## Отказы и неполные итоги

| статус | `code` | что случилось | что делать |
|---|---|---|---|
| `failed` | `recon_failed` | устройство сайта не разобрано: стартовая страница не открылась, словарь и модель не поняли каталог | проверить адрес: начинать лучше с оглавления каталога или раздела со списком; посмотреть [`/probe`](https://docs.parreq.com/crawl-probe) |
| `failed` | `price_unconfirmed` | модель пять раз не согласилась с ценой словаря | сайт подменяет цену скриптом или защитой; пришлите адрес — сайт посмотрит модерация |
| `partial` | `pages_limit` | каталог больше заказанного объёма | увеличить `pages` или запускать по разделам: `note` называет, сколько осталось |
| `partial` | `junk_only` | почти всё на сайте оказалось подборками и фильтрами | начать с адреса раздела, а не с витрины |
| `partial` | `time_limit` | задание шло дольше часа | уменьшить объём или разбить по разделам |
| до старта | `403 site_blocked` | сайт закрыт для обхода по решению модерации | причина в тексте ошибки |

Итог всегда называет числа: «собрано 25 страниц, осталось необойдённых 234 —
сайт больше оплаченного объёма». Молчаливое «готово» на трёх страницах из
двадцати пяти было бы худшим из возможных отчётов. Остальные коды — общие
для обхода, они на странице [Ошибки](https://docs.parreq.com/crawl-errors).

## Дальше

**[Разведка сайта](https://docs.parreq.com/crawl-probe)**
    Узнать устройство и объём до запуска — бесплатно.

**[Разбор одной страницы](https://docs.parreq.com/parse)**
    Проверить, что видит словарь на конкретной странице.

**[Что не каталог](https://docs.parreq.com/crawl-not-catalog)**
    Метки, фильтры и подборки: почему они отсеиваются и как это запоминается.

**[Crawler в кабинете](https://docs.parreq.com/crawler-cabinet)**
    То же самое по расписанию, с письмом о завершении и ночной скидкой.
