В процессе интеграции 1С с интернет-магазинами, прайс-листами и внешними веб-ресурсами программисты часто сталкиваются с необходимостью парсинга веб-страниц. Типичная задача — обойти список ссылок на товары и получить их наименования, которые обычно содержатся в заголовке страницы (теге title). Однако стандартный последовательный обход ссылок работает крайне медленно: обработка всего 60 страниц может занимать до 3-4 минут. В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем частые ошибки разработчиков и реализуем максимально быстрое и эффективное решение на платформе 1С:Предприятие.
Проанализируем пример кода, который часто пытаются использовать начинающие разработчики:
Если ЗначениеЗаполнено(АдресСтраницы) И Найти(АдресСтраницы, "http") > 0 Тогда
Соединение = Новый HTTPСоединение(АдресСайта);
Запрос = Новый HTTPЗапрос(АдресСтраницы);
Ответ = Соединение.Получить(Запрос);
Если Ответ.КодСостояния = 200 Тогда
Значение = Ответ.Заголовки; // Ошибка здесь
КонецЕсли;
КонецЕсли;
В данном примере разработчик пытается получить заголовок страницы через свойство Ответ.Заголовки. Выясним причину, почему этот код не возвращает название страницы и работает медленно:
Content-Type, кодировку, параметры кэширования, длину ответа и cookies). К названию страницы в формате HTML (тегу title) эти данные не имеют никакого отношения.Получить() без дополнительных параметров скачивает абсолютно все тело страницы (HTML-код, внутренние стили, скрипты). Если страница весит несколько мегабайт, процесс загрузки 60 страниц превращается в долгое ожидание.Для решения этой проблемы нам необходимо решить две задачи: минимизировать объем скачиваемых данных и обеспечить сверхбыстрое извлечение тега title из полученного текста страницы.
Зачем скачивать всю веб-страницу целиком, если тег title всегда находится в самом начале документа, внутри контейнера head? Мы можем запросить у сервера только первые несколько килобайт данных. Для этого используется стандартный HTTP-заголовок Range.
Рассмотрим подробнее, как применить этот метод в 1С:
// Укажем адрес сайта и параметры подключения
Сервер = "example.com";
Ресурс = "/index.php?productID=3803";
Соединение = Новый HTTPСоединение(Сервер, , , , , 10); // Таймаут 10 секунд
Запрос = Новый HTTPЗапрос(Ресурс);
// Запрашиваем только первые 4 килобайта (4096 байт) страницы
Запрос.Заголовки.Вставить("Range", "bytes=0-4096");
Ответ = Соединение.Получить(Запрос);
// Большинство современных серверов при получении заголовка Range возвращают статус 206 (Partial Content)
Если Ответ.КодСостояния = 200 Или Ответ.КодСостояния = 206 Тогда
ТекстНачалаСтраницы = Ответ.ПолучитьТелоКакСтроку();
// Переходим к парсингу полученного фрагмента
КонецЕсли;
Важное примечание: Использование заголовка Range позволяет сэкономить до 95% входящего трафика и сокращает время ожидания ответа сервера в разы.
Использование тяжелых объектов платформы, таких как ЧтениеHTML или ПостроительDOM, оправдано только тогда, когда нужно полностью распарсить сложный документ. Если нам требуется вытащить всего одну строку, построение DOM-модели в памяти будет избыточным и медленным.
В современных версиях платформы 1С:Предприятие (начиная с версии 8.3.23) появилась нативная поддержка регулярных выражений. Разберем по шагам, как извлечь заголовок с их помощью:
// Используем регулярное выражение для поиска содержимого тега title (без учета регистра)
ШаблонТега = "(?i)<title\b[^>]*>(.*?)</title>";
РегулярноеВыражение = Новый РегулярноеВыражение(ШаблонТега);
РезультатСопоставления = РегулярноеВыражение.Сопоставить(ТекстНачалаСтраницы);
Если РезультатСопоставления.Количество() > 0 Тогда
// Получаем значение первой группы захвата (текст внутри тегов)
ЗаголовокСтраницы = РезультатСопоставления[0].Группы[1].Значение;
ЗаголовокСтраницы = СокрЛП(ЗаголовокСтраницы);
Сообщить("Заголовок страницы: " + ЗаголовокСтраницы);
Иначе
Сообщить("Тег title не найден в полученном фрагменте.");
КонецЕсли;
Если ваша база работает на более старой версии платформы, регулярные выражения можно вызвать через COM-объект или применить быстрое вырезание подстроки стандартными функциями 1С. Посмотрим на пример реализации классического строкового парсинга:
// Быстрый поиск границ тегов без использования регулярных выражений
ТекстВВерхнемРегистре = ВРег(ТекстНачалаСтраницы);
ПозОткрывающегоТега = СтрНайти(ТекстВВерхнемРегистре, "<TITLE>");
Если ПозОткрывающегоТега > 0 Тогда
ПозЗакрывающегоТега = СтрНайти(ТекстВВерхнемРегистре, "</TITLE>", , ПозОткрывающегоТега);
Если ПозЗакрывающегоТега > 0 Тогда
НачалоТекста = ПозОткрывающегоТега + 7; // Длина тега "<title>"
ДлинаТекста = ПозЗакрывающегоТега - НачалоТекста;
ЗаголовокСтраницы = Сред(ТекстНачалаСтраницы, НачалоТекста, ДлинаТекста);
ЗаголовокСтраницы = СокрЛП(ЗаголовокСтраницы);
КонецЕсли;
КонецЕсли;
Даже если каждый отдельный запрос выполняется за 0.1 секунды, при последовательном обходе 1000 ссылок общее время ожидания все равно будет значительным. Основное время тратится на сетевое ожидание ответа от удаленного сервера. Чтобы решить эту проблему, применим распараллеливание с помощью фоновых заданий.
Проанализируем архитектуру параллельного парсинга:
Посмотрим на пример кода для запуска параллельной обработки:
// Подготовим массив пакетов со ссылками
МассивПакетов = Новый Массив; // Массив массивов со ссылками
// ... код заполнения пакетов по 10 ссылок ...
МассивЗаданий = Новый Массив;
Для Каждого ПакетСсылок Из МассивПакетов Цикл
ПараметрыЗапуска = Новый Массив;
ПараметрыЗапуска.Добавить(ПакетСсылок);
// Запуск фонового задания, выполняющего скачивание
Задание = ФоновыеЗадания.Выполнить("ПарсерСервер.ПолучитьЗаголовкиПакетом", ПараметрыЗапуска);
МассивЗаданий.Добавить(Задание);
КонецЦикла;
// Ожидаем завершения всех потоков (максимум 60 секунд)
Попытка
ФоновыеЗадания.ОжидатьЗавершения(МассивЗаданий, 60);
Исключение
Сообщить("Время ожидания выполнения потоков истекло.");
КонецПопытки;
Если целевой сайт представляет собой современное SPA-приложение (React, Angular, Vue), классическое HTTPСоединение вернет вам пустой каркас страницы, где тег title может быть пустым или содержать статичную фразу вроде "Загрузка...". Настоящий заголовок формируется на стороне клиента встроенным скриптом.
Для таких случаев у нас есть два пути решения:
Элементы.ИмяПоляHTML.Документ.title.ЗапуститьПриложение().Используя комбинацию ограничения объема загрузки (Range) и регулярных выражений, вы сможете обрабатывать десятки страниц в секунду даже в один поток, а применение фоновых заданий позволит поднять производительность парсинга на профессиональный уровень.