Зачем нужен парсинг в среде 1С?
Наверное, любой сотрудник службы маркетинга, имеющий дело с интернет-бизнесом, или аналитик, желающий знать положение своей фирмы на рынке или среди конкурентов, сталкивался с задачей сбора информации на различных сайтах по специфике своего бизнеса.
Обычно цель таких работ – мониторинг цен конкурентов, отслеживание новинок в ассортименте, получение новостей, сбор и аналитическая обработка любой другой информации, не являющейся интеллектуальной собственностью.
Когда за эти задачи берется «не айтишник», это превращается в часы и дни монотонной работы по копированию-вставке однотипной информации с сайтов, что не только само по себе плохо и не эффективно, но еще и приводит к ошибкам – можно вставить повторно одно и то же, случайно затереть или переместить вставленные данные и так далее.
А если эти данные необходимо ввести в 1С и связать с какими-то объектами учета, это требует дополнительных доработок для организации работы пользователя – новые формы, проверки ввода, выполнение нескольких обновлений конфигурации, трата времени на обучение пользователей и т.д.
В современных условиях перехода к цифровизации бизнеса и «четвертой промышленной революции» такой подход, конечно же, является тупиковым, не масштабируемым и не эффективным по всем параметрам. Поэтому необходимо внедрять решения, исключающие человеческий труд, который должен использоваться только для анализа собранных данных и другой «интеллектуальной» работы.
В общемировой практике такие задачи автоматического сбора данных из общедоступных источников существуют со времен появления Интернета и первых поисковых систем. Они называются «парсинг» или «скрапинг», а программы-парсеры можно реализовать на всех популярных языках и платформах. Примеры самых популярных инструментов для парсинга:
- Python: BeautifulSoup, Scrapy
- PHP: Simple HTML DOM, phpQuery
- JavaScript: jQuery, Cheerio, Osmosis
- Java: JSOUP, TagSoup
- .NET: Html Agility Pack
- C++ : htmlcxx, libhtml++
Так же их можно реализовать и на 1С, ведь в нем есть объекты HTTPСоединение, HTTPЗапрос и возможности работы со структурой html-документа (DOM). В сочетании с регламентными заданиями можно реализовать периодический парсинг без участия человека, а совместно с http-сервисами можно организовать еще и публикацию собранных данных внутри сети предприятия – для обмена между разными базами 1С или другими информационными системами.
Новые возможности в платформе
Начиная с версии 8.3.13 в 1С появились новые функции, упрощающие работу с html-документами, это, в частности, поиск по фильтрам, аналогичный отбору по селекторам в jQuery и перечисленных выше библиотеках. Это позволяет парсить веб-страницы на 1С, применяя всего лишь базовые основы CSS. Не нужно изучать и мучиться с XPath, перебором элементов или полноценно погружаться в веб-фреймворки и незнакомые технологии!
Остальные функции помогают очищать содержимое документа, когда нужно его получить целиком, но без лишних элементов. Например, сохранять статьи или новости в виде HTML с минимально необходимой разметкой или вообще только текст страницы или блока.
Все новые функции с кратким описанием:
Функция |
Краткое описание |
ДокументHTML.УдалитьПоКатегории() |
позволяет очистить документ от ненужных элементов, например скриптов, апплетов, фреймов и т.д. |
ДокументHTML.ИзвлечьТолькоТекст() |
позволяет получить текст внутри какого-либо элемента, без необходимости обходить и получать конкретный тег из структуры вложенных блоков. Идентичен свойству innerText в JavaScript |
ДокументHTML.НайтиПоФильтру() |
Функция поиска элементов по селекторам и тегам, основной инструмент парсинга |
ДокументHTML.УдалитьПоФильтру() |
Удаление элементов по определенным селекторам, можно применять для более гибкой очистки содержимого, если нужно скопировать документ целиком, но без некоторых участков |
Пример задачи парсинга
Рассмотрим практический пример задачи извлечения данных. Допустим, мы хотим написать свою конфигурацию для учета инвестиций в акции и другие инструменты. Но вся необходимая информация хранится на разных сайтах или доступна только через REST API. Придется парсить разные источники, чтобы получать данные об инструментах, новостях, показателях компаний и т.д. Преимущество реализации на 1С в том, что мы можем написать несколько парсеров для разных источников и представить сводку в удобном виде, используя преимущества быстрой разработки на 1С – базу данных, конструктор форм и все другие средства.
Возьмем за основу и начальную точку сайт «Тинькоф-инвестиции» и проанализируем структуру страницы с информацией о акциях – на примере Apple https://www.tinkoff.ru/invest/stocks/AAPL/. Выделим блоки с нужной информацией и их CSS-селекторы:
Блок информации |
Селектор |
Название акции |
Класс «SecurityHeaderPure__showName_1DvWf» |
О компании |
Класс «SecurityInfoPure__info_3Mgld», тег <p> внутри |
Сайт компании |
Класс «SecurityInfoPure__link_2lmEC», тег <SPAN> внутри него |
Логотип компании |
Класс «Avatar__image_3KvrS», ссылка в описании стиля: свойство background-image |
Здесь заметно, что имена классов имеют суффикс со случайным набором символов, но это мы легко обойдем.
Проверить корректность отбора можно в консоле разработчика любого браузера (кнопка F12), используя функции document.getElementsByClassName(), document.getElementById(), document.getElementsByTagName() или отбор через jQuery, который все еще часто применяется на многих сайтах: если вызов функции «$» не выдаст ошибки, можно проверять отборы по селекторам в виде $(“.classname”), $(“#element-id”) и т.д.
Создадим код для получения текста страницы:
Заголовки = Новый Соответствие;
Заголовки.Вставить("Origin", "https://www.tinkoff.ru");
Заголовки.Вставить("Referer", "https://www.tinkoff.ru/invest/stocks/"+ВРег(Тикер)+"/");
Заголовки.Вставить("User-Agent","Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36");
Запрос = Новый HTTPЗапрос("/invest/stocks/AAPL/", Заголовки);
Соединение = Новый HTTPСоединение("www.tinkoff.ru",,,,,, Новый ЗащищенноеСоединениеOpenSSL);
Ответ = Соединение.Получить(Запрос);
ТекстОтвета = Ответ.ПолучитьТелоКакСтроку();
Теперь подготовим объект документа для анализа и получения данных:
ЧтениеHTML = Новый ЧтениеHTML;
ЧтениеHTML.УстановитьСтроку(ТекстОтвета);
Построитель = Новый ПостроительDOM;
Док = Построитель.Прочитать(ЧтениеHTML);
Док.НормализоватьДокумент();
Дальше сформируем фильтр для передачи в функцию поиска по документу. Фильтр это конфигурация отбора в виде JSON-строки, и у нее есть два корневых элемента – type и value. Есть несколько типов и для каждого - несколько допустимых значений, полное описание вариантов есть в синтаксис-помощнике в разделе «Описание структуры JSON-конфигурации для отбора».
Такую структуру может быть неудобно каждый раз формировать вручную, если парсинг многоступенчатый и сложный, поэтому лучше сразу вынести это действие в отдельную функцию и пользоваться многократно.
В нашем случае – реализуем функцию для формирования фильтра поиска по тегу и классу элемента, для этого нужно выбрать тип «пересечение условий» («intersection») и добавить значение «value» как массив из трех условий – на равенство тега, наличие атрибута «class» и поиск по шаблону для значения атрибута:
Функция СформироватьФильтрПоКлассуТега(Тег, Класс, ТочноеСоответствие = Истина)
ВидПоиска = ?(ТочноеСоответствие = Истина, "valueequals", "valuematchesregex");
ИмяКласс = ?(ТочноеСоответствие = Истина, Класс, Класс+".+");
ПараметрыПоиска = Новый Структура("type,value", "intersection", Новый Массив);
ПараметрыПоиска.value.Добавить(Новый Структура("type,value", "elementname", Новый Структура("value,operation", Тег, "equals")));
ПараметрыПоиска.value.Добавить(
Новый Структура("type,value", "hasattribute",
Новый Структура("value,operation", "class", "nameequals")));
ПараметрыПоиска.value.Добавить(Новый Структура("type,value","hasattribute",
Новый Структура("value,operation", ИмяКласс,ВидПоиска)));
Запись = Новый ЗаписьJSON;
Запись.УстановитьСтроку();
ЗаписатьJSON(Запись, ПараметрыПоиска);
Фильтр = Запись.Закрыть();
Возврат Фильтр;
КонецФункции
Здесь в зависимости от флага «ТочноеСоответствие» используется два варианта отбора – точное равенство или проверка по регулярному выражению. Это позволяет задавать сложные шаблоны поиска селекторов. В нашем случае при неточном соответствии в переменной ИмяКласс помещаем переданное значение и добавляем символы «.+», что означает «1 или более любых символов» после указанного значения.
Теперь мы можем формировать любые фильтры для выбора элементов и извлекать из них информацию. Оформим парсинг в виде функции, возвращающей структуру с полученными данными страницы:
Функция ПарсингСтраницыАкции(Док)
Результат = Новый Структура("Ошибка,Результат", Ложь);
Данные = Новый Структура("ОписаниеКомпании, СайтКомпании, СсылкаНаКартинку");
// Описание фирмы
Фильтр = СформироватьФильтрПоКлассуТега("div","SecurityInfoPure__wrapper_", Ложь);
МассивУзлов = Док.НайтиПоФильтру(Фильтр);
Если МассивУзлов.Количество() > 0 Тогда
Узел = МассивУзлов[0];
УзелТекстОписания = Узел.ДочерниеУзлы[0];
Данные.ОписаниеКомпании = УзелТекстОписания.ТекстовоеСодержимое;
Если Узел.ДочерниеУзлы.Количество() > 1 Тогда
УзелСсылка = Узел.ДочерниеУзлы[1];
Данные.СайтКомпании = УзелСсылка.ПервыйДочерний.Гиперссылка;
КонецЕсли;
КонецЕсли;
Результат.Результат = Данные;
Возврат Результат;
КонецФункции
Здесь мы сочетали получение узла по селектору и получение дочерних узлов из DOM-структуры вместо нескольких выборок по селектору. В простых случаях это может быть удобнее.
Парсинг картинок
Теперь рассмотрим пример получения изображения и сохранения его в 1С. Адрес картинки с логотипом компании в нашем случае хранится в стиле внутри тега span и его нужно получать через атрибуты узла.
Используем нашу функцию и найдем узел с картинкой, затем извлечем значение его атрибута «style», убрав из начала строки имя CSS-свойства «background-image» и строку «static.tinkoff.ru/».
После этого получим картинку как двоичные данные обычным GET-запросом и сохраним во временное хранилище:
// Картинка
Фильтр = СформироватьФильтрПоКлассуТега("span","Avatar__image_",Ложь);
МассивУзлов = Док.НайтиПоФильтру(Фильтр);
Если МассивУзлов.Количество() > 0 Тогда
Узел = МассивУзлов[0];
Для каждого Атр Из Узел.Атрибуты Цикл
Если Атр.ИмяУзла = "style" Тогда
АдресКартинки = Сред(МассивУзлов[0].Атрибуты[1].ЗначениеУзла, 41);
АдресКартинки = Лев(АдресКартинки, СтрДлина(АдресКартинки)-1);
Запрос = Новый HTTPЗапрос(АдресКартинки, Заголовки);
Соединение = Новый HTTPСоединение("static.tinkoff.ru",,,,,, Новый ЗащищенноеСоединениеOpenSSL);
Ответ = Соединение.Получить(Запрос);
Картинка = Ответ.ПолучитьТелоКакДвоичныеДанные();
Данные.СсылкаНаКартинку = ПоместитьВоВременноеХранилище(Картинка, УникальныйИдентификатор);
КонецЕсли;
КонецЦикла;
КонецЕсли;
Ресайз картинки
Добавим еще один шаг, не относящийся к парсингу, но который поможет при выводе списка ценных бумаг на форме – уменьшим полученное изображение до размера 36*36, чтобы можно было поместить его в динамический список. Для этого используем встроенный в систему компонент «WIA.ImageFile» и его возможности трансформации изображений. Для удобства напишем функцию, которой можно передать, как ранее полученную ссылку на картинку во временном хранилище, так и двоичные данные:
Функция СгенерироватьМиниатюру(СсылкаНаКартинку = "", ДвДанные = Неопределено, УИД)
// сохраняем в файл для обработки
Если ЭтоАдресВременногоХранилища(СсылкаНаКартинку) Тогда
Картинка = ПолучитьИзВременногоХранилища(СсылкаНаКартинку);
Иначе
Картинка = ДвДанные;
КонецЕсли;
ВрФайл = ПолучитьИмяВременногоФайла("png");
Картинка.Записать(ВрФайл);
Изобр = Новый COMОбъект("WIA.ImageFile");
Изобр.LoadFile(ВрФайл);
ОбработкаИзобр = Новый COMОбъект("WIA.ImageProcess");
ОбработкаИзобр.Filters.Add(ОбработкаИзобр.FilterInfos("Scale").FilterID);
ОбработкаИзобр.Filters(1).Properties("MaximumWidth").Value = 36;
ОбработкаИзобр.Filters(1).Properties("MaximumHeight").Value = 36;
// сохраняем рузультат и возвращаем адрес
ВрФайл = ПолучитьИмяВременногоФайла("png");
Изобр = ОбработкаИзобр.Apply(Изобр);
Изобр.SaveFile(ВрФайл);
Возврат ПоместитьВоВременноеХранилище(Новый ДвоичныеДанные(ВрФайл), УИД);
КонецФункции
В результате мы получим такой список акций:
Таким образом, мы получили данные из определенных блоков страницы: текст, ссылку и изображение, которые теперь можем использовать для создания объекта «Ценная бумага» и сохранить его в разработанной конфигурации:
Парсинг = ПарсингСтраницыИнструмента(Тикер, ДанныеПоИнструменту.Результат.Вид, ГУИД);
О = Справочники.ЦенныеБумаги.СоздатьЭлемент();
ЗаполнитьЗначенияСвойств(О, ДанныеПоИнструменту.Результат);
О.Наименование = Тикер;
О.ОписаниеКомпании = Парсинг.Результат.ОписаниеКомпании;
О.СайтКомпании = Парсинг.Результат.СайтКомпании;
О.Картинка = Новый ХранилищеЗначения(ПолучитьИзВременногоХранилища(
Парсинг.Результат.СсылкаНаКартинку));
О.КартинкаМал = Новый ХранилищеЗначения(ПолучитьИзВременногоХранилища(
Парсинг.Результат.СсылкаНаМалКартинку));
О.Записать();
УдалитьИзВременногоХранилища(Парсинг.Результат.СсылкаНаКартинку);
УдалитьИзВременногоХранилища(Парсинг.Результат.СсылкаНаМалКартинку);
Дальнейшее развитие парсинга средствами 1С
Парсинг динамических сайтов
В последнее время получили большое распространение сайты на основе фреймворков React, Vue, Angular, суть которых – построение и модификация структуры DOM-документа на стороне браузера, так называемые Single Page Application (SPA). Проблема парсинга таких сайтов в том, что в ответ на прямой http-запрос приходит только «скелет» сайта из одного тега DIV и большой объем скриптов, которые формируют всю структуру, используя движок браузера.
Для обхода этой проблемы используются так называемые headless-браузеры, которые могут исполнять скрипты и получать конечный сформированный документ, в node.js это, например, пакеты Nighmare или Puppeeter. Либо можно использовать инструмент для тестирования веб-страниц Selenium.
В 1С есть похожее решение – встроенный браузер в компоненте «ПолеHTMLДокумента», но в таком случае парсинг придется запускать вручную в режиме предприятия.
Можно предположить, что автоматизировать этот процесс можно с применением Vanessa Automation, но пока на практике я это не проверял.
Массированный парсинг
При необходимости обрабатывать большое количество страниц или делать это с определенной периодичностью, можно задействовать механизм регламентных заданий и выполнять сбор информации многопоточно и/или по расписанию. Этот механизм должен использоваться и в случае ограничений у сайта на число запросов – при этом можно сочетать регламентное задание с периодической сменой прокси, создав для их учета отдельный регистр.
Вынос парсера в расширение
Для уменьшения вмешательства в основную конфигурацию предприятия, можно реализовать парсер целиком в виде расширения, создав отдельную подсистему, справочники и регистры для хранения данных.
При необходимости привязки данных к объектам основной конфигурации можно сохранять их в дополнительные реквизиты или реализовывать собственные представления данных в виде обработок или отчетов в расширении.
Заключение
В целом, 1С сделала еще один шаг навстречу миру веб-технологий, хоть и снова своим способом – вместо поддержки выбора элементов напрямую по любым CSS-селекторам, реализовала его через JSON-фильтры, что в принципе можно обернуть в свою функцию-декоратор и использовать на разных задачах.