Привет, Хабр! Меня зовут Николай Суворов, я руководитель направления в МТС Digital. Занимаюсь продуктом МТС Premium – это единая подписка на сервисы МТС и партнеров. Сегодня я расскажу о нашем опыте создания робота для автоматизации повторяющихся действий сотрудников с помощью Jupyter, Python и Selenium. Статья будет интересна прежде всего менеджерам, которые хотят оптимизировать свою работу. Разработчикам мой текст будет полезен с точки зрения понимания возможностей по ускорению повторяющихся действий в интерфейсах. Весь необходимый код – ниже.

Зачем автоматизировать рутину

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

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

Часть рутинных операций можно устранить при помощи грамотного UX. Но бывает и так, что требуется постоянно повторять одни и те же действия и в самых продуманных интерфейсах. Даже с кнопкой «копировать» или «создать по шаблону» рутинное действие не всегда происходит быстрее. Кроме того, при ручных операциях с увеличением количества задач возрастает и вероятность ошибки. Особенно, если дело касается цифр, дат, опций. Менеджер забыл поставить опцию отмены промокода в определённый день – и код становится вечным. Узнаем мы об этом только потом, читая еженедельный отчёт по активациям кодов. И это – совсем не единичный случай.

UX/CX-анализ – дело хлопотное и затратное, но доработки, ускоряющие выявленные повторяющиеся операции, обойдутся еще дороже. Их надо проаналитить, закодить, оттестировать, зарелизить. Между выявлением проблемы и ее устранением могут пройти месяцы и даже годы. И все это время компания продолжит тратить ресурсы на повторяющиеся однообразные операции, которые будут выполнять всё возрастающее количество ассистентов.

Как одному сотруднику завести 204 промокода за 2 часа

Итак, вернемся к нашему координатору проектов. Суть ее задачи состояла в том, чтобы через особый интерфейс в 17 подписках завести по 12 промокодов, которые затем идут в некую базу данных, где для каждого такого кода генерируются тысячи уникальных кодов. Такова архитектура, менять ее мы не можем. 

Кроме того, необходимо к каждому коду указать даты его действия, размер предоставляемой скидки, название для облегчения поиска среди других кодов. Сам код нужно придумать, следуя специальному соглашению о нейминге. Помножив 17 на 12 – получаем 204 кода. На каждый тратим около 1-2 минут. Итого, в лучшем случае, уйдет 3,5 часа. А в реальности эта работа займет – 5-6 часов, которые уйдут на нажатия кнопок в интерфейсе и ввод данных в текстовые поля. Кажется, что это слишком простая работа для целого координатора проектов. Но такой работы много и кому-то надо ее выполнить, причем очень быстро. Зачастую бизнес ожидает, что коды будут заведены в тот же день, даже если задача поступает в 17:00.

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

Промокоды были придуманы нами неожиданно. Понятное дело, что система не может дорабатываться под «хотелки» команды каждого отдельного продукта по мере придумывания сотрудниками новых фич. Итогом наших прений была работа координатора проектов над задачей по ручному заведению 200+ промокодов 2-3 раза в неделю.

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

Как нам помог Jupyter, Python и Selenium

При помощи связки Jupyter+Python+Selenium можно в 4-5 раз ускорить известные, повторяемые изо дня в день, операции в интерфейсах. Selenium – известный продукт для симуляции действий в интерфейсах и тестирования. Python – простейший язык, доступный для освоения даже младшим школьникам. Jupyter – лучший инструмент моделирования алгоритмов. Для разработки мы использовали окружение Anaconda, поскольку оно сразу дает почти весь нужный инструментарий в готовом виде. Повозиться пришлось только с установкой Selenium и его драйвера для Google Chrome. Но про это написано множество статей и руководств, поэтому повторяться не будем.

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

Мы не думаем о тестировании – блокнот сам по себе инструмент тестирования гипотез о работе алгоритма. Мы тут же устанавливаем все нужные модули и смотрим справку всех функций, которые нас интересуют, описываем алгоритм словами, на языке разметки Markdown. Не знаю, как для разработчиков, а для менеджеров ничего лучше не придумано. Пожалуй, визуальное zero-code программирование посложнее будет. И оно менее гибкое, если нужен только бэк.

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

Сначала заводим свойства для нашего робота:

#Логин и пароль нашей Большой системы (в которой заводим промокоды)
lg = "oursuperman"
ps = "qwerty1234"
 
#Настройки промо-кампании общие
promoActionName = 'Всемирный день благодарности за хорошие продукты'
campaign_num = '003432' #Общий номер промокампании для облегчения поиска
campaign_length = '45' #Длительность промокампании
campaign_tariff_period = '45' #Через сколько начинаем брать деньги (да, это разные параметры)
tariff_category = '0 ₽' #Пока действует промокод не берём с клиента ни копейки
campaign_period_start = '20.10.2022' #Дата начала промокампании
campaign_period_stop = '28.10.2022' #Дата окончания
promocodes_generated = [] #Сюда будем складывать наши промики для статистики
#А тут у нас будут настройки промокампании по подпискам (те самые 17 подписок)
subs_to_promo = [
	{
    	'name':'МТС некая подписка 1',
    	'prefix':'N',
    	'num_promocodes':1
	},
	{
    	'name':'МТС некая подписка 2',
    	'prefix':'N',
        'num_promocodes':1
    }
]

