# Инструкция для ИИ-агента или программиста: YML-фид для импорта объявлений
Этот файл — техническое задание. Его передал тебе человек, который подключает свой каталог к импорту на сайте объявлений (дальше — «площадка»). Площадка примерно раз в час скачивает YML-файл (фид) по постоянной ссылке и по нему создаёт, обновляет и снимает объявления этого человека.
Твоя задача — в проекте человека сделать выгрузку такого файла, опубликовать её по постоянному адресу и проверить по правилам ниже. Правила описывают фактическую работу площадки. Всё, что здесь не названо, площадка не использует.
Из фида в объявление попадают название, описание, цена, фотографии и ссылка на страницу предложения. Раздел каталога площадки, город и контакты задаёт человек в личном кабинете площадки: в фиде их указывать не нужно.
Действия в личном кабинете (сохранить ссылку на фид, нажать «Проверить фид», выбрать разделы, контакты, отправить заявку) выполняет человек. Ты в кабинет не входишь.
## 1. Что должно получиться
1. Генератор фида в проекте человека: команда, скрипт или обработчик, который выгружает весь актуальный каталог в один XML-файл по правилам этого документа.
2. Постоянный адрес фида, который открывается из интернета без пароля, например `https://shop.example/export/adverts.xml`.
3. Регулярное обновление файла: по расписанию (например, раз в час) или при изменении каталога.
4. Файл подтверждения сайта в корне сайта (раздел 9).
5. Отчёт человеку (раздел 13).
## 2. Порядок работы
1. Изучи проект: где хранятся позиции (товары, объекты недвижимости, услуги), категории, цены, валюта, фотографии, адреса страниц позиций; какой у сайта публичный адрес; как в проекте запускаются задачи по расписанию.
2. Задай человеку вопросы из раздела 3 — только те, на которые проект не отвечает.
3. Напиши генератор. Данные бери из базы или API проекта, а не из HTML-страниц сайта. XML собирай библиотекой языка проекта, а не склейкой строк: так экранирование будет правильным.
4. Опубликуй файл по постоянному адресу и настрой обновление.
5. Проверь результат по чек-листу из раздела 12 и исправь всё, что не проходит.
6. Отчитайся человеку (раздел 13).
## 3. Что спросить у человека
- Какие позиции выгружать: все, только в наличии, только отдельные категории. По умолчанию площадка принимает до 1000 предложений (раздел 4). Если позиций больше, нужно либо выбрать часть, либо попросить площадку увеличить лимит через форму обратной связи.
- Какой город человек выберет в кабинете для объявлений. Название этого города запрещено в названиях предложений (раздел 8).
- Какую цену выгружать (обычную, со скидкой) и в какой валюте.
- Имя и содержимое файла подтверждения (раздел 9). Их показывает кабинет в блоке «Подтверждение сайта» после того, как человек сохранит ссылку на фид. Если ссылки ещё нет — сначала сделай фид, потом попроси человека сохранить ссылку и передать тебе файл.
## 4. Как площадка скачивает файл
- Адрес начинается с `http://` или `https://`, работает на стандартном порту (80 для HTTP, 443 для HTTPS; адреса вида `:8080` не подходят), не содержит логина и пароля, длина — до 255 символов. Секретный параметр в адресе (`?key=...`) допустим.
- Сервер доступен из интернета по публичному IP-адресу. Адреса локальной сети и `localhost` запрещены.
- Площадка скачивает файл обычным HTTP-клиентом, а не браузером: без JavaScript, cookies, капчи и заголовка Referer. Защита от ботов, проверка браузера, блокировка по стране или IP-адресу не должны закрывать фид, файл подтверждения и фотографии. Каждый запрос площадки несёт заголовок `User-Agent` с `PartnerImport/1.0` и адресом площадки: если защиту отключить нельзя, разреши запросы с этим значением. HTTPS-сертификат не проверяется — подойдёт любой.
- Ответ — код 200 и файл целиком. Пустой ответ, страница входа, коды 4xx и 5xx — ошибка загрузки.
- Перенаправлений — не больше 5. Весь запрос вместе с ними должен уложиться в 60 секунд.
- Размер — до 50 МБ после распаковки. Сжатие при передаче (`Content-Encoding: gzip` и другие) поддерживается, но сам файл должен быть XML, а не архивом `.zip` или `.gz`.
- Формат определяется по содержимому, а не по расширению или `Content-Type`. Рекомендуемый `Content-Type` — `application/xml` или `text/xml`.
- Каждый раз площадка получает весь каталог. Выгрузка только изменений, деление на несколько файлов и постраничная выдача не поддерживаются. Позиция, которой нет в новом файле, снимается с показа.
- Условные запросы. Если сервер отдал заголовок `ETag` или `Last-Modified`, следующий запрос придёт с `If-None-Match` или `If-Modified-Since`. Ответ `304 Not Modified` допустим, только если файл действительно не изменился. Статический файл на nginx или Apache отвечает правильно сам. Если файл формирует скрипт и правильный ответ 304 гарантировать нельзя, не отдавай эти заголовки совсем: ложный 304 означает, что изменения каталога не дойдут до площадки.
- Лучший вариант — статический файл, который генератор пересоздаёт по расписанию: сначала пишет во временный файл в той же папке, затем переименовывает его в постоянное имя. Так площадка не получит недописанный файл. Генерация при каждом запросе рискует не уложиться в 60 секунд.
Ограничения каталога:
- Предложений (`offer`) — до 1000 по умолчанию. Считаются все элементы `offer`, включая снятые (`available="false"`), ошибочные и повторные. Если предложений больше лимита, весь файл отклоняется, а не обрезается.
- Категорий (`category`) — до 150, включая родительские и пустые. Если больше, весь файл отклоняется. Если в магазине больше категорий, выгружай укрупнённое дерево (например, два верхних уровня) и только категории, в которых есть выгружаемые предложения.
## 5. Структура файла
```xml
Зоомагазин «Пример»
ООО «Пример»
https://shop.example/
Товары для кошек
Переноски и сумки
Переноска для кошки
https://shop.example/products/SKU-0042
1500
RUB
10
https://shop.example/images/SKU-0042-main.jpg
Новая переноска. Размер 48 × 32 × 30 см. Самовывоз.
```
Правила:
- Кодировка — UTF-8 (рекомендуется) или Windows-1251, объявленная в первой строке: ``. Без объявления файл читается как UTF-8. Другие кодировки не принимаются.
- XML должен быть корректным: `&`, `<` и `>` в тексте экранированы (`&`, `<`, `>`), значения атрибутов — в кавычках, управляющие символы с кодами 0x00–0x1F (кроме табуляции и переводов строк) удалены, байты — валидный UTF-8. Одна ошибка разметки отклоняет весь файл. Тексты из базы часто содержат управляющие символы и незакрытые HTML-теги — очищай их или передавай HTML через CDATA (раздел 7, `description`).
- Запрещены ``, объявления сущностей и XInclude: с ними весь файл отклоняется. Не добавляй строку ``, которую ставят многие старые генераторы YML.
- Корень — один элемент `yml_catalog`. В нём ровно один `shop`. В `shop` — не больше одного `categories` и одного `offers`, прямыми потомками. `offers` обязателен: без него файл отклоняется. `categories` обязателен, если в `offers` есть хоть одно предложение без `available` или с `available="true"`: без него или с пустым `categories` такой файл тоже отклоняется. Пустой `offers` или `offers` только из предложений с `available="false"` площадка примет и без категорий: так снимаются товары. Но проще и надёжнее выгружать `categories` всегда. `category` стоит только прямо внутри `categories`, `offer` — только прямо внутри `offers`. В других местах файла элементы `shop`, `categories`, `offers`, `category` и `offer` недопустимы — иначе весь файл отклоняется.
- Атрибут `yml_catalog@date` необязателен. Если указываешь его — с часовым поясом, как в примере; без пояса будет предупреждение.
Элементы внутри `shop`:
- `url` — обязателен. Абсолютный адрес сайта человека, например `https://shop.example/`: схема http или https, без логина и пароля, без пробелов, кавычек, `<`, `>` и `\`, до 255 символов. Это «сайт источника»: в его корне размещается файл подтверждения (раздел 9), и на него должны вести ссылки `offer/url`. Сам фид и фотографии могут лежать на других доменах. Без корректного `shop/url` весь файл отклоняется.
- `name`, `company` — необязательны: названия магазина и компании, человек видит их в кабинете. Длиннее 255 символов обрезаются.
- `currencies` — не нужен. Если он есть, площадка пропускает его без предупреждения; курсы валют не применяются.
- Любой другой элемент внутри `shop` (`platform`, `version`, `email`, `delivery-options` и т. п.) пропускается с предупреждением в отчёте.
## 6. Категории
- `id` — обязателен: только цифры, целое число больше 0, не длиннее 18 цифр, уникальное в файле. Нечисловой, нулевой или пустой `id`, повтор `id`, неверный `parentId` — весь файл отклоняется.
- `parentId` — необязателен: `id` родительской категории из этого же файла. У корневой категории `parentId` нет или он равен `0`. Ссылка на отсутствующую категорию и циклы недопустимы — весь файл отклоняется.
- Текст элемента — название категории, до 255 символов. Человек видит его в кабинете, когда сопоставляет категории фида с разделами площадки. В объявления название категории не попадает.
- Каждая категория, на которую ссылается `offer/categoryId` предложения к публикации (без `available` или с `available="true"`), должна быть в `categories`. У предложений с `available="false"` `categoryId` не сверяется (раздел 7).
- `id` категорий должны быть постоянными: бери их из базы проекта, а не нумеруй по порядку. Человек сопоставляет категории с разделами площадки по `id`. Категория, исчезнувшая из файла, теряет сопоставление, а категорию с новым `id` придётся настраивать заново. Исключение — файл совсем без категорий (пустой или отсутствующий `categories` при одних снятых предложениях): все сопоставления сохраняются до возвращения товаров. Поэтому, снимая товары, не сокращай `categories`: выгружай всё дерево категорий, в том числе категории без предложений.
- Каждая категория сопоставляется отдельно: от родителя раздел не наследуется. Предложение может ссылаться на категорию любого уровня.
## 7. Предложения
Атрибуты `offer`:
- `id` — обязателен. От 1 до 80 символов после обрезки пробелов по краям. Постоянный: по нему площадка при следующих загрузках находит то же объявление. Регистр и ведущие нули значимы: `SKU-42`, `sku-42` и `SKU-0042` — три разных предложения. Бери артикул или первичный ключ из базы, а не номер строки выгрузки. Пустой или слишком длинный `id` — ошибка предложения.
- `available` — `true` или `false`, строчными буквами. Без атрибута предложение считается доступным, а в отчёте появляется общее предупреждение. Пустое значение, `True`, `1`, `yes` — ошибка предложения. Указывай атрибут всегда.
- `type` — не указывай для обычного предложения: название берётся из `name`. Значение `vendor.model` означает, что название составляется из `typePrefix`, `vendor` и `model`. Любой другой тип (`books`, `medicine`, `event-ticket` и прочие) — ошибка предложения.
Площадка читает только прямых потомков `offer`. Значения обрезаются по краям.
- `name` — обязателен для обычного предложения. Становится названием объявления. Правила текста — раздел 8.
- `typePrefix`, `vendor`, `model` — только для `type="vendor.model"`. Название объявления — `typePrefix vendor model` через пробел. `vendor` и `model` обязательны, `typePrefix` — нет. Без `vendor` или `model` — ошибка предложения.
- `categoryId` — обязателен: `id` категории из `categories`. Нечисловое значение или категория, которой нет в файле, — ошибка предложения. Если `categoryId` нет или он пустой, предложение не будет опубликовано.
- `description` — обязателен: текст объявления. HTML допустим — экранированный или внутри ``; CDATA надёжнее. Площадка удаляет `script` и `style` вместе с содержимым, превращает `p`, `div`, `br`, `li`, `h1`–`h6`, `tr`, `table`, `ul`, `ol`, `blockquote`, `pre`, `hr`, `dl`, `dt`, `dd` в переводы строк, ячейки `td` и `th` — в пробелы, остальные теги удаляет, HTML-сущности раскрывает, пустые строки убирает. Пустой после очистки текст — предложение не будет опубликовано. Необработанное описание больше 10 МБ отклоняет весь файл.
- `price` — необязателен. Неотрицательное десятичное число с точкой: `1500`, `1499.90`. Без пробелов, запятых, знака валюты и экспоненты, иначе ошибка предложения. Дробная часть округляется до целого, половина — вверх: `1499.49` → 1499, `1499.50` → 1500. После округления — не больше 4294967295. Отсутствующая, пустая или нулевая цена означает «договорная»; пометку «бесплатно» фид не ставит.
- `currencyId` — необязателен, регистр не важен. Допустимы `RUB` (и `RUR` как то же самое), `USD`, `EUR`, `AMD`, `KZT`, `GEL`, `BYN`, `UAH`, `AZN`, `KGS`, `RSD`. Без `currencyId` — рубли. Другая валюта — ошибка предложения. Цена не пересчитывается.
- `url` — необязателен, но желателен. Абсолютный адрес страницы предложения: http или https, до 255 символов, без пробелов и обратной косой черты, без логина и пароля. Домен — тот же, что в `shop/url`, его поддомен или вариант с `www` и без него. Иначе — ошибка предложения. В объявлении ссылка выводится строкой «Веб-сайт» и считается способом связи: без неё новое объявление создаётся, только если человек выбрал в кабинете телефон, email или Telegram.
- `picture` — необязателен, повторяется. Абсолютные адреса изображений в нужном порядке; первое — главное. Пустые элементы пропускаются, берутся первые 5 адресов, остальные игнорируются с предупреждением. Подробно — раздел 10.
Предложение с `available="false"` площадка читает почти без проверок: нужен корректный `id`, остальные поля не проверяются. Существующее объявление снимается с показа, новое не создаётся.
Всё остальное внутри `offer` (`param`, `oldprice`, `vendorCode`, `barcode`, `delivery`, `pickup`, `sales_notes`, `country_of_origin` и т. п.) пропускается, а имена элементов попадают в предупреждение отчёта. Это не ошибка, но лишние элементы лучше не выгружать. Если в характеристиках есть важное для покупателя — размеры, состав, комплектация, — перенеси это в `description` текстом.
## 8. Правила для названия и описания
Площадка проверяет название и описание по тем же правилам, что и объявления, поданные вручную. Предложение с нарушением пропускается, причина видна в отчёте. Тексты не обрезаются и не исправляются автоматически.
Важно: эти правила проверяются только при загрузках после одобрения заявки. Кнопка «Проверить фид» в кабинете их не проверяет — она находит ошибки структуры и формата полей, а также пустое описание и отсутствующий `categoryId` у доступного предложения. Поэтому правила текстов проверь сам до сдачи работы.
- Название — от 1 до 150 символов. Описание — от 1 до 5000 символов после очистки HTML.
- Нет ссылок: любые `http://`, `https://` или `www.` в тексте. Ссылку на страницу предложения передавай только в `offer/url`.
- Нет адресов email.
- Нет номеров телефонов. Телефоном считается ряд из 11 цифр, который начинается с `7`, `8` или `+7`, а также `+374` и ещё 8 цифр, если между цифрами нет букв (пробелы, скобки, точки, дефисы, запятые и косые черты не мешают) и ряд не стоит вплотную к другим цифрам или буквам. Под это правило попадают и артикулы вида `8-123-456-78-90`. Контакты площадка добавляет в объявление сама из настроек кабинета.
- Нет «слов» из 30 и более символов без пробелов — считаются любые символы, кроме пробельных, включая знаки препинания. Длинные артикулы, перечисления через косую черту (`кошек/собак/хомяков/попугаев`) и склеенные слова разделяй пробелами.
- В названии нет названия города, выбранного человеком в кабинете, в любой форме: «Москва», «Москвы», «Москве». Проверка не различает регистр, считает латинские буквы-двойники кириллическими и ищет название и внутри других слов. В описании город упоминать можно.
- Нет нецензурной лексики и стоп-слов. Словари открыты: https://github.com/Vasiliy-Makogon/RussianBadWords — нецензурная лексика в файлах `data/profanity.php`, `data/rude.php`, `data/insults.php`, `data/anatomy.php`, стоп-слова — в каталоге `data/stop-words/`. Темы стоп-слов: алкоголь; табак, вейпы, кальяны; наркотики; оружие и средства самообороны; насилие; экстремизм; суицид; интим-услуги; казино и азартные игры; кредиты, займы, криптовалюта, страховки; рецептурные препараты и донорство; поддельные документы и дипломы; магия и гадания; пиратство и взлом; спам и мошенничество; запрещённые орудия лова; даркнет и названия даркнет-площадок (среди них есть обычные слова, например `empire`, `evolution`, `hydra`). Слово ищется целиком в любой форме из словаря, без учёта регистра, буквы «ё», латинских двойников и повторов букв; слово через дефис проверяется и по частям. Если такое слово — часть названия бренда или модели, обсуди с человеком, как переформулировать.
Повторяющиеся названия и описания у разных предложений допустимы.
## 9. Подтверждение сайта
Площадка проверяет, что фид публикует владелец сайта из `shop/url`. После сохранения ссылки на фид кабинет показывает файл вида `partner-verification-<метка>.txt`, где метка — 32 шестнадцатеричных символа, а содержимое файла — та же метка.
- Размести файл в корне сайта из `shop/url`, чтобы он открывался по адресу `<схема и домен из shop/url>/partner-verification-<метка>.txt`, например `https://shop.example/partner-verification-0123456789abcdef0123456789abcdef.txt`. Если в `shop/url` указан порт, файл проверяется на том же порту.
- Ответ — код 200, тело — ровно метка; пробелы и переводы строк по краям допустимы. Размер ответа — не больше 1 КБ, время — до 10 секунд.
- Перенаправление допустимо только в пределах того же сайта: между http и https и между вариантами с `www` и без него. Перенаправление на другой домен или поддомен проверку не проходит.
- Не удаляй файл после подтверждения. Если человек перевыпустит метку или в `shop/url` появится другой домен, понадобится новый файл.
- Если файла подтверждения ещё нет, всё равно сделай фид: человек получит имя файла после сохранения ссылки и передаст его тебе.
## 10. Фотографии
- Абсолютные адреса http или https; относительные пути (`/images/1.jpg`) не загружаются. Те же сетевые правила, что для фида (раздел 4): стандартный порт, публичный адрес, без пароля и защиты от ботов. Защита от прямых ссылок по заголовку Referer не должна блокировать запросы без этого заголовка.
- Форматы — JPEG, PNG или WebP; определяются по содержимому, а не по расширению. GIF, SVG и прочие не принимаются.
- Размер одного файла — до 15 МБ, разрешение — до 25 мегапикселей (ширина × высота), ответ сервера — до 10 секунд.
- Площадка запоминает фотографию по адресу и повторно её не скачивает. Чтобы заменить изображение, измени адрес: имя файла или параметр вроде `?v=2`. Новый файл по старому адресу площадка не заметит.
- Порядок адресов задаёт порядок фотографий. Адрес, исчезнувший из первых пяти, отвязывает свою фотографию от объявления.
- Сбой фотографии — не ошибка предложения: объявление публикуется без неё, а площадка повторяет попытку примерно раз в сутки, пока адрес остаётся в фиде.
## 11. Обновление, снятие и возврат предложений
- Изменил цену, текст или фотографии — оставь прежний `id`: площадка обновит то же объявление, его адрес и дата публикации сохранятся. Обновление не поднимает объявление в списке.
- Новая позиция — новый `id`. Никогда не меняй `id` существующей позиции и не отдавай `id` снятой позиции другой: новый `id` создаёт новое объявление, а старое снимается.
- Временно нет в наличии — `available="false"` или убери предложение из файла; позиция снята с продажи навсегда — убери её из файла. В обоих случаях объявление скрывается и хранится 180 дней: если предложение вернётся с тем же `id`, вернётся прежнее объявление. Через 180 дней объявление удаляется. Учти, что предложения с `available="false"` занимают место в лимите.
- Пустой каталог `` снимает с показа все объявления. Если элемента `offers` в файле нет совсем, весь файл отклоняется, и объявления не меняются. Сопоставления категорий при снятии сохраняются, только если в файле всё дерево `categories` или категорий нет совсем (раздел 6).
- Если `id` повторяется, все предложения с этим `id` пропускаются, а прежнее объявление остаётся без изменений.
- Если у какого-то предложения `id` пустой или длиннее 80 символов, в этой загрузке площадка не снимает предложения, исчезнувшие из файла.
- Объявление, которое удалил администратор площадки, по тому же `id` заново не создаётся; причина видна в отчёте.
- Ошибка в новой версии предложения не портит объявление: оно остаётся с прежними данными до первой успешной загрузки исправленной версии.
## 12. Чек-лист самопроверки
Выполни перед отчётом. Каждый пункт — проверка результата, а не намерение.
1. Фид скачивается без cookies и авторизации: `curl -sS -o feed.xml -w '%{http_code} %{size_download} %{time_total}\n' '<адрес фида>'` — код 200, размер до 50 МБ, время заметно меньше 60 секунд. Запрос сделан не изнутри сервера проекта, а из внешней сети: так видно, что фид доступен из интернета.
2. Файл разбирается XML-парсером без ошибок (например, `xmllint --noout feed.xml`). В нём нет ``.
- «В файле несколько разделов shop.» — оставь один `shop`.
- «В файле отсутствует раздел offers с предложениями.» — добавь `offers` в `shop`.
- «В файле нет категорий: раздел categories отсутствует или пуст. …» — добавь `categories` в `shop` или заполни его, если он выгружается пустым: в нём должны быть все категории, на которые ссылаются `categoryId`.
- «В разделе shop отсутствует корректный HTTP- или HTTPS-адрес сайта источника в поле url.» — исправь `shop/url`.
- «Число предложений в фиде: N, разрешено не более M.» — уменьши выгрузку или договорись о лимите.
- «Количество категорий превышает допустимое» — сократи дерево категорий до 150.
- «У категории не указан или недопустим номер …», «Идентификатор категории N повторяется», «У категории N указан родитель, которого нет в фиде», «В дереве категорий найден цикл» — исправь `categories`.
- «Сайт источника не подтверждён. Причина: …» — раздел 9.
Ошибки предложения:
- «Недопустимое значение available» — только `true` или `false`.
- «Неподдерживаемый тип предложения» — убери `type` или используй `vendor.model`.
- «Не заполнено название предложения в поле name», «Для типа vendor.model заполните поля vendor и model».
- «Некорректная цена», «Цена превышает максимально допустимое значение» — формат `price`.
- «Идентификатор предложения id отсутствует или превышает 80 символов», «Идентификатор предложения повторяется».
- «Категория N не найдена в разделе categories».
- «Категории предложения не настроено соответствие» — у предложения нет `categoryId` или человек ещё не выбрал раздел для этой категории в кабинете.
- «Не заполнено описание предложения в поле description».
- «Неизвестная валюта».
- «Ссылка на страницу предложения превышает допустимую длину», «Ссылка на страницу предложения должна вести на подтверждённый сайт магазина или его поддомен» — `offer/url`.
- «Объявление не прошло проверки: …» — правила раздела 8 или настройки кабинета (способ связи, раздел площадки). Причина указана после двоеточия.
- «Создание объявления по этому предложению запрещено администрацией» — решение площадки, фидом не исправляется.
Предупреждения:
- «Неподдерживаемые элементы пропущены: …» — элементы, которых нет в этом документе; можно не выгружать.
- «Число предложений без атрибута available: N» — добавь `available`.
- «Дата фида указана без часового пояса» — добавь пояс в `yml_catalog@date` или убери атрибут.
- «Загружены первые 5 фотографий из N», «Не удалось скачать фотографию …», «Фотография … не принята», «Фотография … превышает допустимый размер в мегапикселях» — раздел 10.
- «Объявления по предложениям, удалённым из файла, в этом запуске не скрываются» — есть предложения с пустым или слишком длинным `id`.
## 15. Пример фида со всеми поддерживаемыми полями
```xml
Зоомагазин «Пример»
ООО «Пример»
https://shop.example/
Товары для кошек
Переноски и сумки
Когтеточки
Переноска для кошки
https://shop.example/products/SKU-0042
1500
RUB
10
https://shop.example/images/SKU-0042-main.jpg
https://shop.example/images/SKU-0042-side.jpg
Новая переноска.
Размер 48 × 32 × 30 см. Самовывоз.
]]>
Когтеточка
ПримерДом
Floor Tower S
https://shop.example/products/SKU-0107
3490.90
RUB
20
https://shop.example/images/SKU-0107.jpg
Когтеточка-стойка, обмотка из натурального сизаля. Высота 60 см.
Мяч для кошек, 2 шт.
1
Два мяча с колокольчиком.
Лежанка для кошки
1
```