Из HTML можно достать все названия товаров и все цены, а потом случайно соединить цену одного товара с названием другого. Особенно если у какой-то карточки поле пропущено. В этой инструкции сначала разделим страницу на карточки и только потом прочитаем поля внутри каждой. Результат проверим до любой отправки дальше.
Результат инструкции
Соберём ручной workflow на учебном HTML с тремя товарами. Штатный HTML выделит карточки, Split Out превратит их в отдельные items, второй HTML достанет ID, название, цену в копейках и относительную ссылку. Последний Code проверит количество, обязательные поля и типы, затем соберёт один JSON с products и count 3.
ID 001 останется строкой с ведущими нулями, цена 0 — числом, Чай & кофе превратится в Чай & кофе. Ссылки никуда не открываются, HTTP-запросов и изменения каталога нет. action_performed false. AI-модель для этой задачи не нужна.
Что понадобится
- Работающий n8n и права создать отдельный учебный workflow. Проверено на n8n 2.42.4, HTML версии 1.2, Split Out версии 1.
- Только вымышленная разметка из примера. Аккаунты магазинов, браузерная авторизация и API-ключи не нужны.
- Договор полей: три цифры в ID, непустое название, цена целыми неотрицательными копейками, ссылка вида /products/ID.
Установить n8n локально через Docker
Шаг 1. Импортируйте готовый workflow
Скачать n8n-html-products.json
Создайте новый workflow. В меню редактора выберите Import from File и загрузите JSON. Если файл открылся текстом, сохраните его и импортируйте. На полотне появятся восемь последовательно соединённых узлов:
Manual Trigger → Sample HTML → Check HTML Text → Extract Cards
→ Check Card List → Split Cards → Extract Fields → Check Products
Extract Cards и Extract Fields — HTML. Split Cards — Split Out. Sample HTML, Check HTML Text, Check Card List и Check Products — Code. Для ручной сборки используйте эти же имена: проверка обращается к Check Card List по имени. Импорт не содержит credentials и ID чужого workflow, остаётся неактивным.
Шаг 2. Подготовьте учебную разметку
В Sample HTML задайте Mode: Run Once for All Items и Language: JavaScript. Вставьте:
const html = `<!doctype html>
<html lang="ru"><body>
<main class="catalog">
<article class="product-card"><a class="product-link" data-id="001" href="/products/001"><span class="product-name">Чай & кофе</span></a><span class="product-price" data-price-minor="150000">1 500 рублей</span></article>
<article class="product-card"><a class="product-link" data-id="002" href="/products/002"><span class="product-name">Блокнот</span></a><span class="product-price" data-price-minor="0">0 рублей</span></article>
<article class="product-card"><a class="product-link" data-id="003" href="/products/003"><span class="product-name">Лампа</span></a><span class="product-price" data-price-minor="49900">499 рублей</span></article>
</main>
</body></html>`;
return [{ json: { html } }];
Выполните источник от Manual Trigger. В Output должен быть один item с полем html. У каждой карточки есть class product-card, ссылка product-link с data-id и href, название product-name и цена product-price с data-price-minor. ID находится на ссылке внутри карточки, а не на внешнем article.
Текст 1 500 рублей нужен человеку, а data-price-minor содержит машинное значение 150000. В этой инструкции читается именно атрибут, не строка с пробелами и валютой. Согласованность отображаемой и машинной цены в реальном магазине нужно проверять отдельно: наличие атрибута не гарантирует актуальность цены.
Шаг 3. Проверьте входной HTML
В Check HTML Text задайте Run Once for All Items, JavaScript и вставьте:
const items = $input.all();
if (items.length !== 1) throw new Error('one_html_item_required');
const html = items[0].json.html;
if (typeof html !== 'string' || !html.trim()) throw new Error('html_text_required');
if (Buffer.byteLength(html, 'utf8') > 16384) throw new Error('html_too_large');
return [{ json: { html } }];
Принимается ровно один item, непустая строка и не более 16384 байт UTF-8. Пустота, null и объект вместо текста дают html_text_required. Слишком большой вход — html_too_large. Это учебный лимит, а не предел возможностей HTML-узла и не полный аудит безопасности обработки HTML.
В Settings всех узлов оставьте On Error: Stop Workflow. Execute Once, Always Output Data и Retry On Fail выключены. Повтор не вернёт отсутствующую цену и не исправит ошибочный CSS-селектор.
Шаг 4. Извлеките целые карточки
В Extract Cards выберите Operation: Extract HTML Content, Source Data: JSON, JSON Property: html. В Extraction Values добавьте одну строку: Key cards, CSS Selector .product-card, Return Value HTML, Return Array On. В Options включите Trim Values и Clean Up Text.
Выполните узел. Ожидается один item с массивом cards из трёх строк. Каждая строка содержит внутреннюю разметку одной карточки. Сам внешний article в возвращённый HTML не входит. Поэтому во втором HTML-узле не нужно искать .product-card .product-name: внешнего контейнера там уже нет.
Не извлекайте имена и цены сразу со всей страницы в независимые массивы. Их одинаковая длина не доказывает правильное соответствие. Мы сохраняем связь полей через общий контейнер карточки.
Шаг 5. Проверьте, что карточки найдены
В Check Card List задайте Run Once for All Items, JavaScript и вставьте:
const items = $input.all();
if (items.length !== 1 || !Array.isArray(items[0].json.cards)) {
throw new Error('cards_array_required');
}
const cards = items[0].json.cards;
if (!cards.length) throw new Error('cards_empty');
if (cards.length > 20) throw new Error('too_many_cards');
if (cards.some(card => typeof card !== 'string' || !card.trim())) {
throw new Error('card_html_required');
}
return [{ json: { cards } }];
Пустой список даёт cards_empty. Более 20 карточек — too_many_cards. Двадцать — выбранное ограничение учебного прототипа, не ограничение CSS-поиска. Проверка нужна до Split Out: если массив пуст, последующие узлы могут не получить ни одного item, и отсутствие результата легко принять за успешную обработку.
Шаг 6. Разделите карточки на отдельные items
В Split Cards выберите Field to Split Out: cards. В Include задайте No Other Fields. В Options укажите Destination Field Name: card_html. После выполнения должно быть три items, каждый с одной строкой card_html.
Не подавайте в Extract Fields исходный html всей страницы повторно. Его вход — отдельная карточка текущего item. В этом и состоит отличие от сбора нескольких независимых списков. Если карточек одна, Split Cards должен выдать один item, а не другой формат документа.
Подробная инструкция по Split Out
Шаг 7. Извлеките поля внутри каждой карточки
В Extract Fields задайте Operation: Extract HTML Content, Source Data: JSON, JSON Property: card_html. В Extraction Values добавьте четыре строки. Для каждой включите Return Array, даже если ожидается одно значение:
| Key | CSS Selector | Return Value |
|---|---|---|
| id | .product-link | Attribute: data-id |
| name | .product-name | Text |
| price | .product-price | Attribute: data-price-minor |
| href | .product-link | Attribute: href |
Trim Values и Clean Up Text включите. Поле Attribute заполняется отдельно, когда Return Value выбран Attribute. Не вписывайте data-id в CSS Selector вместо .product-link. Return Array нужен, чтобы проверка различала одно значение, отсутствие и повтор поля.
После выполнения первый item Extract Fields должен выглядеть так:
{
"id": [
"001"
],
"name": [
"Чай & кофе"
],
"price": [
"150000"
],
"href": [
"/products/001"
]
}
Цена пока строка, а не число. Сущность & в названии уже преобразована парсером в обычный амперсанд. Заново заменять её вручную не нужно. ID, name, price и href находятся в массивах длины 1 внутри одного item.
Шаг 8. Проверьте поля и соберите результат
В Check Products выберите Run Once for All Items, JavaScript и вставьте весь код:
Показать код (33 строк)
const items = $input.all();
const expectedCount = $('Check Card List').first().json.cards.length;
if (items.length !== expectedCount) throw new Error('product_count_mismatch');
const ids = new Set();
const single = (row, key) => {
const value = row[key];
if (!Array.isArray(value) || value.length !== 1 || typeof value[0] !== 'string' || !value[0].trim()) {
throw new Error(key + '_single_value_required');
}
return value[0];
};
const products = items.map(item => {
const row = item.json;
if (Object.keys(row).sort().join('|') !== 'href|id|name|price') {
throw new Error('product_fields_mismatch');
}
const id = single(row, 'id');
const name = single(row, 'name');
const rawPrice = single(row, 'price');
const href = single(row, 'href');
if (!/^\d{3}$/.test(id) || ids.has(id)) throw new Error('product_id_invalid_or_duplicate');
if (!/^\d+$/.test(rawPrice) || !Number.isSafeInteger(Number(rawPrice))) {
throw new Error('price_minor_invalid');
}
if (href !== '/products/' + id) throw new Error('product_href_mismatch');
ids.add(id);
return { product_id: id, name, price_minor: Number(rawPrice), href };
});
return [{ json: {
products,
count: products.length,
action_performed: false,
} }];
Проверка требует ровно один непустой текст для каждого поля карточки и то же число items, которое было в Check Card List. ID остаётся строкой из трёх цифр и не должен повторяться. Цена должна состоять из цифр и помещаться в безопасный целочисленный диапазон JavaScript, только затем она превращается в Number.
Ссылка сверяется с /products/ плюс ID этой карточки. Это узкое правило учебного каталога, не универсальная нормализация URL. Запросов по ссылкам код не делает. Для реального источника отдельно согласуйте домен, относительные адреса, query-параметры и допустимые маршруты.
Шаг 9. Выполните весь workflow
Нажмите Execute Workflow. Ожидаемые количества: Extract Cards — один item с тремя строками, Split Cards — три items, Extract Fields — три items, Check Products — один item с массивом products. Финальный результат:
{
"products": [
{
"product_id": "001",
"name": "Чай & кофе",
"price_minor": 150000,
"href": "/products/001"
},
{
"product_id": "002",
"name": "Блокнот",
"price_minor": 0,
"href": "/products/002"
},
{
"product_id": "003",
"name": "Лампа",
"price_minor": 49900,
"href": "/products/003"
}
],
"count": 3,
"action_performed": false
}
Сверьте не только count 3, но и каждый комплект полей. У 001 название Чай & кофе, цена 150000 копеек, ссылка /products/001. У 002 Блокнот с ценой 0; у 003 Лампа с ценой 49900. Нулевую цену нельзя отбрасывать проверкой по истинности значения.
| ID | Название | Цена в копейках |
|---|---|---|
| 001 | Чай & кофе | 150000 |
| 002 | Блокнот | 0 |
| 003 | Лампа | 49900 |
Шаг 10. Проверьте одну карточку и посторонний текст
В Sample HTML временно оставьте только article товара 001. После полного запуска ожидаются один item в Extract Fields, count 1 и один объект products. Верните три карточки.
Теперь добавьте вне article отдельный span с классом product-name и любым учебным текстом. Результат должен остаться прежним: внешний текст не входит ни в одну выделенную карточку. Такой тест показывает, что названия извлекаются из карточки, а не глобальным селектором по всей странице. После опыта удалите добавленный элемент.
Шаг 11. Проверьте пропуск и повтор поля
У первого товара временно уберите span product-price целиком. Полный запуск должен остановиться в Check Products с price_single_value_required. Если вместо удаления добавить вторую цену внутрь одной карточки, ожидается та же ошибка: разрешено ровно одно значение.
Для более показательного теста уберите цену из первой карточки и добавьте вторую цену во вторую. Всего на странице всё ещё три цены, но соответствие испорчено. Наша проверка должна отклонить пакет. Не пытайтесь исправить это соединением независимых списков по номеру позиции.
Верните разметку. Замените в первой ссылке href на /products/002, оставив data-id 001. Ожидается product_href_mismatch. Текстовая цена abc даст price_minor_invalid. Все эти нарушения должны завершаться без успешного итогового products.
Шаг 12. Восстановите и сохраните рабочую схему
Верните три исходные карточки, обе операции HTML в Extract HTML Content, правильные JSON Property и все Return Array On. В Extract Cards селектор .product-card, в Extract Fields селекторы из таблицы шага 7. Сохраните и выполните весь workflow, затем сверьте итог с шагом 9.
Если временно поставить в Extract Cards селектор .not-a-card, Check Card List должен дать cards_empty. Если выключить Return Array у name во втором HTML-узле, Check Products даст name_single_value_required. После проверки верните исходные настройки. Не отключайте контроль только ради зелёного статуса.
Если результат отличается
| Симптом | Причина | Что проверить |
|---|---|---|
| cards_empty | Селектор не нашёл карточки | Исходный HTML и .product-card |
| cards_array_required | Карточки выданы строкой | Return Array в Extract Cards |
| name_single_value_required | Нет названия, их несколько или не массив | Селектор, Return Array и текущая карточка |
| price_single_value_required | Нет нужного атрибута или цена повторена | product-price и data-price-minor |
| product_href_mismatch | Ссылка не соответствует ID | data-id и href одной карточки |
Где заканчивается эта схема
Штатный HTML здесь разбирает переданную строку, а не открывает страницу как браузер. Пример не проверяет динамическую загрузку товаров JavaScript, авторизацию, пагинацию, сетевые запросы или доступ к чужому сайту. Если в полученной разметке карточек нет, не считайте, что селектор может извлечь ещё не загруженные данные.
Для настоящего источника сначала проверьте разрешённый способ получения данных и наличие API. Не обходите ограничения доступа. Этот материал использует только собственную вымышленную разметку. Извлечённые тексты также не следует бездумно вставлять в исполняемый HTML-шаблон: безопасный вывод — отдельная задача, не часть данного теста.
Связь полей сохранена в пределах найденной карточки, но это не доказательство подлинности товара или правильности цены у источника. Количество найденных элементов также не гарантирует полноту каталога. Схема не сравнивает подпись цены с её машинным атрибутом и не проверяет реальную возможность покупки.
Разобрать XML в JSON с проверкой полей
Остановить workflow при нарушении договора данных
Что проверено редакцией
16 октября 2026 года workflow проверен на n8n 2.42.4 в отдельном Docker-экземпляре штатными ручными запусками API. Успешно пройдена 31 проверка: три карточки, одна, обратный порядок, посторонние поля вне карточек, вложенная разметка названия, пробелы, пустой/null/объектный/слишком большой вход, отсутствие и избыток карточек, пропущенная цена/название, повтор названия/цены, пропуск и лишняя цена в разных карточках при одинаковом общем количестве, отсутствующий атрибут, повтор/неверный ID, текстовая/отрицательная/дробная/небезопасная цена, неверная/отсутствующая ссылка, Return Array off в двух узлах, неверные селекторы и входное поле. Сверены наборы полей каждой карточки, строки ID, ноль, UTF-8, амперсанд, точный итог и отсутствие успешного результата при нарушениях. Использованы штатные HTML версии 1.2 с Cheerio и Split Out версии 1, настройки сверены с официальной документацией и кодом узлов. Редакторские клики, Cloud, внешние сайты, браузерный JavaScript, авторизация, реальные цены, большие объёмы и полный аудит безопасности HTML не тестировались. Учебный контейнер остановлен, чужие workflows не менялись. Обложка — редакционная AI-иллюстрация.
Частые вопросы
Почему не собрать все названия и цены сразу?
Отсутствие или повтор поля могут сдвинуть независимые списки. Даже одинаковая длина не гарантирует соответствие. В примере сначала выделяются карточки, затем поля читаются внутри каждого отдельного item.
Зачем Return Array, если нужно одно значение?
Так проверка видит количество совпадений: ноль, одно или несколько. В примере для каждого обязательного поля разрешён массив ровно из одной непустой строки.
Почему цена берётся из data-price-minor?
Это договор учебного источника: атрибут содержит целые копейки. Формат отображаемой цены может включать пробелы и валюту. Для другой разметки нужен другой согласованный разбор и проверка.
HTML-узел загрузит товары через JavaScript?
В проверенном сценарии он разбирает уже переданную HTML-строку. Браузерная загрузка, выполнение скриптов и получение динамического каталога не выполнялись и не тестировались.
Можно подставить HTML настоящего магазина?
Сначала проверьте допустимый способ получения данных, структуру карточек и договор полей. Пример рассчитан на учебную разметку и не доказывает корректность селекторов, цен или полноту чужого каталога.