Далее пишем основные функции, которые будем использовать из раза в раз:

def openContent(content_obj):
	#Открываем нужную подписку
	url = f"https://our.domain/subs/{content_obj['content_id']}"
	driver.get(url)
	
def processContent(content_obj):
	#Скролл вниз
	driver.find_element(By.TAG_NAME, "body").send_keys(Keys.PAGE_DOWN)
	#Открываем расхлоп "Промоакции"
	promo_block = driver.find_element(By.XPATH, "//span[text()=' Промоакции ']")
	promo_block.find_element(By.CSS_SELECTOR, "button").click()
	
def createPromocode(num_promocode):
	#Создаём новую промоакцию
	promo_block = driver.find_element(By.XPATH, "//span[text()=' Промоакции ']")
	promo_block.find_element(By.CSS_SELECTOR, "button.inserted").click()
	
def setFieldsPromocode(num_promocode):
	#Выставляем свойства промоакции
	f_dateRange = driver.find_element(By.NAME, "dateRange")
	f_dateRange.click()
    sleep(1) #Обязательно делаем паузы, чтобы было время прогрузиться элементам
    f_dateRange.send_keys(campaign_period_start) #Тут просто печатаем символы в поля ввода
	f_dateRange.send_keys(campaign_period_stop)
	#Добавляем название кампании
	f_promoActionName = driver.find_element(By.NAME, "promoActionName")
	f_promoActionName.find_element(By.CSS_SELECTOR, "input.input_111").send_keys(f"{promoActionName}_{num_promocode}")
	#Добавляем длительность
	f_period = driver.find_element(By.NAME, "period")
	f_period.find_element(By.CSS_SELECTOR, "input.input_222").send_keys(campaign_length)
	#Добавляем период тарификации
	f_tarifficationPeriod = driver.find_element(By.NAME, "tarifficationPeriod")
    f_tarifficationPeriod.find_element(By.CSS_SELECTOR, "input.input_333").send_keys(campaign_tariff_period)
	#Формируем текст промо-кода
	promocode = ""
    #Просто собираем текстовую строку. Проще конечно по шаблону, но тут нагляднее процесс сборки
    promocode += content_obj["prefix"] + "_"
    #... тут много всего в части соблюдения нашего соглашения о нейминге
	promocode += campaign_num + "_"
	promocode += str(num_promocode)
	#Добавляем текст промокода
	f_promoActionName = driver.find_element(By.NAME, "promoCode")
    f_promoActionName.find_element(By.CSS_SELECTOR, "input.input_444").send_keys(promocode)
	return promocode

Далее открываем браузер и логинимся при помощи Selenium:

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.by import By
from time import sleep
 
#Открываем сайт Большой системы (в которой заводим промокоды) при помощи драйвера Selenium
url = "https://big.system.domain"
driver = webdriver.Chrome(executable_path=r"C:\Chrome\DriverPath\chromedriver.exe")
driver.get(url)
#Обязательно ждём пока загрузится URL
sleep(5)
#Находим все нужные поля. Я для этого использовал XPath (см. режим разработчика в Google Chrome и правой кнопкой мыши по нужному нам элементу, там Copy Xpath)
login = "/html/body/div[2]/app-input/div/input"
password = "/html/body/div[2]/app-input-password/div/input"
btn_enter = "/html/body/app-form/form/input"
login_control = driver.find_element(By.XPATH, login)
password_control = driver.find_element(By.XPATH, password)
btn_enter = driver.find_element(By.XPATH, btn_enter)
 
#Логинимся
login_control.clear()
login_control.send_keys(lg)
password_control.clear()
password_control.send_keys(ps)
sleep(3)
btn_enter.click()

И в завершение, основная логика работы робота по заведению промокодов через интерфейс Большой системы:

#Цикл перебора подписок, в которых мы хотим завести промокоды (см. первый блок с настройками)
for content in list(range(0,len(subs_to_promo))):
	content_obj = subs_to_promo[content]
	#Тут уже вызываем описанную нами ранее функцию (см. второй блок с функциями)
    openContent(content_obj)
    #Всегда даём страничке прогрузиться
    sleep(3)
    #Задача этой функции открыть все нужные расхлопы на странице. У вас может быть по другому
	processContent(content_obj)
	sleep(3)
                                  	
	for num_promocode in list(range(1, content_obj['num_promocodes']+1)):
    	createPromocode(num_promocode)
        #Добавляем сгенерированный и положенный нами промокод для статистики
        promocodes_generated.append(setFieldsPromocode(num_promocode))
    	#Нажимаем кнопку "Создать"
    	driver.find_element(By.XPATH, f"//button[text()='Создать']").click()
    	sleep(8)
        
#В конце выводим список сгенерированных промокодов
print("Список промокодов:")
for code in promocodes_generated:
	print(code)

