Пример использования — мотивация и вызов
Всем привет! Я работал с Python в течение последних двух лет, но так и не научился правильному объектно-ориентированному программированию и шаблонам проектирования. В этом году я решил восполнить этот пробел, прочитав несколько книг и применив полученные знания к реальной проблеме. Я с нетерпением жду возможности многому научиться из всех предложений 🙂
Чтобы начать мое обучение, Я решил автоматизировать повторяющуюся еженедельную задачу по заполнению некоторых табелей учета рабочего времени. расположен в Microsoft Teams, используя бота, который выполняет за меня тяжелую работу. Бот должен выполнить следующие действия:
- Перейдите на страницу входа
- Введите логин и пароль
- Войти
- Перейдите на страницу Excel с расписанием
- Заполните мои недельные часы
В настоящее время бот выполняет почти все шаги, кроме двух последних, которые я еще не реализовал.
Разбор кода
Код довольно простой. Я сильно полагаюсь на селен для выполнения всех действий, поэтому я хочу создать экземпляр chrome, в котором агент будет выполнять свои действия.
Естественно, сначала я импортирую библиотеки, которые собираюсь использовать:
import os
import time
import random
from selenium import webdriver
from dataclasses import dataclass
from abc import ABC, abstractmethod
from webdriver_manager.chrome import ChromeDriverManager
Далее я определяю неизменяемые классы, единственная цель которых — хранить статичную информацию в контейнерах., чтобы можно было избежать дублирования кода.
@dataclass(frozen=True)
class XPathsContainer:
teams_login_button: str="//*[@id="mectrl_main_trigger"]/div/div[1]"
teams_login_user_button: str="//*[@id="i0116"]"
teams_login_next_button: str="//*[@id="idSIButton9"]"
teams_login_pwd_button: str="//*[@id="i0118"]"
teams_sign_in_button: str="//*[@id="idSIButton9"]"
teams_sign_in_keep_logged_in: str="//*[@id="KmsiCheckboxField"]"
@dataclass(frozen=True)
class UrlsContainer:
teams_login_page: str="https://www.microsoft.com/en-in/microsoft-365/microsoft-teams/group-chat-software"
Теперь я пытаюсь реализовать базовый класс, который называется Driver
. Этот класс содержит инициализацию объекта Chrome и устанавливает основы для наследования других агентов.. Каждый Agent
дочерний класс может иметь (в будущем) разные действия, но у них должен быть метод сна (чтобы избежать ограничений при использовании ботов), они должны иметь возможность щелкать, записывать информацию и переходить по страницам.
class Driver(ABC):
def __init__(self, action, instruction, driver=None):
if driver:
self.driver = driver
else:
self.driver = webdriver.Chrome(ChromeDriverManager().install())
self.actions = {
'navigate': self.navigate,
'click': self.click,
'write': self.write
}
self.parameters = {
'action': None,
'instruction': None
}
@abstractmethod
def sleep(self, current_tick=1):
pass
@abstractmethod
def navigate(self, *args):
pass
@abstractmethod
def click(self, *args):
pass
@abstractmethod
def write(self, **kwargs):
pass
@abstractmethod
def main(self, **kwargs):
pass
Сейчас реализую базовый Agent
дочерний класс, реализующий логику требуемых функций базового класса Driver
.
class Agent(Driver):
def __init__(self, action, instruction, driver):
super().__init__(action, instruction, driver)
self.action = action
self.instruction = instruction
def sleep(self, current_tick=1):
seconds = random.randint(3, 7)
timeout = time.time() + seconds
while time.time() <= timeout:
time.sleep(1)
print(f"Sleeping to replicate user.... tick {current_tick}/{seconds}")
current_tick += 1
def navigate(self, url):
print(f"Agent navigating to {url}...")
return self.driver.get(url)
def click(self, xpath):
print(f"Agent clicking in '{xpath}'...")
return self.driver.find_element_by_xpath(xpath).click()
def write(self, args):
xpath = args[0]
phrase = args[1]
print(f"Agent writing in '{xpath}' the phrase '{phrase}'...")
return self.driver.find_element_by_xpath(xpath).send_keys(phrase)
def main(self, **kwargs):
self.action = kwargs.get('action', self.action)
self.instruction = kwargs.get('instruction', self.instruction)
self.actions[self.action](self.instruction)
self.sleep()
Наконец, я создал функцию, которая обновляет параметры класса всякий раз, когда есть набор действий и инструкций, которые необходимо выполнить под тем же драйвером Chrome. И я создал функцию, которая принимает сценарий действий и выполняет их.
def update_driver_parameters(driver, values):
params = driver.parameters
params['action'] = values[0]
params['instruction'] = values[1]
return params
def run_script(script):
for script_line, script_values in SCRIPT.items():
chrome = Agent(None, None, None)
for instructions in script_values:
params = update_driver_parameters(chrome, instructions)
chrome.main(**params)
chrome.sleep()
USER = os.environ["USERNAME"]
SECRET = os.environ["SECRET"]
SCRIPT = {
'login': [
('navigate', UrlsContainer.teams_login_page),
('click', XPathsContainer.teams_login_button),
('write', (XPathsContainer.teams_login_user_button, USER)),
('click', XPathsContainer.teams_login_next_button),
('write', (XPathsContainer.teams_login_pwd_button, SECRET)),
('click', XPathsContainer.teams_sign_in_button),
('click', XPathsContainer.teams_sign_in_keep_logged_in),
('click', XPathsContainer.teams_sign_in_button),
]
}
run_script(SCRIPT)
Обеспокоенность
Сейчас я думаю, что у кода есть несколько серьезных проблем, в основном связанных с неопытностью в шаблонах проектирования:
- Я слишком полагаюсь на Xpaths, чтобы заставить бота делать что-то, что приведет к огромному классу данных, если нужно сделать много шагов;
- Кроме того, полагаться на Xpaths может быть плохо, потому что, если страница будет обновлена, мне придется повторить шаги, но это, вероятно, неизбежное зло;
- Я не уверен, правильна ли реализация неизменяемого класса. Я использовал
dataclass
за это; - У меня такое ощущение, что реализованное мной наследование довольно неуклюже. Я хочу иметь возможность использовать один и тот же драйвер вместе с несколькими классами. Я не хочу создавать новый драйвер для каждого действия, я всегда хочу получить последний контекст, который сделал драйвер, но если создается новый агент, то этому агенту должен быть назначен новый драйвер;
- Может быть
kwargs
аргументы могут быть реализованы по-разному, я никогда не уверен, как правильно их проанализировать без использованияkwargs.get
; - Непоследовательное использование args и kwargs, можно ли это реализовать по-другому?