- Регистрация
- 01.10.2015
- Сообщения
- 255
- Благодарностей
- 1 051
- Баллы
- 93
Всем привет! 
Все мы привыкли, что языковые модели генерируют ответы в виде осмысленного человеческого текста, и чаще всего в таком виде его и используем. Для генерации текстов, статей, комментариев, иного контента, просто для ответов на что-либо. Также многие знают, что модели хорошо умеют в код и математику, хоть и используется это чаще для личных целей, а не для генерации контента. К чему я это – модели умеют генерировать не только человекоподобный текст, но и в строгом конкретном формате, например в валидном XML или JSON.
О такой фиче в этой статье мы и поговорим – о Structured Output (Структурированный вывод).
В контексте написания и использования шаблонов в ZennoPoster к ней можно найти массу полезнейших применений – и в этой статье мы в общем виде рассмотрим несколько кейсов-примеров.
Проще и нагляднее всего объяснить базу на примере задач парсинга.
Допустим, наша задача спарсить объявления о сдаче квартир, но не на досках объявлений, а в тематических группах в соцсетях (вк, тг и т.п.). Задача при таких условиях сложнее в том плане, что на таких площадках нет готовой разметки как на досках объявлениях, т.е. нет постоянных полей и элементов на странице, откуда сразу можно парсить конкретные и понятные данные – из этого элемента метраж квартиры, их этого – стоимость, из третьего – количество комнат и т.п. В соцсетях объявления оформлены просто в виде постов текста, а нам всё-таки при парсинге хотелось бы их разобрать в те же удобные таблички, как и при парсинге досок объявлений и специализированных сайтов.
И тут нам и помогает LLM с функцией вывода в Structured Output.
Зеннкой мы целиком парсим текст объявления, и просто отправляем его в LLM, вместе с заранее подготовленной JSON-схемой с нужными полями из объявления.
LLM читает объявление, "анализирует" его и рассовывает важную инфу из него строго по нужной нам JSON-схеме – и возвращает нам ответ в виде чистого валидного JSON.
Нам остаётся только разобрать этот JSON в ту же табличку, БД или иное место, куда нам надо. Обычным кодом, т.к. JSON будет в правильном виде – в этом и есть прелесть фичи Structured Output.
Допустим, мы обычным парсингом в ZennoPoster забрали в переменную такой текст объявления:
Нам надо вычленить из них параметры квартиры и положить в БД, чтобы можно было сравнивать с другими собранными квартирами.
Мы отправляем текст объявления модели и правила JSON-схемы, в которой хотим получить от неё ответ. И получаем именно то, что хотим:
И всё, в таком структурированном виде отправить данные в БД – тривиальная задача.
Таким образом мы с помощью LLM разбираем тексты на составляющие без необходимости придумать суперсложный парсинг (на тех же регулярках например, как в старые времена), чтобы он работал на 100500 объявлений, которые могут писать люди.
Какие ещё могут быть кейсы в контексте автоматизации в ZennoPoster?
Заполнение форм на любых сайтах
Формы бывают на сайтах самые разные. Можно по старинке закодить заполнение наиболее часто встречаемые виды форм, попытаться сделать "универсальный заполнятор". Для создания такого шаблона нужно проделать довольно много работы – собрать популярные формы, спроектировать логику универсального заполнения, закодить это всё, проверить. И всё равно на практике в интеренете всегда найдутся формы, которые вы не предусмотрели.
А можно просто отправлять целиком формы (или даже HTML всей страницы) в LLM, и пусть она сама разбирает вёрстку каждой новой формы, а нам возвращает что-куда заполнять и куда тыкать. И это будет по-настоящему универсально, потому что примерно как настоящий человек разберётся как заполнять любую рандомную форму, так и ИИ может сделать то же самое.
Генерация контента с жёсткими полями под постинг
Допустим, мы генерируем статьи для сайта/площадки. и по API движка/площадки публикуем их. С помощью Structured Output мы можем сразу генерировать статью в формате, который требует API, и сразу с дополнительными полями типа заголовка, тегов и прочего. А не кодить самостоятельно всё это форматирование/обвязку.
Гулялка по сайтам с классификацией страниц
Можно замахнуться даже на что-то более масштабное, а-ля делегирование нагула кук для профилей. Например, заходим на рандомную страницу рандомного сайта, и отправляем её код/DOM целиком в LLM. А она пусть решает, что имеет смысл делать на этой страницы. Например, если это страница со статьёй, то нужно имитировать на ней чтение, если страница с капчей – надо её решить, если есть попап с формой – надо её заполнить. Мы заранее только прописываем в промпте для LLM какие вообще действия можно делать, и кодим блоки выполнения этих действий. А ходить по страницам и что на них делать – будет решать модель на основе их содержимого. Такая гулялка при должной реализации будет куда естественнее в плане поведения на сайтах, чем обычные боты.
Теперь немного о том, где и какие модели будем запускать в нашей демонстрации.
Подключение к LM Studio
В статьях к предыдущих конкурсах я уже неоднократно упоминал LM Studio в качестве локального способа запуска моделей и использования в ZennoPoster.
И в этот раз тоже будем подключаться к LM Studio – просто потому что это очень просто, локально и бесплатно.
Всё, что нам нужно сделать в самой программе – перейти на вкладку "Разработка", запустить сервер и выбрать модель (ну и скачать её предварительно, если раньше вы не пользовались LM Studio).
Что тут стоит упомянуть – модель крайне желательно выбирать ту, которая нативно поддерживает фичу Structured Output.
Не каждая из моделей поддерживает её, хотя и работать скорее всего будет и с обычной не сильно хуже. Отличие в том, что если для LLM заявлена поддержка Structured Output, значит она специально дообучалась под этот формат вывода, и значит что вероятность ошибки в ответах кратно снижается.
Про поддержку пишут обычно в карточке модели, например на том же HuggingFace.
Также, чем "умнее" и современнее модель, и чем больше у неё параметров – тем лучше она будет справляться с нашими задачами. Разумеется, речь не про использование самых лучших и тяжелых локальных моделей, скорее про баланс – выбирать модель так, чтобы она не сильно нагружала вашу рабочую машину/сервер, и чтобы не была слишком "глупой" чтобы типовые разметки и задачи.
Вот пример кода подключения и запроса к модели в LM Studio:
Этим кодом мы отправляем html некоей формы с сайта, промпт и схему JSON, в формате которой модель должна ответить.
От сервера LM Studio приходит ответ модели, который мы помещаем в переменную проекта jsonStructuredOutput. Далее можно разбирать этот ответ, делать что нам нужно исходя из начальной задачи.
Что нужно отправлять модели
В примере подключения у нас используется несколько сущностей-заготовок, которые возможно не всем будет очевидно.
По-хорошему нам всегда надо отправлять 3 вещи.
Ну а пайплайн у нас будет примерно такой, соответственно:
Подробнее пример реализации таких проверок и "починок" можно посмотреть в прикреплённом шаблоне.
Кейсы
Давайте рассмотрим конкретные кейсы использования Structured Output, а также примеры промптов, схем и кода для них.
1. Умное заполнение неизвестной формы
Допустим, вы делаете универсальный шаблон по заполнению любых форм заказа/контактов/регистрации.
Универсальный в том смысле, что заранее неизвестно, как форма будет выглядеть, какие поля иметь и не иметь.
В нашем случае нам надо брать весь блок form и отправлять модели, а она нам будет в JSON отвечать, какие поля и чем именно заполнять, а также куда тыкать для отправки формы. Чем заполнять – берём данные из текущего профиля ZennoPoster.
2. Умный парсер списка объявлений/постов
Похожая на предыдущую задачу, в плане того, что нужна универсальность.
С помощью LLM и Structured Output мы её и достигаем.
3. Гулялка с классификацией страниц
Тот самый кейс, который мы упоминали в начале статьи – что делать на странице решает LLM, в зависимости от содержимого этой самой страницы.
Тоже момент по универсальности. Не надо по шаблону тыкать те же проверки на капчу. Если капча на странице вдруг появится перед очередным действием – LLM нам об этом отрапортует и выберет ветку блока решения капчи в шаблоне, например.
4. Извлечение контактов/реквизитов/соцсетей со страницы
Ещё одна задача по парсингу, но другого плана.
Тут кстати можно прикрутить дополнительное применение способностей LLM. Мы можем например просить модель не просто собирать телефоны со страницы, а сразу возвращать их в определённом формате (например XXX-XX-XX). Отпадает необходимость самостоятельно потом нормализовывать спаршенные данные.
Тут мы затронули 4 простых кейса и примеры промптов и схем к ним. Однако, в прикреплённом шаблоне и видео упоминаются ещё несколько.
Надёжность и решение проблем
Выше мы уже упоминали, что хоть и модели с поддержкой Structured Output отлично справляются с соответствующими задачами, проверять ответы всегда надо.
Поэтому по-хорошему надо заранее предусмотреть возможные проблемы и пути их решения.
В принципе, на этом и закончим. Областей применения для этой фичи в действительности несравнимо больше, и можно сильно упростить себе разработку шаблонов вот таким вот образом, плюс найти новые применения или даже ниши. Задачка вам на подумать.

Все мы привыкли, что языковые модели генерируют ответы в виде осмысленного человеческого текста, и чаще всего в таком виде его и используем. Для генерации текстов, статей, комментариев, иного контента, просто для ответов на что-либо. Также многие знают, что модели хорошо умеют в код и математику, хоть и используется это чаще для личных целей, а не для генерации контента. К чему я это – модели умеют генерировать не только человекоподобный текст, но и в строгом конкретном формате, например в валидном XML или JSON.
О такой фиче в этой статье мы и поговорим – о Structured Output (Структурированный вывод).
В контексте написания и использования шаблонов в ZennoPoster к ней можно найти массу полезнейших применений – и в этой статье мы в общем виде рассмотрим несколько кейсов-примеров.
Проще и нагляднее всего объяснить базу на примере задач парсинга.
Допустим, наша задача спарсить объявления о сдаче квартир, но не на досках объявлений, а в тематических группах в соцсетях (вк, тг и т.п.). Задача при таких условиях сложнее в том плане, что на таких площадках нет готовой разметки как на досках объявлениях, т.е. нет постоянных полей и элементов на странице, откуда сразу можно парсить конкретные и понятные данные – из этого элемента метраж квартиры, их этого – стоимость, из третьего – количество комнат и т.п. В соцсетях объявления оформлены просто в виде постов текста, а нам всё-таки при парсинге хотелось бы их разобрать в те же удобные таблички, как и при парсинге досок объявлений и специализированных сайтов.
И тут нам и помогает LLM с функцией вывода в Structured Output.
Зеннкой мы целиком парсим текст объявления, и просто отправляем его в LLM, вместе с заранее подготовленной JSON-схемой с нужными полями из объявления.
LLM читает объявление, "анализирует" его и рассовывает важную инфу из него строго по нужной нам JSON-схеме – и возвращает нам ответ в виде чистого валидного JSON.
Нам остаётся только разобрать этот JSON в ту же табличку, БД или иное место, куда нам надо. Обычным кодом, т.к. JSON будет в правильном виде – в этом и есть прелесть фичи Structured Output.
Допустим, мы обычным парсингом в ZennoPoster забрали в переменную такой текст объявления:
Код:
Сдаю однокомнатную квартиру на улице Кораблестроителей дом 25, метро Приморская.
От метро 15 минут пешком. 1й этаж, ЗЗ квадратных метра.
На кухне и в комнате недавно сделан ремонт. Новая мебель, хороший матрас, телевизор.
Сдам по договору на 6 месяцев, дальше возможно продление.
Стоимость З7 тысячи в месяц + ку (5000 зимой, 3000 летом).
Залог З0 тысяч. Без комиссии. Порядочным и адекватным.
Мы отправляем текст объявления модели и правила JSON-схемы, в которой хотим получить от неё ответ. И получаем именно то, что хотим:
JSON:
{
"area": 33,
"floor": 1,
"rooms": 1,
"address": "ул. Кораблестроителей, 25",
"metro": "Приморская"
}
Таким образом мы с помощью LLM разбираем тексты на составляющие без необходимости придумать суперсложный парсинг (на тех же регулярках например, как в старые времена), чтобы он работал на 100500 объявлений, которые могут писать люди.
Какие ещё могут быть кейсы в контексте автоматизации в ZennoPoster?
Заполнение форм на любых сайтах
Формы бывают на сайтах самые разные. Можно по старинке закодить заполнение наиболее часто встречаемые виды форм, попытаться сделать "универсальный заполнятор". Для создания такого шаблона нужно проделать довольно много работы – собрать популярные формы, спроектировать логику универсального заполнения, закодить это всё, проверить. И всё равно на практике в интеренете всегда найдутся формы, которые вы не предусмотрели.
А можно просто отправлять целиком формы (или даже HTML всей страницы) в LLM, и пусть она сама разбирает вёрстку каждой новой формы, а нам возвращает что-куда заполнять и куда тыкать. И это будет по-настоящему универсально, потому что примерно как настоящий человек разберётся как заполнять любую рандомную форму, так и ИИ может сделать то же самое.
Генерация контента с жёсткими полями под постинг
Допустим, мы генерируем статьи для сайта/площадки. и по API движка/площадки публикуем их. С помощью Structured Output мы можем сразу генерировать статью в формате, который требует API, и сразу с дополнительными полями типа заголовка, тегов и прочего. А не кодить самостоятельно всё это форматирование/обвязку.
Гулялка по сайтам с классификацией страниц
Можно замахнуться даже на что-то более масштабное, а-ля делегирование нагула кук для профилей. Например, заходим на рандомную страницу рандомного сайта, и отправляем её код/DOM целиком в LLM. А она пусть решает, что имеет смысл делать на этой страницы. Например, если это страница со статьёй, то нужно имитировать на ней чтение, если страница с капчей – надо её решить, если есть попап с формой – надо её заполнить. Мы заранее только прописываем в промпте для LLM какие вообще действия можно делать, и кодим блоки выполнения этих действий. А ходить по страницам и что на них делать – будет решать модель на основе их содержимого. Такая гулялка при должной реализации будет куда естественнее в плане поведения на сайтах, чем обычные боты.
Теперь немного о том, где и какие модели будем запускать в нашей демонстрации.
Подключение к LM Studio
В статьях к предыдущих конкурсах я уже неоднократно упоминал LM Studio в качестве локального способа запуска моделей и использования в ZennoPoster.
И в этот раз тоже будем подключаться к LM Studio – просто потому что это очень просто, локально и бесплатно.
Всё, что нам нужно сделать в самой программе – перейти на вкладку "Разработка", запустить сервер и выбрать модель (ну и скачать её предварительно, если раньше вы не пользовались LM Studio).
Что тут стоит упомянуть – модель крайне желательно выбирать ту, которая нативно поддерживает фичу Structured Output.
Не каждая из моделей поддерживает её, хотя и работать скорее всего будет и с обычной не сильно хуже. Отличие в том, что если для LLM заявлена поддержка Structured Output, значит она специально дообучалась под этот формат вывода, и значит что вероятность ошибки в ответах кратно снижается.
Про поддержку пишут обычно в карточке модели, например на том же HuggingFace.
Также, чем "умнее" и современнее модель, и чем больше у неё параметров – тем лучше она будет справляться с нашими задачами. Разумеется, речь не про использование самых лучших и тяжелых локальных моделей, скорее про баланс – выбирать модель так, чтобы она не сильно нагружала вашу рабочую машину/сервер, и чтобы не была слишком "глупой" чтобы типовые разметки и задачи.
Вот пример кода подключения и запроса к модели в LM Studio:
Запрос к модели в LM Studio:
// LM Studio BaseUrl
// http://localhost:1234/v1/chat/completions
string apiUrl = project.Variables["ApiUrl"].Value;
string model = project.Variables["Model"].Value;
string system = project.Variables["SystemPrompt"].Value;
string prompt = project.Variables["Prompt"].Value;
string schemaName = project.Variables["SchemaName"].Value;
string schemaJson = project.Variables["SchemaJson"].Value;
string formHtml = project.Variables["FormHtml"].Value;
// Вставляем взятую со страницы форму в промпт
prompt = prompt.Replace("<<<FORM_HTML>>>", formHtml);
// Собираем messages с промптом
var messages = new JArray
{
new JObject { ["role"] = "system", ["content"] = system },
new JObject { ["role"] = "user", ["content"] = prompt }
};
// Получаем schema object
var schemaObj = JObject.Parse(schemaJson);
// Формируем request object для запроса к модели
var requestObj = new JObject
{
["model"] = model,
["messages"] = messages,
["response_format"] = new JObject
{
["type"] = "json_schema",
["json_schema"] = new JObject
{
["name"] = schemaName,
["strict"] = "true",
["schema"] = schemaObj
}
},
["temperature"] = 0.2,
["stream"] = false
};
string body = requestObj.ToString(Formatting.None);
// POST-запрос к запущенной модели, получение ответа
var req = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(apiUrl);
req.Method = "POST";
req.ContentType = "application/json";
req.Headers["Authorization"] = "Bearer lm-studio";
using (var sw = new System.IO.StreamWriter(req.GetRequestStream()))
sw.Write(body);
string raw = String.Empty;
using (var resp = (System.Net.HttpWebResponse)req.GetResponse())
using (var sr = new System.IO.StreamReader(resp.GetResponseStream()))
raw = sr.ReadToEnd();
// Достаём ответ (content) из ответа API
var jo = JObject.Parse(raw);
// Здесь строка с JSON по схеме
string contentJson = (string)jo["choices"][0]["message"]["content"];
// Сохраняем в переменную проекта
project.Variables["jsonStructuredOutput"].Value = contentJson;
От сервера LM Studio приходит ответ модели, который мы помещаем в переменную проекта jsonStructuredOutput. Далее можно разбирать этот ответ, делать что нам нужно исходя из начальной задачи.
Что нужно отправлять модели
В примере подключения у нас используется несколько сущностей-заготовок, которые возможно не всем будет очевидно.
По-хорошему нам всегда надо отправлять 3 вещи.
- Промпт. Тестовое описание, что должна сделать модель. Промпт зависит от каждой задачи/кейса.
- Входные данные, которые надо обработать модели. Как правило, во многих кейсах это HTML либо отдельного элемента или целого блока на странице, либо код/DOM страницы целиком. Входные данные вставляем в промпт, например с помощью макроса и его замены.
- Схема JSON. Под каждую задачу/кейс надо составить структуру JSON, в который мы хотим принимать ответы от LLM. Это надо обязательно, т.к. иначе модель не будет знать в какой структуре отвечать, а нам не будет понятно, как разбирать её ответ. Если вы не знаете, как создать схему, посмотрите примеры ниже, или же сразу попросите помочь условный чатгпт или любую другую нейронку.
Ну а пайплайн у нас будет примерно такой, соответственно:
- Парсим в ZP обычными способами нужные блоки данных со страницы (или берём в переменную всю страницу).
- Отправляем модели Промпт+Данные+Схему с включённой опцией Structured Output.
- Парсим и заодно проверяем уверенность модели в ответе.
- Если модель не уверена в ответе, либо разбор JSON падает или нас не устраивает – просим модель починить и отправляем на доработку.
- Если с ответом сразу или после "починки" всё ок – делаем всё что нужно обычными средствами (например, на основе ответа модели кликаем сышкой по нужным местам на странице).
Подробнее пример реализации таких проверок и "починок" можно посмотреть в прикреплённом шаблоне.
Кейсы
Давайте рассмотрим конкретные кейсы использования Structured Output, а также примеры промптов, схем и кода для них.
1. Умное заполнение неизвестной формы
Допустим, вы делаете универсальный шаблон по заполнению любых форм заказа/контактов/регистрации.
Универсальный в том смысле, что заранее неизвестно, как форма будет выглядеть, какие поля иметь и не иметь.
В нашем случае нам надо брать весь блок form и отправлять модели, а она нам будет в JSON отвечать, какие поля и чем именно заполнять, а также куда тыкать для отправки формы. Чем заполнять – берём данные из текущего профиля ZennoPoster.
JSON:
{
"type": "object",
"properties": {
"fields": {
"type": "array",
"items": {
"type": "object",
"properties": {
"label": { "type": "string" },
"nameAttr": { "type": "string" },
"inputType": { "type": "string" },
"requiredGuess": { "type": "boolean" },
"valueSource": {
"type": "string",
"enum": ["profile.email","profile.phone","profile.firstName","profile.lastName","custom","skip"]
},
"value": { "type": "string" },
"xpath": { "type": "string" },
"confidence": { "type": "number" }
},
"required": ["valueSource","xpath","confidence"]
}
},
"submit": {
"type": "object",
"properties": {
"xpath": { "type": "string" },
"confidence": { "type": "number" }
},
"required": ["xpath","confidence"]
},
"confidence": { "type": "number" },
"warnings": { "type": "array", "items": { "type": "string" } },
"evidence": { "type": "array", "items": { "type": "string" } }
},
"required": ["fields","submit","confidence","warnings","evidence"]
}
HTML формы:
<<<FORM_HTML>>>
Данные профиля:
email = {project.Profile.Email}
phone = {project.Profile.Phone}
firstName = {project.Profile.FirstName}
lastName = {project.Profile.LastName}
Найди поля, которые имеет смысл заполнить пользователю.
- Игнорируй captcha/hidden/readonly/служебные поля
- Для каждого поля дай XPath и valueSource (из профиля или custom)
- XPath составляй только короткие уникальные, например: //input[@id='email']
- Найди XPath кнопки отправки формы (submit)
Верни только JSON по схеме.
<<<FORM_HTML>>>
Данные профиля:
email = {project.Profile.Email}
phone = {project.Profile.Phone}
firstName = {project.Profile.FirstName}
lastName = {project.Profile.LastName}
Найди поля, которые имеет смысл заполнить пользователю.
- Игнорируй captcha/hidden/readonly/служебные поля
- Для каждого поля дай XPath и valueSource (из профиля или custom)
- XPath составляй только короткие уникальные, например: //input[@id='email']
- Найди XPath кнопки отправки формы (submit)
Верни только JSON по схеме.
JSON:
{
"fields": [
{
"label": "Email",
"nameAttr": "email",
"inputType": "email",
"requiredGuess": true,
"valueSource": "profile.email",
"value": "",
"xpath": "//input[@type='email' or contains(@name,'mail')]",
"confidence": 0.86
},
{
"label": "First name",
"nameAttr": "first_name",
"inputType": "text",
"requiredGuess": false,
"valueSource": "profile.firstName",
"value": "",
"xpath": "//input[contains(@name,'first') or contains(@id,'first')]",
"confidence": 0.7
}
],
"submit": {
"xpath": "//button[@type='submit' or contains(.,'Send') or contains(.,'Отправить')]",
"confidence": 0.78
},
"confidence": 0.8,
"warnings": ["Проверьте, что форма не требует подтверждения чекбокса согласия (privacy/terms)."],
"evidence": ["input type=email найден", "button type=submit найден"]
}
C#:
// Ответ от LLM
string json = project.Variables["jsonStructuredOutput"].Value;
var jo = JObject.Parse(json);
double conf = (double)jo["confidence"];
var warnings = jo["warnings"]?.Select(x => (string)x).ToArray() ?? new string[0];
if (warnings.Length > 0)
project.SendInfoToLog("Предупреждения от LLM: " + string.Join(" | ", warnings), true);
// 1) Валидация уверенности модели в ответе
if (conf < 0.6)
{
project.SendInfoToLog("Низкая уверенность модели по форме. Повторно отправим более узкий HTML/уточним промпт.", true);
// Можгл вырезать более точный контейнер формы и повторить запрос.
// Если второй раз тоже плохо — fallback ниже.
throw new Exception("Выход по красной ветке");
}
var tab = instance.ActiveTab;
// 2) Заполнение полей в цикле
foreach (var f in jo["fields"])
{
string valueSource = (string)f["valueSource"];
if (valueSource == "skip") continue;
string xpath = (string)f["xpath"];
if (string.IsNullOrWhiteSpace(xpath)) continue;
string value = String.Empty;
switch (valueSource)
{
case "profile.email": value = project.Profile.Email; break;
case "profile.phone": value = project.Profile.Phone; break;
case "profile.firstName": value = project.Profile.Name; break;
case "profile.lastName": value = project.Profile.Surname; break;
case "custom": value = (string)f["value"]; break;
}
if (string.IsNullOrWhiteSpace(value)) continue;
project.SendInfoToLog($"valueSource: {valueSource} xpath: {xpath} value: {value}", true);
var el = tab.FindElementByXPath(xpath, 0);
if (el.IsNull || el.IsVoid) continue;
tab.FullEmulationMouseMoveToHtmlElement(el);
tab.FullEmulationMouseClick("left", "click");
project.SendInfoToLog($"SetValue: {value}", true);
// el.Click();
el.SetValue(value, "SuperEmulation");
}
// 3) Отправка формы
string submitXpath = (string)jo["submit"]["xpath"];
double submitConf = (double)jo["submit"]["confidence"];
var btn = tab.FindElementByXPath(submitXpath, 0);
// Fallback: если submit не найден — пробуем простую эвристику
if (btn.IsNull || submitConf < 0.55)
{
project.SendInfoToLog("Submit не найден/неуверенно. Fallback: ищу button[type=submit] и input[type=submit].", true);
btn = tab.FindElementByXPath("//button[@type='submit']|//input[@type='submit']", 0);
}
if (!btn.IsNull && !btn.IsVoid) btn.Click();
2. Умный парсер списка объявлений/постов
Похожая на предыдущую задачу, в плане того, что нужна универсальность.
С помощью LLM и Structured Output мы её и достигаем.
JSON:
{
"type": "object",
"properties": {
"listContainerXpath": { "type": "string" },
"itemXpath": { "type": "string" },
"fields": {
"type": "object",
"properties": {
"titleXpath": { "type": "string" },
"priceXpath": { "type": "string" },
"urlXpath": { "type": "string" },
"imageXpath": { "type": "string" }
},
"required": ["titleXpath","urlXpath"]
},
"pagination": {
"type": "object",
"properties": {
"nextPageXpath": { "type": "string" }
},
"required": ["nextPageXpath"]
},
"confidence": { "type": "number" },
"warnings": { "type": "array", "items": { "type": "string" } },
"evidence": { "type": "array", "items": { "type": "string" } }
},
"required": ["listContainerXpath","itemXpath","fields","pagination","confidence","warnings","evidence"]
}
HTML блока выдачи:
<<<LIST_HTML>>>
Нужно извлечь поля: title, price, url, image (если есть).
Дай XPath:
- listContainerXpath
- itemXpath (так, чтобы можно было получить коллекцию карточек)
- поля внутри карточки (titleXpath/priceXpath/urlXpath/imageXpath)
- пагинация: nextPageXpath
Верни только JSON по схеме.
<<<LIST_HTML>>>
Нужно извлечь поля: title, price, url, image (если есть).
Дай XPath:
- listContainerXpath
- itemXpath (так, чтобы можно было получить коллекцию карточек)
- поля внутри карточки (titleXpath/priceXpath/urlXpath/imageXpath)
- пагинация: nextPageXpath
Верни только JSON по схеме.
JSON:
{
"listContainerXpath": "//div[contains(@class,'catalog') or contains(@class,'results')]",
"itemXpath": ".//div[contains(@class,'card') or contains(@class,'item')]",
"fields": {
"titleXpath": ".//h2|.//h3|.//a[contains(@class,'title')]",
"priceXpath": ".//*[contains(@class,'price') or contains(.,'₽') or contains(.,'$')]",
"urlXpath": ".//a[@href][1]",
"imageXpath": ".//img[@src][1]"
},
"pagination": {
"nextPageXpath": "//a[contains(.,'Next') or contains(.,'Следующая') or contains(@aria-label,'Next')]"
},
"confidence": 0.73,
"warnings": ["priceXpath может цеплять лишний текст — лучше нормализовать regex’ом."],
"evidence": ["Найден повторяющийся паттерн карточек (div.card/div.item)"]
}
C#:
// Ответ от LLM
string json = project.Variables["jsonStructuredOutput"].Value;
var jo = JObject.Parse(json);
double conf = (double)jo["confidence"];
if (conf < 0.6)
{
project.SendInfoToLog("Низкий confidence по парсеру выдачи. Вырезаем более точный контейнер и повторяем.", true);
// Retry: вместо всего main — взять ближайший контейнер вокруг 5-10 карточек.
}
var tab = instance.ActiveTab;
string listXp = (string)jo["listContainerXpath"];
string itemXp = (string)jo["itemXpath"];
var listEl = tab.FindElementByXPath(listXp, 0);
if (listEl == null)
{
project.SendInfoToLog("Не найден контейнер выдачи. Fallback: пробуем искать карточки по глобальному XPath.", true);
listEl = tab.FindElementByXPath("//body", 0); // грубый fallback
}
string titleXp = (string)jo["fields"]["titleXpath"];
string priceXp = (string)jo["fields"]["priceXpath"];
string urlXp = (string)jo["fields"]["urlXpath"];
var items = listEl.FindChildrenByXPath(itemXp);
if (items.Count < 1)
{
project.SendInfoToLog("Не найдено карточек. Пробуем типовые XPath для карточек.", true);
items = tab.FindElementsByXPath("//article|//div[contains(@class,'card') or contains(@class,'item')]");
}
foreach (var it in items)
{
string title = it.FindChildByXPath(titleXp, 0)?.GetAttribute("innerText");
string url = it.FindChildByXPath(urlXp, 0)?.GetAttribute("href");
string price = it.FindChildByXPath(priceXp, 0)?.GetAttribute("innerText");
// Тут обычно: нормализация price regex'ом + запись в таблицу/файл
project.SendInfoToLog($"ITEM => {title} | {price} | {url}", false);
}
// Пагинация
string nextXp = (string)jo["pagination"]["nextPageXpath"];
var nextBtn = tab.FindElementByXPath(nextXp, 0);
if (!nextBtn.IsVoid) nextBtn.Click();
3. Гулялка с классификацией страниц
Тот самый кейс, который мы упоминали в начале статьи – что делать на странице решает LLM, в зависимости от содержимого этой самой страницы.
Тоже момент по универсальности. Не надо по шаблону тыкать те же проверки на капчу. Если капча на странице вдруг появится перед очередным действием – LLM нам об этом отрапортует и выберет ветку блока решения капчи в шаблоне, например.
JSON:
{
"type": "object",
"properties": {
"pageType": {
"type": "string",
"enum": ["ok","login","captcha","rate_limit","blocked","error","unknown"]
},
"recommendedAction": {
"type": "string",
"enum": ["continue","relogin","solve_captcha","change_proxy","wait_and_retry","retry","skip","stop"]
},
"confidence": { "type": "number" },
"warnings": { "type": "array", "items": { "type": "string" } },
"evidence": { "type": "array", "items": { "type": "string" } }
},
"required": ["pageType","recommendedAction","confidence","warnings","evidence"]
}
URL: <<<URL>>>
Title: <<<TITLE>>>
Text snippet:
<<<TEXT_SNIPPET>>>
Определи состояние страницы (ok/login/captcha/rate_limit/blocked/error/unknown)
и что делать дальше (continue/relogin/solve_captcha/change_proxy/wait_and_retry/retry/skip/stop).
Верни только JSON по схеме.
Title: <<<TITLE>>>
Text snippet:
<<<TEXT_SNIPPET>>>
Определи состояние страницы (ok/login/captcha/rate_limit/blocked/error/unknown)
и что делать дальше (continue/relogin/solve_captcha/change_proxy/wait_and_retry/retry/skip/stop).
Верни только JSON по схеме.
JSON:
{
"pageType": "rate_limit",
"recommendedAction": "wait_and_retry",
"confidence": 0.82,
"warnings": ["Похоже на временное ограничение. Не усугубляйте частыми ретраями."],
"evidence": ["фраза 'Too many requests'", "код/текст про ограничение запросов"]
}
C#:
// Ответ от LLM
string json = project.Variables["jsonStructuredOutput"].Value;
var jo = JObject.Parse(json);
string pageType = (string)jo["pageType"];
string action = (string)jo["recommendedAction"];
double conf = (double)jo["confidence"];
if (conf < 0.55)
{
project.SendInfoToLog("Классификация неуверенная. Fallback: простые проверки по ключевым словам.", true);
throw new Exception("Выход по красной ветке");
// Пример грубого fallback:
// if (text.Contains("captcha") || text.Contains("Cloudflare")) pageType="captcha";
// if (text.Contains("login") || text.Contains("войти")) pageType="login";
}
project.SendInfoToLog($"Тип страницы: {pageType}, действие: {action}, уверенность модели: {conf}", true);
switch (action)
{
case "continue":
// продолжаем основной сценарий
break;
case "relogin":
// перейти к блоку залогина
break;
case "solve_captcha":
// Решить капчу, или остановить/уведомить
break;
case "change_proxy":
// смена прокси, затем повтор
break;
case "wait_and_retry":
Thread.Sleep(15000);
// затем обновить страницу/повторить действие
break;
case "skip":
// пропустить текущую цель
break;
case "stop":
// остановить шаблон
throw new Exception("Stop by classifier");
}
project.Variables["action"].Value = action;
4. Извлечение контактов/реквизитов/соцсетей со страницы
Ещё одна задача по парсингу, но другого плана.
Тут кстати можно прикрутить дополнительное применение способностей LLM. Мы можем например просить модель не просто собирать телефоны со страницы, а сразу возвращать их в определённом формате (например XXX-XX-XX). Отпадает необходимость самостоятельно потом нормализовывать спаршенные данные.
JSON:
{
"type": "object",
"properties": {
"company": {
"type": "object",
"properties": {
"name": { "type": "string" },
"address": { "type": "string" },
"inn": { "type": "string" },
"ogrn": { "type": "string" }
},
"required": ["name","address"]
},
"contacts": {
"type": "object",
"properties": {
"emails": { "type": "array", "items": { "type": "string" } },
"phones": { "type": "array", "items": { "type": "string" } },
"socials": {
"type": "object",
"properties": {
"telegram": { "type": "string" },
"whatsapp": { "type": "string" },
"vk": { "type": "string" },
"instagram": { "type": "string" }
}
}
},
"required": ["emails","phones","socials"]
},
"confidence": { "type": "number" },
"warnings": { "type": "array", "items": { "type": "string" } },
"evidence": { "type": "array", "items": { "type": "string" } }
},
"required": ["company","contacts","confidence","warnings","evidence"]
}
URL: <<<URL>>>
HTML:
<<<CONTACTS_HTML>>>
Извлеки и нормализуй:
- company: name, address, inn, ogrn (если есть)
- contacts: emails[], phones[], socials{telegram,whatsapp,vk,instagram}
Верни только JSON по схеме.
HTML:
<<<CONTACTS_HTML>>>
Извлеки и нормализуй:
- company: name, address, inn, ogrn (если есть)
- contacts: emails[], phones[], socials{telegram,whatsapp,vk,instagram}
Верни только JSON по схеме.
JSON:
{
"company": {
"name": "Example Studio",
"address": "Москва, ул. Примерная, 10",
"inn": "7701234567",
"ogrn": ""
},
"contacts": {
"emails": ["info@example.com", "sales@example.com"],
"phones": ["+7 (999) 123-45-67"],
"socials": {
"telegram": "https://t.me/example",
"whatsapp": "",
"vk": "https://vk.com/example",
"instagram": ""
}
},
"confidence": 0.84,
"warnings": ["Телефон не приведён к E.164 — при необходимости нормализуйте."],
"evidence": ["Найдены mailto:", "Найдены ссылки на соцсети в футере"]
}
C#:
// Ответ от LLM
string json = project.Variables["jsonStructuredOutput"].Value;
var jo = JObject.Parse(json);
double conf = (double)jo["confidence"];
string name = (string)jo["company"]["name"];
string addr = (string)jo["company"]["address"];
var emails = jo["contacts"]["emails"].Select(x => (string)x).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToArray();
var phones = jo["contacts"]["phones"].Select(x => (string)x).Where(x => !string.IsNullOrWhiteSpace(x)).Distinct().ToArray();
if (conf < 0.6)
{
project.SendInfoToLog("Низкий уверенность по сбору контактов, прерываемся или пробуем спарсить по-другому", true);
// Тут можно начать заново, спарсив по другому XPath/регулярке страничку и снова передать LLM
throw new Exception("Выход по красной ветке");
}
// Пример: сохраняем в переменные/списки
project.Variables["CompanyName"].Value = name;
project.Variables["CompanyAddress"].Value = addr;
project.Lists["Emails"].AddRange(emails);
project.Lists["Phones"].AddRange(phones);
// Соцсети
string tg = (string)jo["contacts"]["socials"]["telegram"] ?? "";
project.Variables["Telegram"].Value = tg;
Тут мы затронули 4 простых кейса и примеры промптов и схем к ним. Однако, в прикреплённом шаблоне и видео упоминаются ещё несколько.
Надёжность и решение проблем
Выше мы уже упоминали, что хоть и модели с поддержкой Structured Output отлично справляются с соответствующими задачами, проверять ответы всегда надо.
Поэтому по-хорошему надо заранее предусмотреть возможные проблемы и пути их решения.
- Если модель помимо прочего создаёт для вас пути XPath – проверяйте их перед использований. Если по XPath нашлось 0 (или наоборот слишком много) элементов – просить модель починить.
- Если ваш код не может нормально спарсить JSON от модели – опять же отправляем модели обратно с просьбой починить.
- Если параметр собственной уверенности модели низкий (confidence) – пробуем по возможности изменить входные данные и отправляем модели на вторую попытку.
- Если какие-то из обязательных полей в JSON пустые или с некорректными данными – отправляем модели обратно с просьбой починить.
- Если ещё какие-либо проблемы, или не получается с N-ой попытки – придумываем запасной сценарий решения штатными средствами "по старинке", либо просто адекватно сворачиваем выполнение. Про штатные средства такой пример: допустим, у модели никак не получается составить нормальный XPath для конкретного поля. В этом случае можно продумать несколько заготовленных универсальных путей, и запускать их если модель не справляется.
В принципе, на этом и закончим. Областей применения для этой фичи в действительности несравнимо больше, и можно сильно упростить себе разработку шаблонов вот таким вот образом, плюс найти новые применения или даже ниши. Задачка вам на подумать.

Вложения
-
23,6 КБ Просмотры: 12
Последнее редактирование модератором:

