3 место Навигация бота на web-страницах

Discussion in 'Девятый конкурс статей' started by LaGir, May 15, 2018.

  1. LaGir

    LaGir Client

    Joined:
    Oct 1, 2015
    Messages:
    181
    Likes Received:
    515
    Приветствую всех! :-)

    В этой статье мы рассмотрим основные принципы, типовые задачи, полезные сниппеты по теме навигации и взаимодействия с web-страницами.

    Чтобы статья была более полной и подходящей и для самых новичков, и был какие-то переходы от простого к сложному, решил включить обзор азов навигации и взаимодействии со страницами. Неожиданно он получился сравнительно объемным, пардон за столько писанины.

    Исходя из этого статья разделена на 2 части: первая для новичков, вторая для всех остальных.

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


    Основы навигации и взаимодействия с web-страницами

    Начнём с того, как вообще строится взаимодействие бота со страницами, открытыми в браузере ZennoPoster, как оно устроено.

    Когда мы переходим на какую-либо страницу в браузере (или, когда наш бот активирует экшен «Табы --> Переход на страницу») – браузер получает исходный код этой странички от сервера, на основе которого выстраивает так называемую DOM-модель (Document Object Model, объектная модель документа/страницы), а далее и сам визуальный вид страницы, который мы привыкли видеть.

    DOM-модель – именно та часть, которая нам понадобится в построении взаимодействия нашего бота и браузера. Для просмотра DOM в ProjectMaker предназначено специальное окно под названием «Дерево элементов».

    1.1.png

    Название окна очень точно отражает суть DOM-модели, которая по сути и представляет собой набор всех элементов страницы, упорядоченных в иерархическую структуру.

    На скриншоте мы видим выделенный элемент под именем «form» (имя элемента, кстати, равно html-тегу этого элемента). Визуально на странице этот элемент-форма состоит из поля ввода и кнопки «Найти», которые в свою очередь тоже являются элементами (элемент с тегом «input» и элемент с тегом «button»). То есть, элементы могут быть вложенные друг в друга.

    Если развернуть все элементы в дереве (кнопка «+» слева от названия элемента) – то, как правило, окажется, что на страницы сотни, а то и тысячи различных элементов.

    Как можно использовать дерево элементов?

    В окне дерева элементов можно быстро найти нужный элемент на странице, для того чтобы дальше что-либо с ним сделать.

    Например, нам нужно найти поле ввода, чтобы вписать в него какое-либо слово. Полями ввода являются элементы с тегом «input» - благодаря этому знанию можем быстро найти этот элемент в дереве. Однако, так как в дереве обычно сотни и тысячи элементов, проще исходить из обратного – найти элемент на странице визуально, а потом автоматически перейти к нему в дереве.

    Сделать это можно, кликнув правой кнопкой мыши по элементу в браузере, выбрать из контекстного меню пункт «Исследовать элемент» – сразу после этого в дереве автоматически выберется этот же элемент. Примечание: визуально на странице элементы дерева часто «накладываются» друг на друга, поэтому данное действие может выбрать не тот элемент. В этом случае просто нужно поискать нужный элемент среди соседей выделенного, он обычно находится рядом.

    1.2.png

    У каждого элемента есть набор параметров (атрибутов), которые отличают его ото всех других элементов на странице. Эти атрибуты можно посмотреть в окне под названием «Свойства элемента».

    1.3.png

    В этом окне отображаются атрибуты того элемента, который выделен в данный момент в дереве элементов. У каждого атрибута есть название (например, «id») и значение (например, «text»).

    На скриншоте выделено поле ввода с тегом «input». Если мы внимательно просмотрим дерево элементов, то найдём ещё несколько таких полей с тегом «input». Однако, многие атрибуты у них разные. Именно по атрибутам наш бот сможет находить нужные элементы. Например, для ввода имени при регистрации найдёт именно то поле, которое предназначено для имени, а не для фамилии или логина, и не поле-обманку. Для этого в шаблоне нужно будет указать, что надо найти элемент с определенным тегом («input») и уникальной парой атрибут-значение (в примере выше подойдёт атрибут «id» со значением «text»). Как именно это можно сделать – разберём чуть позже.


    Для чего нам могут понадобиться элементы?

    Практически любое взаимодействия бота со страницей – это взаимодействие с её элементами.
    С элементами можно сделать следующие действия.

    1. Установить значение атрибута.
    Пример использования – когда нужно заполнить какое-нибудь поле ввода. Чаще всего в этом случае нужно установить заданный текст в значение атрибута «value».

    2. Получить значение атрибута.
    Пример использования – когда нужно спарсить какой-либо текст с элемента. Чаще всего нужный текст можно получить из атрибутов «InnerHtml» и «InnerText».

    3. Вызвать событие.
    Пример использования – клик по элементу, эмуляция наведения мыши на элемент.

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

    Для этих целей в ProjectMaker существует инструмент под названием «Конструктор действий». Его можно вызвать для конкретного элемента, щелкнув по нему правой клавишей мыши в Дереве элементов или окне браузера и выбрав пункт «В конструктор действий».

    1.4.png

    Панель «Конструктора» состоит из двух рабочих областей: «Поиск элемента» и «Выбор действия».

    «Поиск элемента»

    В первой области находятся настройки поиска элемента, с которым необходимо взаимодействовать боту. При вызове «Конструктора» все поля и выпадающие списки автоматически заполняются параметрами и атрибутами того элемента, через который произведен вызов.

    Так как тут всё заполнено, по логике, можно сразу переходить к выбору действия, однако перед этим обязательно рекомендуется обратить внимание на некоторые автозначения.

    № документа и № формы. Документов и форм на странице может быть несколько, и по умолчанию ZennoPoster ставит те номера форм, в которых находится целевой элемент. Однако, в большинстве случаев это нежелательный вариант. Лучше, чтобы при каждом запуске бот искал элемент на всех документах и формах страницы, так как со стороны сайта однажды может поменяться их порядок, и привязка к номерам полетит.

    Например, владелец сайта добавит новый блок с рекламой в виде отдельного документа под номером 2. Все последующие номера документов страницы сместятся на 1. Если, скажем, наш элемент был на 3-м документе, то после добавления блока он будет на 4-м. При этом экшен поиска перестанет находить нужный элемент, потому что будет искать его только в 3-м документе.

    Чтобы поиск происходил во всех документах и формах, в выпадающих списках нужно выбрать значение «-1». Чтобы не делать это каждый раз, рекомендуется поставить следующие галочки в настройках ProjectMaker или ZennoPoster.

    1.5.png

    Имя и значение атрибута. В идеале эта пара должна быть уникальная для всей страницы, чтобы наш бот однозначно мог найти нужный элемент, не спутав его с другим. Чтобы проверить уникальность, жмём справа кнопку «Поиск», под которой отобразятся номера элементов, которые были найдены по текущим настройкам поиска. Если нашёлся всего 1 элемент под номером «0» - значит всё хорошо, атрибут и значения были подобраны верно. Если нашлось несколько – желательно попробовать другие пары «атрибут-значение», чтобы находился только 1 наш элемент.

    Важно. В выпадающем списке и табличном виде окна «Свойства элемента» отображаются только типовые атрибуты, однако в реальности их может быть гораздо больше. Все атрибуты элемента можно увидеть в значении поля «OuterHtml».

    1.6.png

    Например, для этого поля ввода мы можем вручную вписать в «Конструкторе» Имя элемента «aria-label», а в значение «Запрос», и мы так же найдём только наш один элемент.

    1.7.png

    Ещё один важный момент, связанный с именем и значением атрибута – необходимо следить, чтобы значение было «надёжное» с точки зрения возможных изменений на сайте, как в верстке, так и вообще. Если значение выглядит как автоматически сгенерированное или динамически меняющееся, то лучше сразу выбрать другую пару «атрибут-значение».

    1.8.png

    Иначе уже при следующем запуске бота элемент может не найтись.

    Обычно чисто по виду несложно догадаться о том, генерируемое или динамически меняется то или иное свойство, или нет. Однако, если возникнут сомнения - автоматически генерируемые значения можно проверить так – нужно обновить страницу в браузере ProjectMaker и попытаться найти тот же элемент по той же паре «атрибут-значение». Если элемент нашёлся – скорее всего значение статично.

    Более тонкую настройку проверки значения атрибута можно настроить с помощью выпадающего списка «Тип поиска». По умолчанию выставлен тип «text» – т.е. значение атрибута содержит текст из поля «Значение». По аналогии, тип «notext» – значение атрибута содержит текст из поля «Значение», тип «regexp» – значение атрибута соответствует регулярному выражению из поля «Значение»

    «Выбор действия»

    В этой области можно выбрать и протестировать действие с выбранным элементом. Есть 3 типа действия, которые соответствуют упоминаемым нами ранее:

    1) Set – установить значение атрибута;

    2) Get – получить значение атрибута;

    3) Rise – вызвать событие.

    Для первых двух действий выбираем атрибут, для «Set» дополнительно вписываем нужный текст в поле «Значение» – всё довольно просто.

    Для действия «Rise» нужно выбрать JS-событие, которое будет применено к текущему элементу. Примеры популярных событий: «click» – эмуляция клика по элементу, «onmouseover» – эмуляция наведения мыши на элемент. Для большинства сайтов подобные действия в виде вызова событий выполняются нормально, однако нередко наиболее «вкусные» сайты для ботоводов имеют либо особую структуру, либо повышенную защиту. Поэтому «Rise» будет работать не всегда, нужна более высокий уровень эмуляции. Но, об этом в следующих главах.

    Далее, по кнопке «Тестировать» можно сразу протестировать выбранное действие на странице, по кнопке «Добавить в проект» в шаблоне автоматически создастся экшен, который будет искать элемент и делать с ним действие, в точности по выставленным настройкам из «Конструктора».

    Полезные сниппеты

    Итак, мы прошлись по базовым инструментам ProjectMaker, необходимых для взаимодействия со страницами. Однако, в современных реалиях лучше знать больше, иметь возможность использовать более гибкие инструменты.

    В окончании данной главе рассмотрим предпочтительные способы для поиска элементов, а именно XPath и CSS-селекторы.

    Поиск через CSS-селекторы предпочтителен для тех, кто новичок в ZennoPoster, но не новичок в web-разработке, таким люди обычно хорошо умеют ими пользоваться, для них и предпочтителен этой вариант.

    Для полных же новичков я бы порекомендовал потихоньку начать осваивать XPath – язык запросов к элементам страницы, очень гибкий и мощный. В одном из прошлых конкурсов статей была хорошая статья по этому инструменту. Так же в гугле/ютубе можно найти плейлист с уроками по XPath от sibbora (заодно рекомендую обратить на внимание на полный его курс по ZennoPoster+C#).

    К сожалению, пользоваться XPath и CSS-селекторами на момент написания статьи можно только из C#-кода, что является серьёзной проблемой для новичков. Чтобы компенсировать этот недостаток, я создал несколько сниппетов, принцип построения которых – максимальное удобство, насколько это в данном случае возможно.

    Чтобы у вас не было необходимости копаться в C# коде, все настройки сниппетов вынесены в переменные проекта со специальными префиксами (т.е. перед использованием желательно создать у себя в шаблоне эти переменные).

    1.9.png

    Достаточно заполнить значения переменных и выполнить нужный сниппет.

    [​IMG]

    Таскать из проекта в проект такие сниппеты неудобно, поэтому к сообщению прикрепляю архив с папкой «Навигация по страницам», который нужно закинуть в подобную папку на компьютере:

    [​IMG]

    После этого можно в любом проекте ProjectMaker создать экшен C#-кода и вставить туда любой сниппет в пару кликов:

    [​IMG]



    Для тех, кому интересен код сниппетов, выкладываю тут пример первого.

    Code (CSharp):
    1. // НАСТРОЙКИ СНИППЕТА
    2.  
    3. // По умолчанию все значения для сниппета берутся из переменных
    4. // проекта с префиксами "find_element_", "set_value_", "get_value_", "rise_"
    5.  
    6. // Если заполнить поля ниже - значения переменных будут игнорироваться
    7.  
    8. //Один или несколько тегов (через ';')
    9. string tag = "";
    10. //Название атрибута
    11. string attributeName = "";
    12. //Значение атрибута
    13. string attributeValue = "";
    14. //Тип поиска значения (text, notext, regexp)
    15. string searchKind = "";
    16. //Номер совпадения
    17. int number = 0;
    18.  
    19. //Название атрибута для установки
    20. string setAttrName = "";
    21. //Значение атрибута для установки
    22. string setAttrValue = "";
    23.  
    24.  
    25. #region Капот
    26.  
    27. //Получаем значения из переменных
    28. if (string.IsNullOrWhiteSpace(tag) && project.Variables.Keys.Contains("find_element_tag"))
    29. {
    30.     tag = project.Variables["find_element_tag"].Value;
    31. }
    32. if (string.IsNullOrWhiteSpace(attributeName) && project.Variables.Keys.Contains("find_element_attr_name"))
    33. {
    34.     attributeName = project.Variables["find_element_attr_name"].Value;
    35. }
    36. if (string.IsNullOrWhiteSpace(attributeValue) && project.Variables.Keys.Contains("find_element_attr_value"))
    37. {
    38.     attributeValue = project.Variables["find_element_attr_value"].Value;
    39. }
    40. if (string.IsNullOrWhiteSpace(searchKind) && project.Variables.Keys.Contains("find_element_search_kind"))
    41. {
    42.     searchKind = project.Variables["find_element_search_kind"].Value;
    43. }
    44. if (searchKind!="text" && searchKind!="notext" && searchKind!="regexp")
    45. {
    46.     searchKind = "text";
    47. }
    48. if (project.Variables.Keys.Contains("find_element_number"))
    49. {
    50.     int.TryParse(project.Variables["find_element_number"].Value, out number);
    51. }
    52.  
    53. if (string.IsNullOrWhiteSpace(setAttrName) && project.Variables.Keys.Contains("set_value_attr_name"))
    54. {
    55.     setAttrName = project.Variables["set_value_attr_name"].Value.ToLower();
    56. }
    57. if (string.IsNullOrWhiteSpace(setAttrValue) && project.Variables.Keys.Contains("set_value_attr_value"))
    58. {
    59.     setAttrValue = project.Variables["set_value_attr_value"].Value;
    60. }
    61.  
    62. //Ищем элемент
    63. var tab = instance.ActiveTab;
    64. var el = tab.FindElementByAttribute(tag, attributeName, attributeValue, searchKind, number);
    65. if (el.IsNull || el.IsVoid)
    66. {
    67.     throw new Exception("Элемент по заданным атрибутам не найден, действие не выполнено!");
    68. }
    69. //Устанавливаем значение атрибута
    70. if (setAttrName=="value")
    71. {
    72.     el.SetValue(setAttrValue, instance.EmulationLevel);
    73. }
    74. else
    75. {
    76.     el.SetAttribute(setAttrName, setAttrValue);
    77. }
    78.  
    79. #endregion
     

    Attached Files:

  2. LaGir

    LaGir Client

    Joined:
    Oct 1, 2015
    Messages:
    181
    Likes Received:
    515
    Дополнительные средства навигации, разбор некоторых задач


    Расширенная эмуляция мыши

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

    Главным образом это относится к выполнению действий на странице. Основной альтернативой вызова событий «Rise» в данный момент является расширенная эмуляция мыши: http://zennolab.com/wiki/ru:virtual_mouse

    Подробно она рассматривалась в этой теме. Там же можно найти шаблон-пример, в котором демонстрируется использование всех методов виртуальной мыши.

    Так же на форуме можно найти пользовательскую альтернативу продвинутой мыши в виде подключаемой dll-библиотеки.

    Так как опять же расширенная эмуляция мыши в текущий момент доступна только в C#-коде, по этой теме я так же подготовил набор сниппетов по FullEmulationMouse, доступных в прикрепленном архиве «Эмуляция мыши». Помещаем их в папку «Snippets» по тому же принципу, как написано в предыдущей главе. Далее можем вставлять в C#-сниппет любого своего проекта:

    2.1.png

    Они построены по-другому принципу, нежели сниппеты из предыдущей главы, поэтому новичкам перед использованием нужно будет немного почерпнуть информации из вики и форума насчёт использования экшенов C#-кода.


    Теперь предлагаю разобрать пару ситуаций-задачек по навигации.


    Фейковые элементы

    Делая ботов под ZennoPoster, рано или поздно натыкаешься на проблему элементов-дубликатов, которые 1 в 1 нужные нам элементы, с одинаковыми атрибутами и их значениями. Особенно это актуально, если наша задача заполнить какую-нибудь форму, у них часто стоит защита от ботов, построенная на этом принципе.

    2.2.png

    Определить настоящие от фейков обычно всё равно можно по скрытым атрибутам, положению в дереве элементов, атрибутам высоты и ширины элемента.

    Последний способ, пожалуй, самый действенный и универсальный. Если высота (атрибут «height») и ширина (атрибут «width») элемента больше нуля – это означает, что элемент визуально виден на странице, он показывается пользователю. А это, в свою очередь, практически всегда означает, что данный элемент «настоящий», с им можно взаимодействовать.

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

    На стандартных экшенах такую проверку при поиске элементов делать достаточно проблематично, да и место много занимает, поэтому вновь остановимся на сниппетах.

    Для примера возьмём кусок кода из первого сниппета предыдущей главы:

    Code (CSharp):
    1. var el = tab.FindElementByAttribute(tag, attributeName, attributeValue, searchKind, number);
    2. if (el.IsNull || el.IsVoid)
    3. {
    4.     throw new Exception("Элемент по заданным атрибутам не найден, действие не выполнено!");
    5. }
    6.  
    Он отвечает за поиск элемента по атрибутам, но найдёт любой подходящий элемент, не только видимый на странице. Чтобы найти именно видимый, нужно немного переделать сниппет. Заодно предусмотрим ещё один случай. Бывает, что элементы имеют высоту и ширину, но расположены они за пределами страницы, и по факту тоже не видны пользователю. В этом случае нужна дополнительная проверка того, что значения атрибутов «topInBrowser» и «leftInBrowser» не являются отрицательными.

    Code (CSharp):
    1. //Ищем коллекцию элементов по заданным атрибутам
    2. var col = tab.FindElementsByAttribute(tag, attributeName, attributeValue, searchKind);
    3. //Создаём результирующий элемент
    4. var elResult = tab.FindElementByXPath("//sngvuyrbuy",0);
    5. //Если в коллекции есть элементы
    6. if (col.Count>0)
    7. {
    8.     //В цикле проверяем каждый элемент коллекции
    9.     foreach (var el in col.Elements)
    10.     {
    11.         //Получаем значения topInBrowser и leftInBrowser
    12.         int topInBrowser = int.Parse(el.GetAttribute("topInBrowser"));
    13.         int leftInBrowser = int.Parse(el.GetAttribute("leftInBrowser"));
    14.         //Если элемент видимый
    15.         if (el.Height>0 && el.Width>0 && topInBrowser>=0 && leftInBrowser>=0)
    16.         {
    17.             //Присваиваем результирующему элементу текущий
    18.             elResult = el;
    19.             //Выходим из цикла проверки
    20.             break;
    21.         }
    22.     }
    23. }//Проверяем, нашли ли нужный элемент
    24. if (elResult.IsNull || elResult.IsVoid)
    25. {
    26.     throw new Exception("Видимый элемент по заданным атрибутам не найден, действие не выполнено!");
    27. }
    В данном случае код будет искать первый видимый элемент на странице, удовлетворяющий поиску по атрибутам.

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

    Code (CSharp):
    1. // НАСТРОЙКИ СНИППЕТА
    2.  
    3. // По умолчанию все значения для сниппета берутся из переменных
    4. // проекта с префиксами "find_element_", "set_value_", "get_value_", "rise_"
    5.  
    6. // Если заполнить поля ниже - значения переменных будут игнорироваться
    7.  
    8. //Один или несколько тегов (через ';')
    9. string tag = "";
    10. //Название атрибута
    11. string attributeName = "";
    12. //Значение атрибута
    13. string attributeValue = "";
    14. //Тип поиска значения (text, notext, regexp)
    15. string searchKind = "";
    16. //Номер совпадения
    17. int number = 0;
    18.  
    19. //Название атрибута для установки
    20. string setAttrName = "";
    21. //Значение атрибута для установки
    22. string setAttrValue = "";
    23.  
    24.  
    25. #region Капот
    26.  
    27. //Получаем значения из переменных
    28. if (string.IsNullOrWhiteSpace(tag) && project.Variables.Keys.Contains("find_element_tag"))
    29. {
    30.     tag = project.Variables["find_element_tag"].Value;
    31. }
    32. if (string.IsNullOrWhiteSpace(attributeName) && project.Variables.Keys.Contains("find_element_attr_name"))
    33. {
    34.     attributeName = project.Variables["find_element_attr_name"].Value;
    35. }
    36. if (string.IsNullOrWhiteSpace(attributeValue) && project.Variables.Keys.Contains("find_element_attr_value"))
    37. {
    38.     attributeValue = project.Variables["find_element_attr_value"].Value;
    39. }
    40. if (string.IsNullOrWhiteSpace(searchKind) && project.Variables.Keys.Contains("find_element_search_kind"))
    41. {
    42.     searchKind = project.Variables["find_element_search_kind"].Value;
    43. }
    44. if (searchKind!="text" && searchKind!="notext" && searchKind!="regexp")
    45. {
    46.     searchKind = "text";
    47. }
    48. if (project.Variables.Keys.Contains("find_element_number"))
    49. {
    50.     int.TryParse(project.Variables["find_element_number"].Value, out number);
    51. }
    52.  
    53. if (string.IsNullOrWhiteSpace(setAttrName) && project.Variables.Keys.Contains("set_value_attr_name"))
    54. {
    55.     setAttrName = project.Variables["set_value_attr_name"].Value.ToLower();
    56. }
    57. if (string.IsNullOrWhiteSpace(setAttrValue) && project.Variables.Keys.Contains("set_value_attr_value"))
    58. {
    59.     setAttrValue = project.Variables["set_value_attr_value"].Value;
    60. }
    61.  
    62. var tab = instance.ActiveTab;
    63. //Ищем коллекцию элементов по заданным атрибутам
    64. var col = tab.FindElementsByAttribute(tag, attributeName, attributeValue, searchKind);
    65. //Создаём результирующий элемент
    66. var elResult = tab.FindElementByXPath("//sngvuyrbuy",0);
    67. //Если в коллекции есть элементы
    68. if (col.Count>0)
    69. {
    70.     //В цикле проверяем каждый элемент коллекции
    71.     foreach (var el in col.Elements)
    72.     {
    73.         //Получаем значения topInBrowser и leftInBrowser
    74.         int topInBrowser = int.Parse(el.GetAttribute("topInBrowser"));
    75.         int leftInBrowser = int.Parse(el.GetAttribute("leftInBrowser"));
    76.         //Если элемент видимый
    77.         if (el.Height>0 && el.Width>0 && topInBrowser>=0 && leftInBrowser>=0)
    78.         {
    79.             //Присваиваем результирующему элементу текущий
    80.             elResult = el;
    81.             //Выходим из цикла проверки
    82.             break;
    83.         }
    84.     }
    85. }
    86. //Проверяем, нашли ли нужный элемент
    87. if (elResult.IsNull || elResult.IsVoid)
    88. {
    89.     throw new Exception("Видимый элемент по заданным атрибутам не найден, действие не выполнено!");
    90. }
    91.  
    92. //Устанавливаем значение атрибута
    93. if (setAttrName=="value")
    94. {
    95.     elResult.SetValue(setAttrValue, instance.EmulationLevel);
    96. }
    97. else
    98. {
    99.     elResult.SetAttribute(setAttrName, setAttrValue);
    100. }
    101.  
    102. #endregion
    По такому же принципу можно (и рекомендуется) переделать все остальные заготовки сниппетов, как из первой главы по навигации, так из этой, по эмуляции мыши. Пусть это будет домашним заданием для заинтересовавшихся. J


    Нестандартные выпадающие списки

    Классические выпадающие списки на страницах состоят из элемента с тегом «select» и вложенных элементов с тегом «option». Почти всегда нужные пункты из них легко выбираются с помощью сниппетов этой темы.

    Однако, лично я в последнее такие встречаю крайне редко, в основном у крупных ресурсов стоят другие выпадающие списки. Например, стоит одиночный «div», при клике мыши на который скрипты показывают на странице блок с вариантами выбора, состоящих из других «div». Нередко на таких списках вызов событий «RiseEvent» не работает, и приходится использовать либо эмуляцию мыши, либо клавиатуры (стрелки «вверх» и «вниз» для перебора вариантов, «Enter» для выбора). Предлагаю рассмотреть примерный алгоритм выбора вариантов в таких списках.

    Для примера возьмём, скажем, выбор контрольного вопроса в форме регистрации яндекса. Допустим, нам нужно выбрать вариант «Ваша любимая компьютерная игра».

    Кнопка списка

    2.3.png

    Варианты списка

    2.4.png


    Первая задача – подвести мышку и кликнуть по списку. Ищем элемент списка и кликаем.

    Code (CSharp):
    1. var tab = instance.ActiveTab;
    2. string text = "Ваша любимая компьютерная игра";
    3.  
    4. //Находим элемент выпадающего списка
    5. var elDropDown = tab.FindElementByXPath("//button[contains(@class,'control-questions')]", 0);
    6.  
    7. //Ведём мышь к списку
    8. tab.FullEmulationMouseMoveToHtmlElement(elDropDown);
    9. Thread.Sleep(300);
    10. //Кликаем по списку
    11. tab.FullEmulationMouseClick("left", "click");
    12. Thread.Sleep(400);
    Обратите внимание на небольшие паузы. При работе с такими списками с эмуляцией мыши желательно их ставить, без задержек клики иногда не срабатывают, мышка иногда идет не туда, куда надо. Связано это в основном с тем, что скрипты ещё не успели поменять положение элементов, а мышь что-то делает (например, пошла к старым координатам).
    UPD: Как заметил Zymlex в комментариях, паузы - не самый рациональный подход. В идеале вместо пауз делать проверку элемента, а точнее цикл с проверкой (пока элемент не готов - делаем микропаузы в цикле, как только становится готов - прерываем цикл).

    Теперь ищем коллекцию элементов-вариантов, находим нужный, кликаем по нему. Ищем коллекцию именно сейчас, когда они появились на странице перед пользователем (в идеале сделать проверку на то, что они действительно появились – как это сделать, мы рассматривали в предыдущем примере).

    Code (CSharp):
    1. //Ищем элементы
    2. var colOptions = tab.FindElementsByXPath("//div/span[@class='menu__text']");
    3. //В цикле ищем элементы
    4. foreach (var elOption in colOptions)
    5. {
    6.     //Если нашли нужный вариант
    7.     if (elOption.InnerText==text)
    8.     {
    9.         //Ведём мышь
    10.         tab.FullEmulationMouseMoveToHtmlElement(elOption);
    11.         Thread.Sleep(300);
    12.         //Кликаем
    13.         tab.FullEmulationMouseClick("left", "click");
    14.         Thread.Sleep(400);
    15.         break;
    16.     }
    17. }

    Заготовка для универсальной гулялки по web-страницам

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

    Логика работы сниппета:

    1 ) Переходим на стартовую страницу;
    2 ) Ищем видимые элементы с текстом;
    3 ) Если страниц нет – переходим к пункту №10;
    4 ) Берём первый элемент для чтения;
    5 ) С определенной вероятностью пропускаем его и переходим к следующему;
    6 ) Иначе, если текст короткий, пропускаем и переходим к следующему;
    7 ) Иначе эмулируем чтение текста на элементе;
    8 ) С небольшой вероятностью переходим по ссылке в текущем тексте, возвращаемся к пункту №2;
    9 ) Повторяем пункты №4-8 для всех элементов страницы, предназначенных для чтения;
    10 ) Переходим по случайной отображаемой ссылке на странице, возвращаемся к пункту №2;
    11 ) Выполняем пункты №2-10 пока не просмотрим нужное количество страниц.

    Code (CSharp):
    1. //Адрес стартовой страницы
    2. string url = "https://ru.wordpress.org/";
    3. //Количество страниц для чтения
    4. int pageCount = 3;
    5. //XPath элементов для эмуляции чтения
    6. string xpath = "//h1[string-length(text())>10]|//h2[string-length(text())>10]|//h3[string-length(text())>10]|//h4[string-length(text())>10]|//p[string-length(text())>10]";
    7. //Переменная для определения вероятности
    8. int chance = 0;
    9. //Шанс пропуска блоков, в %
    10. int chanceSkip = 70;
    11. //Переменная для определения перехода в абзаце
    12. bool isLinkClicked = false;
    13.  
    14. var rnd = Global.Classes.rnd;
    15. var tab = instance.ActiveTab;
    16.  
    17. //Переходим на заданную страницу
    18. tab.Navigate(url);
    19. if (tab.IsBusy)    tab.WaitDownloading();
    20. Thread.Sleep(1700);
    21.  
    22. //Цикл чтения страниц (1 итерация - 1 страница)
    23. for (int p=0; p<pageCount; p++)
    24. {
    25.     //Ищем элементы странички для эмуляции чтения
    26.     var colTexts = tab.FindElementsByXPath(xpath);
    27.     //Вычленяем только видимые на странице элементы
    28.     var colResult = tab.FindElementsByXPath("//sngvuyrbuy");
    29.     if (colTexts.Count>0)
    30.     {
    31.         foreach (var el in colTexts.Elements)
    32.         {
    33.             int topInBrowser = int.Parse(el.GetAttribute("topInBrowser"));
    34.             int leftInBrowser = int.Parse(el.GetAttribute("leftInBrowser"));
    35.             //Если элемент видимый
    36.             if (el.Height>0 && el.Width>0 && topInBrowser>=0 && leftInBrowser>=0)
    37.             {
    38.                 colResult.Add(el);
    39.             }
    40.         }
    41.     }
    42.     colTexts = colResult;
    43.  
    44.     //Если коллекция пуста, выводи соответствующее сообщение в лог
    45.     if (colTexts.Count<1)
    46.     {
    47.         project.SendWarningToLog("Подходящие тексты для чтения не найдены! Страница: '"+tab.URL+"'", true);
    48.     }
    49.  
    50.     int num = 1;
    51.     //В цикле проходим элементы коллекции
    52.     foreach (var elText in colTexts.Elements)
    53.     {
    54.         // Определяем, пропускать ли данный блок
    55.         chance = rnd.Next(1, 101);
    56.         //На основе выпавшего значения решаем, пропускать ли блок
    57.         if (chance <= chanceSkip)
    58.         {
    59.             //С вероятностью chanceSkip пропускаем
    60.             project.SendInfoToLog("Пропускаем чтение элемента №" + num + ", тег: "+elText.TagName);
    61.             num++;
    62.             Thread.Sleep(rnd.Next(3,9)*100);
    63.             continue;
    64.         }
    65.  
    66.         // Пропускаем слишком короткие блоки с определенной вероятностью
    67.         if (elText.InnerText.Length < 100)
    68.         {
    69.             //Если длина текста блока меньше 100 символов
    70.             chance = rnd.Next(1, 101);    //Случайно берём значение от 1 до 100
    71.             if (chance <= chanceSkip)
    72.             {
    73.                 //С вероятностью chanceSkip не читаем его
    74.                 project.SendInfoToLog("Пропускаем чтение элемента №" + num + " из-за малой длины его текста, тег: "+elText.TagName);
    75.                 num++;
    76.                 continue;
    77.             }
    78.         }
    79.  
    80.         //Записываем в лог номер текущего элемента и название его тега
    81.         project.SendInfoToLog("Читаем элемент №" + num + ", тег: "+elText.TagName);
    82.  
    83.         //Эмулируем чтение элемента
    84.         tab.FullEmulationMouseMoveAboveHtmlElement(elText, 30);
    85.  
    86.         //С определенной вероятностью переходим по случайной ссылке в абзаце
    87.         var colLinks = elText.FindChildrenByXPath(".//a");
    88.         //Если в коллекции есть элементы (ссылки)
    89.         if (colLinks.Count > 0)
    90.         {
    91.             chance = rnd.Next(1, 101);
    92.             if (chance <= 3)
    93.             {
    94.                 //С вероятностью 3% переходим по случайной ссылке из абзаца
    95.                 var elLink = colLinks.Elements[rnd.Next(colLinks.Count)];
    96.                 tab.FullEmulationMouseMoveToHtmlElement(elLink);
    97.                 Thread.Sleep(rnd.Next(4,8)*100);
    98.                 tab.FullEmulationMouseClick("left", "click");
    99.                 if (tab.IsBusy)    tab.WaitDownloading();
    100.                 Thread.Sleep(rnd.Next(5,25)*100);
    101.                 //Сообщаем коду ниже что перешли по ссылке
    102.                 isLinkClicked = true;
    103.                 //Выходим из цикла, переходим к следующей страничке
    104.                 break;
    105.             }
    106.         }
    107.  
    108.         //Случайная пауза между чтением блоков
    109.         chance = rnd.Next(0, 2);
    110.         if (chance==0)
    111.         {
    112.             Thread.Sleep(rnd.Next(5,50)*100);
    113.         }
    114.  
    115.         num++;
    116.     }
    117.  
    118.     //Если при чтении не переходили на другую страницу
    119.     if (isLinkClicked == false)
    120.     {
    121.         //Находим все элементы, содержащие ссылки
    122.         var colLinks = tab.FindElementsByXPath("//a");
    123.         //Вычленяем только видимые на странице ссылки
    124.         colResult = tab.FindElementsByXPath("//sngvuyrbuy");
    125.         if (colLinks.Count>0)
    126.         {
    127.             foreach (var el in colLinks.Elements)
    128.             {
    129.                 int topInBrowser = int.Parse(el.GetAttribute("topInBrowser"));
    130.                 int leftInBrowser = int.Parse(el.GetAttribute("leftInBrowser"));
    131.                 if (el.Height>0 && el.Width>0 && topInBrowser>=0 && leftInBrowser>=0)
    132.                 {
    133.                     colResult.Add(el);
    134.                 }
    135.             }
    136.         }
    137.         colLinks = colResult;
    138.         //Проверка наличия ссылок
    139.         if (colLinks.Count<1)
    140.         {
    141.             project.SendWarningToLog("Видимых ссылок на странице не найдено! "+tab.URL, true);
    142.             project.SendInfoToLog("Завершили гуляние по страницам. Количество прочитанных страниц: "+(p+1), true);
    143.             return "ok";
    144.         }
    145.         //Ведём мышку к случайному элементу со ссылкой
    146.         var elLink = colLinks.Elements[rnd.Next(colLinks.Elements.Length)];
    147.         tab.FullEmulationMouseMoveToHtmlElement(elLink);
    148.         Thread.Sleep(rnd.Next(4,8)*100);
    149.         tab.FullEmulationMouseClick("left", "click");
    150.         if (tab.IsBusy)    tab.WaitDownloading();
    151.         Thread.Sleep(rnd.Next(5,25)*100);
    152.     }
    153.     project.SendInfoToLog("Завершили чтение "+ (p+1) +"-й страницы.", true);
    154. }
    155.  
    156. project.SendInfoToLog("Завершили гуляние по страницам.", true);
    Вы можете тестировать данный сниппет на любых страницах, по мере надобности расширять его. В нём не хватает ряда нужных в плане универсальности вещей, но и в таком виде он неплохо гуляет по многим страницам интернета.

    На этом всё, благодарю за внимание. :-)
     

    Attached Files:

    Last edited: May 16, 2018
  3. Zymlex

    Zymlex Client

    Joined:
    Oct 24, 2016
    Messages:
    1,425
    Likes Received:
    723
    Второй пост :ay:
    От большинства задержек можно избавиться, просто заменив их на проверку элемента.
     
    LaGir likes this.
  4. one

    one Client

    Joined:
    Sep 22, 2015
    Messages:
    5,122
    Likes Received:
    853
    focus :D
     
  5. LaGir

    LaGir Client

    Joined:
    Oct 1, 2015
    Messages:
    181
    Likes Received:
    515
    Абсолютно верно, в данном контексте целесообразнее добавлять небольшой цикл проверки с минизадержками, дополню чутка пример в статье.
    Видимо, не очень внимательно читали :-) Пример рассчитан как раз на то, чтобы не использовать вызов событий, так как не всегда в таких ситуациях они работают как надо.
     
  6. Moadip

    Moadip Client

    Joined:
    Sep 26, 2015
    Messages:
    424
    Likes Received:
    569
    Отличная статья. Для новичков самое то.
    Умение работать с DOM моделью это самый первый навык который надо оттачивать когда начинаешь работать с зенкой.
    Без четкого понимания как добраться до нужного элемента, вся остальная работа просто встанет.
    А это понимание основывается на различных "фишках" при поиске(к чему привязываться, что смотреть), и приходит только в процессе практики. :D
    Xpath тоже musthave, хотя бы азы. Т.к. без него в некоторых случаях до нужного элемента будет очень трудно добраться.
     
    Zymlex, LaGir, Lord_Alfred and 3 others like this.
  7. Juniorcpa

    Juniorcpa Client

    Joined:
    May 27, 2014
    Messages:
    1,183
    Likes Received:
    542
    Пытался когда-то произвести эмуляцию мышкой, но если элемент не в зоне видимости инстанса, а мышка двигается к нему, то мышка зависает. Есть решение? :-)
     
  8. Zymlex

    Zymlex Client

    Joined:
    Oct 24, 2016
    Messages:
    1,425
    Likes Received:
    723
    Второй пост статьи содержит пример проверки.
    В основном надо проверять координаты, размер и атрибут hidden.
     
    LaGir likes this.
  9. vladinvest

    vladinvest Client

    Joined:
    May 29, 2016
    Messages:
    50
    Likes Received:
    6
    Как реализовать клик по кнопке, прикрытой js? Пробовал и через атрибуты, и через XPath сделать, только с третьего раза прошибается. Js отключать нельзя, проект сразу отвалится.
     
  10. LaGir

    LaGir Client

    Joined:
    Oct 1, 2015
    Messages:
    181
    Likes Received:
    515
    Перед движением, помимо описанного в статье, можно проверять, находится ли целевой элемент в пределах окна инстанса.
    Начальные координаты инстанса (0; 0), значит атрибуты элементы "leftInBrowser" и "topInBrowser" не должны быть меньше 0.
    Конечные координаты инстанса (правая и нижняя границы) обычно равны конечным координатам главного документа на вкладке (т.е. по сути его высоте и ширине). Значит, координаты элемента не должны превышать этих значений.
    Соответственно, можем сделать такую проверку:
    Code (CSharp):
    1. var tab = instance.ActiveTab;
    2.  
    3. //Находим элемент
    4. var el = tab.FindElementByXPath("//h1", 0);
    5.  
    6. //Высота и ширина главного документа страницы
    7. //Они, как правило - нижняя и правая конечные точки инстанса, что нам и нужно
    8. int borderRight= tab.MainDocument.Width;
    9. int borderBottom = tab.MainDocument.Height;
    10.  
    11. //Координаты элемента в браузере
    12. int elX = int.Parse(el.GetAttribute("leftInBrowser"));
    13. int elY = int.Parse(el.GetAttribute("topInBrowser"));
    14.  
    15. //Проверка нахождения элемента в пределах окна инстанса
    16. if (elX < 0 || elY < 0 || elX > borderRight || elY > borderBottom)
    17. {
    18.     throw new Exception("Элемент находится за пределами окна инстанса!");
    19. }


    Пока не совсем понятно, что за проблема, что подразумевается под "js, который прикрывает кнопку". В идеале реальный пример/страничку где такое происходит, или хотя более детальное описание.
     
    Nike59, yriy158 and Zymlex like this.
  11. Viking01

    Viking01 Client

    Joined:
    Aug 19, 2017
    Messages:
    34
    Likes Received:
    7
    огонь статья) особенно полезна в плане эмуляции) проголосую однозначно)
     
    LaGir likes this.
  12. bizzon

    bizzon Client

    Joined:
    Sep 8, 2015
    Messages:
    295
    Likes Received:
    11
    В видео есть сниппет "Количество элементов по XPath". Можно где-то найти такой?
     
  13. LaGir

    LaGir Client

    Joined:
    Oct 1, 2015
    Messages:
    181
    Likes Received:
    515
    Он виден на видео полностью, можно просто переписать. Вот код:
    Code (CSharp):
    1. //Получаем путь XPath
    2. string xpath = project.Variables["find_element_xpath"].Value;
    3. //Ищем количество элементов по пути
    4. int count = instance.ActiveTab.FindElementsByXPath(xpath).Count;
    5. //Выводим количество в лог и в возвращаемую переменную
    6. return "Найдено "+count+" элементов по пути '"+xpath+"'";

    Если XPath не знаком (или иные трудности с ним), вот сниппет для поиска по атрибутам:
    Code (CSharp):
    1. // Получаем тег, название и значение атрибута, тип поиска из переменных проекта
    2. //Один или несколько тегов (через ';')
    3. string tag = project.Variables["find_element_tag"].Value;
    4. //Название атрибута
    5. string attributeName = project.Variables["find_element_attr_name"].Value;
    6. //Значение атрибута
    7. string attributeValue = project.Variables["find_element_attr_value"].Value;
    8. //Тип поиска значения (text, notext, regexp)
    9. string searchKind = project.Variables["find_element_search_kind"].Value;
    10.  
    11. //Ищем количество элементов
    12. int count = instance.ActiveTab.FindElementsByAttribute(tag, attributeName, attributeValue, searchKind).Count;
    13. //Выводим количество в лог и в возвращаемую переменную
    14. return "Найдено "+count+" элементов";
     
    Nike59, Platon and bizzon like this.
  14. AcidX

    AcidX Client

    Joined:
    Aug 19, 2012
    Messages:
    8
    Likes Received:
    3
    Извините, может быть нубский вопрос, но все таки, если я правильно понял, то методы навигации - это методы таба. А как добраться до вложенных элементов в другой элемент, т.е. не от таба, а от конкретного элемента в цикле например?
     
  15. doc

    doc Client

    Joined:
    Mar 30, 2012
    Messages:
    6,914
    Likes Received:
    3,241
    у элементов в коде есть методы FindChild и FindChildren, похожие на FindElement и FindElements у таба
     
    LaGir and AcidX like this.
  16. LaGir

    LaGir Client

    Joined:
    Oct 1, 2015
    Messages:
    181
    Likes Received:
    515
    В последних версиях ZennoPoster следующий код уже не работает:
    Code (CSharp):
    1. //Координаты элемента в браузере
    2. int elX = int.Parse(el.GetAttribute("leftInBrowser"));
    3. int elY = int.Parse(el.GetAttribute("topInBrowser"));
    Нужно использовать взамен подобный:
    Code (CSharp):
    1. //Координаты элемента в браузере
    2. int elX = el.DisplacementInBrowser.X;
    3. int elY = el.DisplacementInBrowser.Y;
    Также, при наличии проверки на нахождение элемента в пределах окна инстанса периодически встречаю странные глюки, которых без неё не бывает. Пока не разобрался почему так происходит, поэтому пока не рекомендовал бы добавлять эту проверку. Хотя, проблемы вроде только при отладке в PM, но надо потестить получше.
     
  17. Tp0yaH

    Tp0yaH Client

    Joined:
    Sep 26, 2017
    Messages:
    1
    Likes Received:
    0
    Почему бы, не рекомендовать, сразу осваивать JS? Более универсальное решение, менее громоздкое, не будет являться серьезной проблемой для новичков, даст понимания DOM-а...
     
  18. Zymlex

    Zymlex Client

    Joined:
    Oct 24, 2016
    Messages:
    1,425
    Likes Received:
    723
    Будет, взаимодействие всё равно идёт с использованием C#, да и логику предпочтительней писать на нём, как и весь шаб.
    Но JS тоже нужен, некоторые моменты в C# работают не так как бы хотелось или отсутствуют, большинству же хватает кубиков, иногда сниппетов.
     
    LaGir likes this.
  19. Nord

    Nord Client

    Joined:
    Mar 22, 2012
    Messages:
    1,898
    Likes Received:
    924
    А что вот это за абракадабра "sngvuyrbuy"?
     
  20. LaGir

    LaGir Client

    Joined:
    Oct 1, 2015
    Messages:
    181
    Likes Received:
    515
    @Nord
    Просто набор случайных символов. В этой строчке нужно объявить/получить пустую коллекцию элементов, это можно сделать как раз через поиск несуществующих элементов (по крайней мере я других рабочих вариантов не нашёл).


    @Tp0yaH
    1) То, что вы процитировали, реально освоить за 1-2 вечера. За сколько осваивается полноценный язык программирования, пусть это и JavaScript? Соответственно, вы уверены, что такое сравнение корректно в контексте полезности для новичков?
    2) Если и давать рекомендацию по изучению полноценного языка программирования чисто для ZP, то уж лучше C#, возможностей в этом контексте гораздо больше именно у него. Ну и необходимые основы JS новичками и так сами по себе изучаются в процессе освоения PM и ZP. Большее от JS в пределах ZP обычно не нужно, за исключением некоторых специфических задач.
    3) Если говорить об универсальном решении при наличии большого количества времени (как для изучения JS), то, имхо, тут стоит изучать понемножку, в комплексе всё то, что часто используется в Zenno и с Zenno - C#, JS, SQL, HTTP, PHP, RegExp, XPath, ... Концентрируясь на чём-то одном, далеко на платформе Zenno не уедешь.
     
    JurgenZolle, Zymlex and Nord like this.
  21. g1sm0

    g1sm0 Client

    Joined:
    Sep 25, 2015
    Messages:
    2
    Likes Received:
    0
    А что именно подразумевается под номерами форм и документов? Ведь мы работаем со страницей - разве 1 страница(то, что находится между <body></body>) не есть 1 документ?
     
  22. doc

    doc Client

    Joined:
    Mar 30, 2012
    Messages:
    6,914
    Likes Received:
    3,241
    боди может быть не один, а форма тем более
     
    g1sm0 likes this.
  23. Armagidec

    Armagidec Client

    Joined:
    Aug 27, 2014
    Messages:
    66
    Likes Received:
    5
    Подскажите как вставить переменную в код - //Номер совпадения

    Вот так не работает - int num = project.Variables["schetchik"].Value;

    / Поиск элемента по тегу
    //Тег элемента
    string tag = "h2";
    //Номер совпадения
    int num = 0;
     
  24. Armagidec

    Armagidec Client

    Joined:
    Aug 27, 2014
    Messages:
    66
    Likes Received:
    5
    Code (text):
    1. // Поиск элемента по тегу
    2. //Тег элемента
    3. string tag = "h2";
    4. //Номер совпадения
    5. int num = int.Parse(project.Variables["number"].Value);
    6.  
    7. //Поиск элемента
    8. var el = instance.ActiveTab.FindElementByTag(tag, num);
    9. //Движение к элементу
    10. instance.ActiveTab.FullEmulationMouseMoveToHtmlElement(el);
    Разобрался...

    Вот так работает
    Code (text):
    1. //Номер совпадения
    2. int num = int.Parse(project.Variables["number"].Value);
     
  25. Veterinar

    Veterinar Client

    Joined:
    Jul 1, 2016
    Messages:
    187
    Likes Received:
    18
    Он имеет ввиду, элемент спрятанный за JS popup, я с такой фигней в Одноклассниках сталкивался, когда после действия OnMouseOver выскакивают дополнительные элементы с которыми можно взаимодействовать, сам эту проблему и решил только действием OnMouseOver. А как сделать чтобы без лишних действий так и не разобрался.
     
  26. ZhbanOFF

    ZhbanOFF Client

    Joined:
    Feb 23, 2018
    Messages:
    2
    Likes Received:
    0
    Люди добрые, подскажите как сделать что бы мышька зажала элимент и передвинула его в нужное место?! нашёл только как сделать чтобы мышка пошла на элимент и нажала, а чтобы перетащила не нашёл...
     

    Attached Files:

    • 123.jpg
      123.jpg
      File size:
      51.9 KB
      Views:
      51

Пользователи просматривающие тему (Пользователей: 0, Гостей: 0)