Заключение

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

Если что-то идет не так – всегда можно остановить выполнение и начать с того места на котором остановились. Это же Jupyter, он позволяет такие вещи. Выглядит просто, но оно работает и реально экономит время на другие задачи, много времени. 

Ранее для автоматизации рутины я использовал AutoIt, но этот продукт почти заброшен, он не развивается. Драйверы к браузерам перестали работать. Поэтому пришлось быстро осваивать Selenium, но оно того стоило. Его возможности гораздо шире. А Python мне намного привычнее языка, используемого в AutoIt. 

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

Спасибо за уделенное статье время! Надеюсь, вы сможете применить наш метод на практике и он поможет вам сделать работу более разнообразной и продуктивной. Если у вас есть вопросы, замечания или опыт решения подобных задач – жду вас в комментариях!

Комментарии (19)


  1. mrbald
    27.10.2022 09:53
    +4

    Я бы сделал без UI, какой-нибудь простенький workflow manager (хоть gnu make) и библиотеку requests для сервисных вызовов.

    Автоматизировать нужно, да.


    1. nsuvorov Автор
      27.10.2022 11:30
      +2

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


  1. vilgeforce
    27.10.2022 10:37
    +5

    Я все равно не понимаю зачем Юпитер? Что мешает запускать скрипт через python scriptname.py?


    1. eltardowut
      27.10.2022 11:18
      +2

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


      1. vilgeforce
        27.10.2022 11:31
        +2

        API это сложна...


      1. nsuvorov Автор
        27.10.2022 11:33
        +1

        Долго. В крупной компании с ИС в пром эксплуатации это целая не очень прямая история. Месяца два, если в роадмап влезу конечно.


    1. nsuvorov Автор
      27.10.2022 11:31
      +1

      Можно. Но я не был уверен, что всё работает так как я думаю. Поэтому сразу и изучал/вспоминал синтаксис и нужные модули и писал алгоритм. Я же менеджер) Отладка в IDE заняла бы больше времени.


      1. eltardowut
        27.10.2022 18:32
        +1

        Какой грамотный нынче менеджер пошёл, однако. Нет, кроме шуток, все бы так подкованы были по технической части.


  1. WondeRu
    27.10.2022 10:42
    +1

    О! Вы придумали RPA!)


    1. nsuvorov Автор
      27.10.2022 11:32
      +1

      Спасибо) Бесплатно и из подручных материалов.


  1. Amstremi
    27.10.2022 14:24
    +1

    Вот это да! Спасибо, покажу своим)


  1. NNikolay
    27.10.2022 16:44
    +2

    Мы примерно также делаем. Но когда алгоритм готов, то перекладываем из jupyter в streamlit. Так можно давать юзеру простой интерфейс с кнопкой и не пугать его видом блоков кода. И кладем почти всё на сервер, чтобы питон не ставить юзеру.


    1. nsuvorov Автор
      27.10.2022 17:51

      А как переводите в streamlit? Это же модуль питоновский? Расскажите пожалуйста чуть подробнее. Очень интересно.


      1. NNikolay
        27.10.2022 18:14
        +3

        Тут можно целую статью писать про streamlit. В jupyter удобно итеративно управлять селениумом пока пишешь последовательность действий. Потом весь код кладем в обычный .py файл и в нем же импортируем стримлит и рисует интерфейс. Он запускается в браузере как и jupyter, если на локали.

        Только нужно внимательно смотреть чтобы selenium ждал подгружения страницы. У Вас кстати это сделано через sleep. Лучше использовать webdriverwait. Селениум так умеет - если страница загрузится быстрее, то и скрипт быстрее отработает.

        Вообще у нас напилен целый “app store” с парой десятков приложений в стримлите и аутентификация юзера через корпоративный ldap.


        1. nsuvorov Автор
          27.10.2022 19:52
          +1

          Звучит очень круто! А можете написать об этом? Было бы полезно многим, я уверен. Сам после вашего комментария уже копаю streamlit.


  1. Dzenses
    27.10.2022 21:33
    +2

    Буквально сегодня возился с Selenium для автоматизации конфигурирования локального дев-стенда с чужим (черный ящик) беком/фронтом через веб-интерфейс. Инструмент очень крутой.

    Повозиться пришлось только с установкой Selenium и его драйвера для Google Chrome.

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

    pip install webdriver-manager

    from selenium import webdriver
    from selenium.webdriver.chrome.service import Service
    from webdriver_manager.chrome import ChromeDriverManager
    
    options = webdriver.ChromeOptions()
    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) 


    1. nsuvorov Автор
      27.10.2022 21:33

      Шикарная идея! Спасибо!


  1. VVakko
    27.10.2022 22:32
    +1

    Зачем локальный Selenium, если есть Selenoid? Хочешь, Chrome, хочешь Firefox. Любой версии и без проблем с веб-драйверами. Один раз настроил и запустил в Докере и дальше хоть в n потоков эти ваши RPA пускай. :)


    1. nsuvorov Автор
      27.10.2022 22:38

      Похоже на промышленное решение) посмотрим, спасибо за наводку!