Парсинг сайтов из 1С на примере ломбарды.рф с помощью XPATH для ДокументDOM

На всякую хитрую гайку всегда найдется болт с резьбой (с)

ПАРСИНГ САЙТОВ НА 1С

Всем добрый промежуток времени между задачами! Вот и мне прилетела задачка с просьбой вытащить с вышеуказанного сайта данные о ломбардах. Поначалу задача показалась мне крайне тривиальной, но, как оказалось, за пятнадцать минут ее не решить, а в вылезших на поверхность мелочах скрывалось десяток различных демонов. Полагаю, что демонов этих вполне хватит на статью, которая, надеюсь, будет интересна местным (и не только) читателям.

ВВЕДЕНИЕ

Обычно для парсинга сайта нужно просто взять страницу и найти на ней определенные теги. Совсем юные дарования просто читают строки одну за другой и проверяют их на наличие какой-то комбинации символов, после которой они ожидают увидеть те или иные полезные данные. Более взрослые товарищи используют для этого XPath.

Мне пришлось использовать следующие объекты: ЧтениеJSON, ЧтениеXML, HTTPСоединение и HTTPЗапрос ну и ПостроительDOM, с помощью которого мы будем парсить через XPath.

Но давайте по-порядку.

С ЧЕГО НАЧАТЬ?

Современный сайт — это не просто сгенерированная чем-то веб-страница — это веб-приложение с AJAX и прочими динамическими штучками. Для того, чтобы начать, нужно определиться, что перед нами за сайт и как он хранит данные.

Самое простое — это посмотреть код сайта, который пришел с сервера и отобразился у нас в виде веб-страницы. Если посмотреть на указанный сайт, то видно, что список интересующей нас информации не отдается весь целиком. Но давайте разберем все предельно подробно.

Итак, загрузив сайт ломбарды.рф мы видим следующую картину:

Здесь мы видим, что нам доступна только маленькая часть списка. За остальными нужно лезть через "Еще результаты".

Если мы посмотрим код, то по этой кнопке дергается сервис (data-url="/lombards/load_more.php?category=&sort=&minloan=&maxloan=&region="). Если мы откроем в браузере эту ссылку, то увидим вот такую интересную штуку:

Как подсказывает нам ({"content":"
) в начале строки — это JSON. 1С умеет его читать, поэтому давайте начнем с простого — создадим HTTP-запрос и обработаем ответ.

СОЗДАНИЕ HTTP-СОЕДИНЕНИЯ, ВЫЗОВ ЗАПРОСА И ОБРАБОТКА ОТВЕТА

Для создания HTTP-соединения и запроса в 1С есть простые объекты, которые прямо так и называются:

  С = Новый HTTPСоединение(Адрес);
З = Новый HTTPЗапрос(Урл);

После того, как мы прочитали JSON и распарсили его, получили такой вот объект:

В объекте есть два поля: content и remaining, в первом находится HTML страницы, а во втором — количество оставшихся элементов.

Давайте попробуем засунуть значение в ДокументDOM, чтобы можно было написать к нему XPath:

 П = Новый ПостроительDOM;
Х = Новый ЧтениеXML;
Х.УстановитьСтроку(СС["content"]);
ДОМ = П.Прочитать(Х);
Р = Новый РазыменовательПространствИменDOM(ДОМ);
Результат = ДОМ.ВычислитьВыражениеXPath(".", ДОМ, Р, ТипРезультатаDOMXPath.Любой);

Итак, что тут происходит? Я прочитал JSON в соответствие СС, после чего создал построитель ДОМ, которым прочитал XML из СС["content"]. Но у меня вывалилась первая ошибка:

{ВнешняяОбработка.ЧтениеЛомбардов.Форма.Форма.Форма(20)}: Ошибка при вызове метода контекста (Прочитать)
    ДОМ = П.Прочитать(Х);
по причине:
Ошибка разбора XML:  — [8,41]
Фатальная ошибка:
Opening and ending tag mismatch: img line 7 and a

Что там у нас в 7-й строке? Тег img, который не закрывается!

<img src="http://xn--80abkzflr3g.xn--p1ai/upload/iblock/74a/74a8c12d4c7acf804a0812b501bd0d5a.jpg" alt="">

Да, мы можем прочитать данные в ДокументHTML вместо DOM, но тогда нам не будет доступен XPath. Также мы не можем просто так взять и поправить все ">" на "/>" — есть такие теги, которые содержат внутренние элементы. Надеюсь Вы теперь понимаете, почему почтенные веб-разработчики просят соблюдать стандарт и не писать "<br>" вместо "<br />" (кстати, не стоит путать стандарт XML и нечто от 1С об именовании переменных).

Благо, что у нас в img всегда есть alt, по которому мы сможем узнать, что тег надо закрыть. Итак, давайте исправим это:

 Ст = СтрЗаменить(СС["content"], "alt="""">", "alt="""" />");

Но ничего не вышло:

Ошибка разбора XML:  — [39,1]
Фатальная ошибка:
Extra content at the end of the document

Что на этот раз? Тут все просто — 1С не может прочитать неполный документ, т.е. все теги документа должны быть в одном корневом контейнере. Исправить это нетрудно:

 Ст = "<main>" + СтрЗаменить(СС["content"], "alt="""">", "alt="""" />") + "</main>";

В итоге при чтении XML в DOM у нас пока больше нет ошибок. На том же PHP у меня нет ошибок сразу — я могу любую ересь в него прочитать и применить к прочитанному XPath. Но это так — лирическое отступление.

XPATH-ВЫРАЖЕНИЯ В 1С

Для того, чтобы применить ограниченный функционал XPath в 1С прежде всего нам нужен ДокументDOM и РазименовывательПространствИменDOM. Если со смыслом первого объекта как-то можно смириться, то вникнуть в смысл второго у меня пока не получается — я просто инициализирую его через документ — и все:

 Р = Новый РазыменовательПространствИменDOM(ДОМ);
Результат = ДОМ.ВычислитьВыражениеXPath("//div[@class='item-info']", ДОМ, Р);

В результате мы получим объект с типом "РезультатXPath". Для получения элемента нам нужно просто вызвать его функцию "ПолучитьСледующий()":

 Пока Истина Цикл
Узел = Результат.ПолучитьСледующий();
Если Узел = Неопределено Тогда Прервать;
КонецЕсли;

// какой-то полезный код
КонецЦикла;

У нас на странице будет 5 элементов, которые мы получили с помощью запроса "//div[@class=’item-info’]". Мы выбрали все элементы "div" у которых атрибут "class" равен "item-info".

Итак, мы получили элементы XML, в которых содержатся имена и адреса ломбардов. Можно лазить за ними через всю эту иерархию ДОМ’а, а можно просто применить XPath к указанным узлам. Если посмотреть внимательно на файл, то можно увидеть, что имя ломбарда содержится в div’е с классом "item-info__title", там же и ссылка на страницу с телефоном (в действительности — на ее редирект). А адрес находится в теге "<address>". Давайте напишем для них XPath-выражения:

 Результат1 = ДОМ.ВычислитьВыражениеXPath("//div[@class='item-info']", ДОМ, Р);

Пока Истина Цикл
Узел = Результат1.ПолучитьСледующий();
Если Узел = Неопределено Тогда Прервать;
КонецЕсли;
Результат2 = ДОМ.ВычислитьВыражениеXPath("./div[@class='item-info__title']/h4/a/text()", Узел, Р, ТипРезультатаDOMXPath.Строка);
Результат3 = ДОМ.ВычислитьВыражениеXPath("./div[@class='item-info__title']/h4/a/@href", Узел, Р, ТипРезультатаDOMXPath.Строка);

Результат = Результат + "
|ИМЯ: " + Результат2.СтроковоеЗначение + "
|Урл: " + Результат3.СтроковоеЗначение;
КонецЦикла;

В результате мы получим что-то такое:

ИМЯ:
                        VIPLOMBARD                    
Урл: /lombards/yuvelirnye/10370
ИМЯ:
                        Chronoland                    
Урл: /lombards/yuvelirnye/10374
ИМЯ:
                        AUTO-PERSPECTIVA                    
Урл: /lombards/avtomobiley/10442
ИМЯ:
                        Кредиты Населению Автоломбард                    
Урл: /lombards/avtomobiley/10443
ИМЯ:
                        Гольфстрим (Профсоюзная, 127Б)                    
Урл: /lombards/avtomobiley/10504

Дальше нам придется прочитать данные по ссылке и получить уже из обычного HTML информацию об адресах, телефонах и прочем.

ИТОГ

В ходе парсинга я столкнулся со следующими проблемами:

  1. Редирект. По указанной в Урл странице находится страница с перманентным редиректом. Взять из нее адрес не составит никакого труда. а определить ее можно по 301-й ошибке в ответе веб-сервера.
  2. Наличие в комментарии двух минусов подряд ("—"). Такой комментарий ДОМ 1С не читает и вы получите эксепшн. Как бороться? СтрЗаменить — наше все.
  3. Закрывающийся тег </b> без открывающегося. Просто удалял все <b> и </b>.
  4. Тег <br> — менял на <br />, но можно просто удалить.
  5. "&" в текстовых полях — надо менять на "&amp;", иначе 1С такой XML не прочитает.
  6. Ну и не надо читать весь HTML — начните с <body> и им же заканчивайте (для этого сайта лучше начать и закончить тегом <main> — он там как раз есть).
  7. Вишенка на торте — скрытый параметр, подставляемый в урл JSON-а, получаемого PHP-скриптом. Найдите его сами — в качестве домашнего задания. Подскажу — можно воспользоваться консолью хрома и запустить сбор данных о производительности — там будут все запросы, а в них, в свою очередь, будут все параметры.
  8. Удачи!

33 Comments

  1. bonv

    (0)

    1. Редирект. По указанной в Урл странице находится страница с перманентным редиректом. Взять из нее адрес не составит никакого труда. а определить ее можно по 301-й ошибке в ответе веб-сервера.

    Используйте

    https://infostart.ru/public/709325/

    и не будет проблем с редиректами

    Reply
  2. webester

    (1)Мне нравится простой пример с ИТС https://its.1c.ru/db/metod8dev#content:5574:hdoc обрабатывает перенаправления указывает на типы ошибок

    Reply
  3. bonv

    (2) все хорошо, пока не захочется парсить сайты, требующие предварительной авторизации

    Reply
  4. webester

    (3)Это на тему перенаправления. Использовать только ради него Коннектор, как из пушки по воробьям. Библиотека сама по себе отличная.

    Reply
  5. Поручик

    Редирект в ответе HTTP и не только http://forum.aeroion.ru/topic749.html

    Reply
  6. starik-2005

    Редирект на столько прост, что я о нем даже говорить не стал — скучно! Особенно когда редирект внутри одного домена

    О = С.Ролучить(З);
    Если О.КодОтвета МЕЖДУ 300 и 399 Тогда
    З = Новый HTTPЗапрос(ПолучитьУрл(О));
    О = С.Получить(З);
    КонецЕсли;
    
    Reply
  7. s_vidyakin

    лучше поднапрячься и изучить как это делается в цивилизованном мире — nodejs + axios + cheerio https://nuancesprog.ru/p/3102/ делов на полчаса ))

    XPath это непонятная хрень, иногда работает иногда нет, на определенных тегах/классах/фазах Луны

    Reply
  8. starik-2005

    (8) ну это как с регулярками — у меня работает, а у пользователей компьютера не всегда, хотя у нас даже аналитики уже регулярки освоили и дату в локальном формате могут заменять на xml- дату, и всегда работает)))

    Reply
  9. starik-2005

    (8) кстати, вывод автора неутешительный: «мы можем извлекать данные только из статических сайтов». Я же привел пример извлечения из динамического сайта как раз — основная хитрость тут — это разобраться с источниками данных.

    Также если посмотреть на статью внимательно, то понятно становится, что ничего нового — тот же запрос к HTML-ДОМ’у, преобразованному в виртуальный ДОМ с помощью компонента node.

    Reply
  10. s_vidyakin

    (10) там есть вторая часть, для динамических сайтов — https://nuancesprog.ru/p/3125/.

    Конечно тот же DOM, но удобнее, у XPath язык отличается от стандартных CSS селекторов

    Да и скорость еще под вопросом у XPath, думаю он ляжет на больших объемах

    Reply
  11. starik-2005

    (11) так я и не спорю, что 1С для парсинга сайтов подходит весьма условно. Во второй статье очень хороший и интересный подход через кликер для динамики — реально вещь! )))

    Reply
  12. s_vidyakin

    (9) пробовал получать вложенные теги в определенном теге, указываю типа «.class1 > .class2» — НОЛЬ элементов. В консоли браузера все выбирается. Пришлось выбирать глобальным поиском по class2, но они там и в других местах были, логика усложнилась проверками. Больше с XPath не связываюсь

    Возможно было бы более интересно если бы написали библиотеку на OScript типа cheerio и сделали обзор )

    Reply
  13. starik-2005

    (13)

    пробовал получать вложенные теги в определенном теге, указываю типа «.class1 > .class2» — НОЛЬ элементов

    Если речь об 1С, то я даже уточнил в статье, что XPath в ней ограничен. Хотя //div[@class=’c1′]/div[class=’c2′]/text() — вполне рабочая конструкция даже для 1С.

    Reply
  14. fr13

    Материал полезен для новичков в этой теме, но зачем же так называть переменные?

    Reply
  15. starik-2005

    (15)

    но зачем же так называть переменные?

    Переменные названы просто отлично. Так, как в примерах для других языков программирования.

    ЗЫ: Я, кстати, тоже с Вологды.

    Reply
  16. TODD22

    (16)Вы все переменные называете как «счётчик цикла» ?

    Reply
  17. starik-2005

    (17)

    Вы все переменные называете как «счётчик цикла» ?

    Мы все переменные называем ровно так, чтобы было понятно, что они есть.

    Reply
  18. TODD22

    (18)

    Да я заметил «О», «С», «З»… очень содержательные имена.

    Reply
  19. starik-2005

    (19) для понимания достаточно?

    Reply
  20. premierex

    (20) Вот имя переменной ПроцессорВыводаРезультатаКомпоновкиДанныхВКоллекциюЗначений вполне себе содержательное. Для понимания достаточно, даже если с места её объявления прокрутить несколько сотен строк, смысл всё равно будет понятен.

    А вот смысловую нагрузку «О», «С», «З» можно понять, если текст, где происходит объявление этой переменной и где время её жизни заканчивается, находятся на одной экранной страннице. Не экономьте время на читабельности кода! Через некоторое время самому сложно будет этот код править.

    Reply
  21. starik-2005

    (21) кто не умеет — тому пропроцессор компоновки как ни назови — все бессмысленно. А кто умеет — тому хоть горшком (наролная мудрость, кстати, а вряд ли мы умнее народа по-одному)

    Reply
  22. premierex

    (0) Информация по теме: в версии платформы 8.3.13 у объекта ДокументHTML появилась функция НайтиПоФильтру(Фильтр). С её помощью можно получить требуемые узлы, выполнив следующий код:

     ЧтениеHTML = Новый ЧтениеHTML;
    ЧтениеHTML.УстановитьСтроку(ТекстHTML);
    Построитель = Новый ПостроительDOM;
    ДокументHTML = Построитель.Прочитать(ЧтениеHTML);
    ЧтениеHTML.Закрыть();
    ДокументHTML.НормализоватьДокумент();
    Фильтр = «{
    | «»type»»: «»intersection»»,
    | «»value»»:
    | [
    |  {
    |   «»type»»: «»elementname»»,
    |   «»value»»:
    |   {
    |    «»value»»: «»div»»,
    |    «»operation»»: «»equals»»
    |   },
    |  }
    |  ,
    |  {
    |   «»type»»: «»hasattribute»»,
    |   «»value»»:
    |   {
    |    «»value»»: «»class»»,
    |    «»operation»»: «»nameequals»»
    |   }
    |  }
    |  ,
    |  {
    |   «»type»»: «»hasattribute»»,
    |   «»value»»:
    |   {
    |    «»value»»: «»item-info»»,
    |    «»operation»»: «»valueequals»»
    |   }
    |  }
    | ]
    |}»;
    МассивУзлов = ДокументHTML.НайтиПоФильтру(Фильтр);
    

    Показать

    Ну а затем уже анализировать дочерние элементы. Можно с помощью XPath, а можно и без.

    Reply
  23. fr13

    (16) мы пересекались в Вологде года 4 назад. Магазин автозапчастей. Я тогда работал в своем первом франче, а Вы были как приглашенный московский спец ))

    Reply
  24. starik-2005

    (24) я кстати к ним на днях заеду — чисто поглядеть. А по поводу приглашенного — это мои клиенты с 2004-го года.

    Reply
  25. starik-2005

    (24) кстати, переработал и дополнил их программно-аппаратную часть — вот что получилось: https://infostart.ru/public/1051601/

    Reply
  26. fr13

    (26) Да, я прочитал этот материал ) сразу понял про кого речь ))

    Reply
  27. ture

    Jsoup, и не забивай голову ерундой

    Reply
  28. starik-2005

    (28)

    Jsoup

    А что там у него с динамическим контентом?

    Reply
  29. ture

    (29) работает

    Reply
  30. starik-2005

    (30) куда нажимать?

    Reply
  31. amd1986

    Брр. Кто мешает использовать 1C 8.3.14 и javascript?

    Reply
  32. starik-2005

    (32)

    Кто мешает использовать 1C 8.3.14 и javascript?

    А как JS заставить работать в регламентном задании? Мне вот реально надо (бывал как-то в Кадникове ВО в детском доме — давно это было).

    Reply
  33. amd1986

    (33) В фоновом наверно не получится(не проверял). Страница должна прогрузиться на форме, чтобы с ней можно было работать через ноды.

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *