Форум программистов, компьютерный форум, киберфорум
IndentationError
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Веб-автоматизация с Python и Selenium

Запись от IndentationError размещена 25.06.2025 в 19:28
Показов 10739 Комментарии 0

Нажмите на изображение для увеличения
Название: Веб-автоматизация с Python и Selenium.jpg
Просмотров: 1205
Размер:	69.3 Кб
ID:	10927
Selenium с Python — это комбинация, которая выдержала проверку временем. Несмотря на появление новых инструментов вроде Playwright или Puppeteer, связка Python-Selenium остаётся золотым стандартом для множества задач автоматизации. Почему? Да потому что она невероятно гибкая и при этом относительно простая в освоении.

Какие задачи решает веб-автоматизация? Спектр применения удивительно широк. От классического тестирования веб-приложений до сложных систем мониторинга цен конкурентов, от автоматического заполнения форм и генерации отчётов до создания ботов для социальных сетей. Я лично автоматизировал процесс бронирования билетов на концерты, которые разлетаются за секунды после начала продаж — попробуйте сделать это вручную!

Особенно ценной автоматизация становится при работе с одностраничными приложениями (SPA) на React, Vue или Angular. Эти фреймворки рендерят контент динамически, и часто единственный способ надёжно взаимодействовать с ними — через реальный браузер. К тому же, современные сайты часто используют сложные проверки для отсеивания ботов, и иногда только полноценная браузерная сессия позволяет обойти такие ограничения.

С технической точки зрения, Selenium позволяет запускать "безголовый" браузер, который выполняет команды без отображения графического интерфейса. Это открывает массу возможностей для автоматизации рутинных задач, экономя часы ручного труда. А если добавить сюда возможности машиного обучения для распознавания контента — получается мощный инструмент.

Основы работы с Selenium в Python



Когда я впервые столкнулся с Selenium, меня поразила простота взаимодействия с этим инструментом через Python. Selenium — это целый фреймворк для автоматизации браузеров, который существует с 2004 года. В основе его работы лежит компонент WebDriver — интерфейс, позволяющий управлять браузером через специализированный драйвер. Архитектура Selenium состоит из нескольких ключевых компонентов. Клиентские библиотеки (в нашем случае Python-биндинги) отправляют команды драйверу браузера, который в свою очередь взаимодействует с самим браузером. Такая многослойная структура обеспечивает гибкость и кроссплатформенность решения. Я могу написать скрипт на своем MacBook, а запустить его на Windows-сервере — и все будет работать одинаково.

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
 
# Настройка опций браузера
options = Options()
options.add_argument("--headless")  # Запуск в безголовом режиме
 
# Создание экземпляра драйвера
driver = webdriver.Firefox(options=options)
 
# Открытие веб-страницы
driver.get("https://python.org")
 
# Получение заголовка страницы
print(driver.title)
 
# Закрытие браузера
driver.quit()
Этот простой пример демонстрирует основной поток работы с Selenium: инициализация драйвера, открытие страницы, выполнение операций и закрытие браузера. Обратите внимание на строку driver.quit() — это критически важная операция, которую часто забывают. Если не закрыть драйвер правильно, в системе могут остаться "призрачные" процессы браузера, пожирающие ресурсы.

Одна из самых мощных возможностей Selenium — способность находить элементы на странице и взаимодействовать с ними. Существует несколько стратегий поиска элементов:

Python
1
2
3
4
5
6
7
8
9
10
11
# Поиск по ID
element = driver.find_element(By.ID, "login-button")
 
# Поиск по CSS-селектору
element = driver.find_element(By.CSS_SELECTOR, ".product-card:nth-child(2)")
 
# Поиск по XPath
element = driver.find_element(By.XPATH, "//button[contains(text(), 'Отправить')]")
 
# Поиск по тексту ссылки
element = driver.find_element(By.LINK_TEXT, "Забыли пароль?")
Важно понимать, что современный интерфейс Selenium для Python рекомендует использовать методы find_element() и find_elements() с явным указанием типа локатора через класс By. Старый стиль с методами вроде find_element_by_id() считается устаревшим и может перестать работать в будущих версиях.
После нахождения элемента вы можете выполнять различные действия:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Клик по элементу
element.click()
 
# Ввод текста
element.send_keys("Привет, Selenium!")
 
# Очистка поля ввода
element.clear()
 
# Получение текста элемента
text = element.text
 
# Получение атрибута
href = element.get_attribute("href")
Что меня особенно восхищает в Selenium — это возможность эмулировать сложные действия пользователя через цепочки ActionChains. Я часто использую их для перетаскивания элементов, наведения курсора и выполнения последовательностей действий:

Python
1
2
3
4
5
6
7
8
9
10
11
12
from selenium.webdriver.common.action_chains import ActionChains
 
# Создание объекта ActionChains
actions = ActionChains(driver)
 
# Наведение курсора на элемент
menu = driver.find_element(By.ID, "main-menu")
actions.move_to_element(menu).perform()
 
# Клик на подменю, которое появляется при наведении
submenu = driver.find_element(By.ID, "submenu-item")
actions.click(submenu).perform()
Selenium позволяет даже эмулировать нажатия клавиш, что бывает необходимо для некоторых веб-приложений с продвинутыми интерфейсами:

Python
1
2
3
4
5
6
7
from selenium.webdriver.common.keys import Keys
 
# Нажатие Ctrl+A (выделить всё)
element.send_keys(Keys.CONTROL, "a")
 
# Нажатие Enter
element.send_keys(Keys.ENTER)
Обязательный момент, который я часто наблюдаю у новичков в Selenium — это проблемы с синхронизацией. Современные веб-приложения загружают контент асинхронно, и если ваш скрипт пытается взаимодействовать с элементом до его появления на странице, вы получите исключение. Selenium предлагает два механизма ожидания:

Python
1
2
3
4
5
6
7
8
9
10
11
# Неявное ожидание (устанавливается один раз для всей сессии)
driver.implicitly_wait(10)  # Ждать до 10 секунд при поиске элементов
 
# Явное ожидание (более гибкое, для конкретных условий)
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
 
# Ждем, пока элемент станет кликабельным
element = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.ID, "dynamic-button"))
)
Я предпочитаю явные ожидания, хотя они и требуют больше кода. Они дают точный контроль над тем, что именно вы ждете — появление элемента, его кликабельность, изменение текста и так далее. К тому же explicit wait умеет ждать более сложные условия:

Python
1
2
3
4
5
6
7
# Ожидание загрузки страницы по изменению заголовка
WebDriverWait(driver, 10).until(EC.title_contains("Готово"))
 
# Ожидание исчезновения элемента (например, индикатора загрузки)
WebDriverWait(driver, 10).until(
    EC.invisibility_of_element_located((By.ID, "loading-spinner"))
)
Ещё один частый сценарий — работа с несколькими окнами или вкладками. Selenium позволяет переключаться между ними, что бывает необходимо, например, при тестировании функционала, открывающего новые окна:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Запоминаем идентификатор текущего окна
original_window = driver.current_window_handle
 
# Клик по кнопке, открывающей новое окно
driver.find_element(By.ID, "open-new-window").click()
 
# Ждем появления нового окна и переключаемся на него
WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) > 1)
for window_handle in driver.window_handles:
    if window_handle != original_window:
        driver.switch_to.window(window_handle)
        break
 
[H2]Выполняем действия в новом окне...[/H2]
 
# Возвращаемся в исходное окно
driver.switch_to.window(original_window)
Selenium также позволяет взаимодействовать с JavaScript на странице, что открывает огромные возможности. Я часто использую это для обхода ограничений или выполнения действий, которые сложно реализовать через стандартный API:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
# Выполнение JavaScript для прокрутки страницы
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
 
# Изменение стилей элемента
driver.execute_script(
    "arguments[0].style.backgroundColor = 'yellow'", 
    element
)
 
# Извлечение данных, недоступных через стандартный API
data = driver.execute_script(
    "return window.myAppData;"  # Доступ к данным в глобальной переменной JS
)
Для отладки автоматизированных скриптов невероятно полезно уметь делать скриншоты:

Python
1
2
3
4
5
# Сохранение скриншота всей страницы
driver.save_screenshot("screenshot.png")
 
# Сохранение скриншота конкретного элемента
element.screenshot("element.png")
При работе с современными веб-приложениями часто приходится иметь дело с всплывающими окнами и диалогами. Selenium предоставляет API для взаимодействия с ними:

Python
1
2
3
4
5
6
7
8
9
# Принятие JavaScript-алерта
alert = driver.switch_to.alert
alert.accept()  # Нажатие "OK"
 
# Или отклонение
alert.dismiss()  # Нажатие "Отмена"
 
# Ввод текста в промпт
alert.send_keys("Текст для промпта")
Важный аспект, о котором я не упомянул ранее — управление cookies. Это полезно для тестирования аутентификации или сохранения состояния между запусками:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Добавление cookie
driver.add_cookie({
    "name": "session_id", 
    "value": "12345", 
    "domain": "example.com"
})
 
# Получение всех cookies
cookies = driver.get_cookies()
 
# Удаление определенной cookie
driver.delete_cookie("session_id")
 
# Удаление всех cookies
driver.delete_all_cookies()

Python Selenium Прокрутка Веб Элемента
Здравствуйте! Есть небольшая программа, которая должна изменять время публикации поста в группе ВК....

Python+Selenium. Как управлять уже открытой веб-страницей
Эта страница требует авторизацию с помощью ЭЦП (Подписание с ЭЦП проводится посторонним приложением...

selenium.common.exceptions.NoSuchElementException и selenium.common.exceptions.ElementNotInteractableException
Хочу создать бота для авто-ставки на сайте(luckyduck.app), делаю проверку на существование блока...

Selenium webdrive автоматизация браузера
Привет. Никто не знает как управлять элементами яндекса при автоматизированном тестировании. В...


Установка и настройка среды разработки



Для начала вам понадобится Python версии 3.8 или выше. Если вы ещё не установили его, скачайте установщик с официального сайта Python. После установки Python необходимо создать виртуальное окружение. Это изолированная среда, которая позволяет избежать конфликтов между зависимостями разных проектов. Создать её можно с помощью модуля venv:

Python
1
2
3
4
5
6
7
# Для Windows
python -m venv venv
.\venv\Scripts\activate
 
# Для macOS/Linux
python3 -m venv venv
source venv/bin/activate
Когда виртуальное окружение активировано, установите Selenium с помощью pip:

Python
1
pip install selenium
Теперь самая интересная часть — установка драйвера браузера. Selenium не может напрямую управлять браузерами, ему требуется специальный драйвер. Для Firefox нужен GeckoDriver, для Chrome — ChromeDriver, для Edge — EdgeDriver и так далее. Я обычно работаю с Firefox, поэтому остановлюсь на нём подробнее.

Скачайте GeckoDriver с официального репозитория Mozilla, соответствующий вашей операционной системе. Для Windows это будет .exe файл, для macOS и Linux — исполняемый бинарный файл. После скачивания разместите драйвер в директории, которая находится в PATH, чтобы Selenium мог его найти. Альтернативно можно указывать полный путь при инициализации:

Python
1
driver = webdriver.Firefox(executable_path='/путь/к/geckodriver')
Но лучше всё-таки добавить директорию с драйвером в PATH:
Windows: скопируйте драйвер в C:\Windows\System32 или другую директорию в PATH,
macOS/Linux: переместите драйвер в /usr/local/bin и сделайте его исполняемым с помощью chmod +x /usr/local/bin/geckodriver

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

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
 
options = Options()
options.add_argument("--headless")  # Для запуска без интерфейса
 
try:
    driver = webdriver.Firefox(options=options)
    driver.get("https://www.python.org")
    print(f"Титул страницы: {driver.title}")
    driver.quit()
    print("Установка Selenium прошла успешно!")
except Exception as e:
    print(f"Ошибка: {e}")
Если скрипт выполнился без ошибок и вывел заголовок страницы, значит ваша среда настроена правильно.
При настройке могут возникнуть типичные проблемы. Например, если вы видите ошибку WebDriverException: Message: 'geckodriver' executable needs to be in PATH, значит Selenium не может найти драйвер. Убедитесь, что он находится в директории из PATH или укажите полный путь к нему. Другая распространенная проблема — несоответсвие версий браузера и драйвера. Каждая версия браузера требует определенную версию драйвера, поэтому регулярно обновляйте оба компонента.
Для более продвинутой работы я рекомендую установить дополнительные пакеты:

Python
1
2
3
4
pip install webdriver-manager  # Автоматическое управление драйверами
pip install pytest  # Для написания тестов
pip install pytest-selenium  # Интеграция Selenium с pytest
pip install allure-pytest  # Для красивых отчетов
Особенно удобен webdriver-manager, который автоматически скачивает и настраивает нужную версию драйвера:

Python
1
2
3
4
from selenium import webdriver
from webdriver_manager.firefox import GeckoDriverManager
 
driver = webdriver.Firefox(executable_path=GeckoDriverManager().install())
Эта конфигурация избавит вас от головной боли с ручным обновлением драйверов при обновлении браузера. Теперь, когда ваша среда готова, можно приступать к реальной автоматизации браузера.

Практические аспекты поиска элементов



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

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

CSS-селекторы обычно более производительны и проще в написании для стандартных задач. Они отлично подходят для поиска по классам, идентификаторам и атрибутам:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Поиск по ID
driver.find_element(By.CSS_SELECTOR, "#login-form")
 
# Поиск по классу
driver.find_element(By.CSS_SELECTOR, ".button-primary")
 
# Комбинирование селекторов
driver.find_element(By.CSS_SELECTOR, "div.modal-content button.close")
 
# Поиск по атрибутам
driver.find_element(By.CSS_SELECTOR, "input[type='email'][required]")
 
# Использование псевдоклассов
driver.find_element(By.CSS_SELECTOR, "li:nth-child(3)")
XPath, в свою очередь, предлагает более мощные возможности навигации по DOM-дереву. Я часто обращаюсь к нему, когда нужно найти элемент по тексту или относительному расположению:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Поиск по тексту
driver.find_element(By.XPATH, "//button[text()='Отправить']")
 
# Частичное совпадение текста
driver.find_element(By.XPATH, "//div[contains(text(), 'Привет')]")
 
# Поиск родительского элемента
driver.find_element(By.XPATH, "//input[@id='email']/parent::div")
 
# Поиск по позиции
driver.find_element(By.XPATH, "(//table//tr)[3]")
 
# Поиск по нескольким условиям
driver.find_element(By.XPATH, "//button[@type='submit' and @disabled]")
Распространеное заблуждение, будто XPath работает медленнее CSS. На практике разница настолько незначительна, что редко влияет на производительность автоматизации. Выбирайте тот инструмент, который лучше решает конкретную задачу.

Как я определяю локаторы для элементов? Обычно использую инструменты разработчика в браузере. В Chrome или Firefox достаточно кликнуть правой кнопкой на элементе и выбрать "Inspect" или "Исследовать". Затем можно посмотреть HTML-структуру и атрибуты. Особено полезен селектор копирования — правый клик на элементе в DevTools и "Copy" → "Copy selector" или "Copy XPath". Но внимание! Автоматически сгенерированные селекторы часто оказываются хрупкими. Например, такой XPath:

Python
1
/html/body/div[2]/div[3]/form/div[2]/input
Хотя и находит элемент, но при малейшем изменении структуры страницы перестанет работать. Я предпочитаю создавать более устойчивые локаторы:

Python
1
2
# Вместо абсолютного пути используем ID или другие стабильные атрибуты
driver.find_element(By.CSS_SELECTOR, "#search-form input[name='query']")
При работе с современными фреймворками вроде React или Angular вы столкнетесь с динамически генерируемыми классами и атрибутами. Например, React может добавлять к классам хеши: button_primary__1a2b3c. Такие селекторы не стоит использовать напрямую, поскольку при пересборке приложения хеши изменятся. Вместо этого я ищу более стабильные атрибуты или использую частичное совпадение:

Python
1
2
3
4
5
# Используем частичное совпадение класса
driver.find_element(By.CSS_SELECTOR, "[class*='button_primary']")
 
# Или data-атрибуты, которые часто добавляют специально для тестирования
driver.find_element(By.CSS_SELECTOR, "[data-testid='submit-button']")
Для комплексного поиска иногда приходится комбинировать несколько стратегий. Например, сначала найти контейнер, а затем искать внутри него:

Python
1
2
3
4
5
# Сначала находим форму
form = driver.find_element(By.ID, "registration-form")
 
# Затем ищем кнопку внутри формы
submit_button = form.find_element(By.CSS_SELECTOR, "button[type='submit']")
Такой подход не только повышает точность поиска, но и ускоряет выполнение, поскольку поиск выполняется не по всей странице, а в ограниченом контексте.
Часто возникают ситуации, когда на странице есть несколько похожих элементов, и нужно выбрать конкретный. Метод find_elements возвращает список всех подходящих элементов:

Python
1
2
3
4
5
# Находим все чекбоксы
checkboxes = driver.find_elements(By.CSS_SELECTOR, "input[type='checkbox']")
 
# Кликаем по третьему чекбоксу
checkboxes[2].click()
Помните, что индексация начинается с нуля, поэтому checkboxes[2] — это третий элемент в списке.
Для обработки динамических списков я часто использую методы фильтрации. Допустим, нам нужно найти элемент с определенным текстом среди множества похожих:

Python
1
2
3
4
5
6
7
8
9
10
# Находим все элементы списка
items = driver.find_elements(By.CSS_SELECTOR, ".item")
 
# Фильтруем по текстовому содержимому
target_item = next((item for item in items if "Нужный текст" in item.text), None)
 
if target_item:
    target_item.click()
else:
    print("Элемент не найден!")
Такой подход гораздо надежнее прямого индексирования, особенно когда порядок элементов может меняться.
Отдельная боль — работа с фреймами и iframe. Selenium требует явного переключения контекста перед взаимодействием с содержимым фрейма:

Python
1
2
3
4
5
6
7
8
9
10
11
12
# Переключение на фрейм по индексу
driver.switch_to.frame(0)
 
# Переключение по имени или id
driver.switch_to.frame("frame_name")
 
# Переключение по элементу
frame_element = driver.find_element(By.CSS_SELECTOR, "iframe.content")
driver.switch_to.frame(frame_element)
 
# После работы с фреймом вернуться к основному содержимому
driver.switch_to.default_content()
Забыв переключиться на нужный фрейм, вы получите NoSuchElementException, даже если элемент визуально присутствует на странице. Я потратил немало часов на отладку, пока не научился автоматически проверять наличие фреймов.

Современные веб-приложения активно используют Shadow DOM для инкапсуляции компонентов. Обычные селекторы не проникают через границу Shadow DOM, но в Selenium 4 появилась встроеная поддержка таких структур:

Python
1
2
3
host = driver.find_element(By.CSS_SELECTOR, "my-component")
shadow_root = host.shadow_root
shadow_content = shadow_root.find_element(By.CSS_SELECTOR, ".inner-element")
Для более старых версий Selenium приходится использовать JavaScript:

Python
1
2
3
4
shadow_content = driver.execute_script(
    "return arguments[0].shadowRoot.querySelector('.inner-element')",
    host
)
Еще одна интересная возможность появилась в Selenium 4 — относительные локаторы. Они позволяют находить элементы на основе их положения относительно других элементов:

Python
1
2
3
4
5
6
7
8
9
10
11
from selenium.webdriver.support.relative_locator import locate_with
 
# Найти элемент справа от заданного
element = driver.find_element(
    locate_with(By.TAG_NAME, "button").to_right_of(reference_element)
)
 
# Или над заданным элементом
element = driver.find_element(
    locate_with(By.TAG_NAME, "div").above(reference_element)
)
Это особенно полезно в ситуациях, когда у элемента нет уникальных идентификаторов, но известно его визуальное положение относительно других элементов.

При отладке проблем с локаторами я обычно использую несколько приемов:
1. Временно отключаю безголовый режим, чтобы видеть, что происходит.
2. Добавляю "подсветку" элементов через JavaScript:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
def highlight(element, driver, duration=3):
    original_style = element.get_attribute("style")
    driver.execute_script(
        "arguments[0].setAttribute('style', arguments[1]);",
        element,
        "border: 2px solid red; background: yellow;"
    )
    time.sleep(duration)
    driver.execute_script(
        "arguments[0].setAttribute('style', arguments[0]);", 
        element, 
        original_style
    )
3. Использую пошаговую отладку с брейкпоинтами, чтобы проверить состояние страницы в конкретный момент времени
4. Добавляю скриншоты в ключевых точках скрипта:

Python
1
driver.save_screenshot(f"debug_{timestamp}.png")
Ещё один подход, который часто выручает — создание "здоровых" кастомных локаторов для элементов, с которыми часто возникают проблемы. Я создаю классы, инкапсулирующие логику поиска:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ComplexElement:
    def __init__(self, driver):
        self.driver = driver
    
    def find(self):
        # Сложная логика поиска с несколькими попытками и стратегиями
        try:
            return self.driver.find_element(By.ID, "preferred-id")
        except NoSuchElementException:
            try:
                return self.driver.find_element(By.CSS_SELECTOR, ".alternative-class")
            except NoSuchElementException:
                # Последний вариант
                return self.driver.find_element(By.XPATH, "//div[contains(text(), 'Уникальный текст')]")
Такой подход позволяет разделить логику взаимодействия с элементами и логику их поиска, что делает код более поддерживаемым.

Особого внимания заслуживают всплывающие окна и модальные диалоги. Часто они имеют отдельную структуру DOM и требуют специфичных подходов к поиску. Я обычно создаю отдельный класс для работы с модальными окнами:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
class ModalDialog:
    def __init__(self, driver):
        self.driver = driver
        self.modal = driver.find_element(By.CSS_SELECTOR, ".modal.active")
    
    def get_title(self):
        return self.modal.find_element(By.CSS_SELECTOR, ".modal-header").text
    
    def close(self):
        self.modal.find_element(By.CSS_SELECTOR, ".close-button").click()
        WebDriverWait(self.driver, 10).until(
            EC.invisibility_of_element_located((By.CSS_SELECTOR, ".modal.active"))
        )
В целом, успех автоматизации во многом зависит от качества локаторов. Стремитесь к использованию стабильных, однозначных и надежных селекторов, которые не сломаются при небольших изменениях интерфейса. Иногда стоит обсудить с командой разработчиков возможность добавления специальных атрибутов вроде data-testid именно для целей автоматизации.

Работа с динамическим контентом



Одна из самых сложных задач при автоматизации веб-приложений — взаимодействие с динамическим контентом. В современном вебе данные часто подгружаются асинхронно через AJAX, элементы появляются в ответ на действия пользователя, а состояние интерфейса меняется без перезагрузки страницы. Я вспоминаю свой первый серьезный проект с Selenium — большинство времени ушло не на написание тестов, а на битву с внезапно исчезающими элементами и состояниями гонки. Ключевой принцип работы с динамическим контентом — правильное ожидание. Мы уже обсуждали implicitly_wait и явные ожидания, но давайте рассмотрим более продвинутые техники.

При работе с приложениями на React, Vue или Angular я регулярно сталкиваюсь с необходимостью ожидать не просто появления элемента, а определенного его состояния. Вот несколько полезных ожиданий:

Python
1
2
3
4
5
6
7
8
9
10
# Ожидание изменения текста в элементе
def text_changed(locator, original_text):
    def check(driver):
        element = driver.find_element(*locator)
        return element.text != original_text
    return check
 
original = driver.find_element(By.ID, "counter").text
driver.find_element(By.ID, "increment").click()
WebDriverWait(driver, 10).until(text_changed((By.ID, "counter"), original))
Для обработки бесконечной прокрутки я создал свой кастомный обработчик ожидания:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def scroll_and_wait_for_more_items(driver, item_locator, initial_count, max_scrolls=5):
    scrolls = 0
    current_count = len(driver.find_elements(*item_locator))
    
    while current_count <= initial_count and scrolls < max_scrolls:
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        try:
            WebDriverWait(driver, 5).until(
                lambda d: len(d.find_elements(*item_locator)) > current_count
            )
            current_count = len(driver.find_elements(*item_locator))
        except TimeoutException:
            scrolls += 1
    
    return current_count > initial_count
Часто приходится иметь дело со спиннерами загрузки. Стратегия здесь обычно такая: дождаться появления спиннера, а затем его исчезновения:

Python
1
2
3
4
5
6
7
8
9
# Ждем появления спиннера
WebDriverWait(driver, 5).until(
    EC.presence_of_element_located((By.CLASS_NAME, "loading-spinner"))
)
 
# Затем ждем его исчезновения
WebDriverWait(driver, 30).until(
    EC.invisibility_of_element_located((By.CLASS_NAME, "loading-spinner"))
)
Если вы работаете с Angular-приложениями, полезно дождаться завершения всех запросов:

Python
1
2
3
4
5
6
def angular_requests_completed(driver):
    return driver.execute_script(
        "return window.getAllAngularTestabilities().findIndex(x => !x.isStable()) === -1"
    )
 
WebDriverWait(driver, 15).until(angular_requests_completed)
С React-приложениями ситуация сложнее из-за отсутствия глобального индикатора загрузки, но иногда можно отслеживать запросы через Network API:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def network_idle(driver, timeout=3000):
    return driver.execute_script("""
        return new Promise(resolve => {
            const startTime = new Date().getTime();
            let requestCount = 0;
            
            const originalOpen = XMLHttpRequest.prototype.open;
            XMLHttpRequest.prototype.open = function() {
                requestCount++;
                originalOpen.apply(this, arguments);
            };
            
            const check = () => {
                if (requestCount === 0 && new Date().getTime() - startTime > timeout) {
                    resolve(true);
                } else {
                    requestCount = 0;
                    setTimeout(check, timeout);
                }
            };
            
            setTimeout(check, timeout);
        });
    """)
Еще одна хитрость, которая меня часто выручает при тестировании SPA — ожидание загрузки определенных ресурсов:

Python
1
2
3
4
5
6
7
8
9
10
def resource_loaded(url_pattern):
    def check(driver):
        resources = driver.execute_script(
            "return window.performance.getEntriesByType('resource')"
        )
        return any(url_pattern in resource['name'] for resource in resources)
    return check
 
# Ждем загрузки важного JSON-файла с данными
WebDriverWait(driver, 10).until(resource_loaded("data.json"))
При работе с динамическими списками иногда бывает полезно дождаться определенного количества элементов:

Python
1
2
3
4
5
6
7
8
9
def element_count_greater_than(locator, count):
    def check(driver):
        elements = driver.find_elements(*locator)
        return len(elements) > count
    return check
 
WebDriverWait(driver, 10).until(
    element_count_greater_than((By.CSS_SELECTOR, ".product-card"), 5)
)
Не забывайте про обработку исключений при работе с динамическим контентом. Часто ошибка может означать просто, что мы не дождались нужного состояния:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
def safe_click(driver, locator, max_attempts=3):
    attempt = 0
    while attempt < max_attempts:
        try:
            element = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable(locator)
            )
            element.click()
            return True
        except (StaleElementReferenceException, ElementClickInterceptedException):
            attempt += 1
            time.sleep(0.5)
    raise Exception(f"Failed to click element {locator} after {max_attempts} attempts")

Продвинутые техники автоматизации



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

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
 
# Имитация клика с зажатым Shift
element = driver.find_element(By.ID, "interactive-canvas")
actions = ActionChains(driver)
actions.key_down(Keys.SHIFT).click(element).key_up(Keys.SHIFT).perform()
 
# Перетаскивание элемента
source = driver.find_element(By.ID, "draggable")
target = driver.find_element(By.ID, "drop-zone")
actions.drag_and_drop(source, target).perform()
 
# Более сложное перетаскивание с паузами
actions.click_and_hold(source)
actions.pause(0.5)  # Пауза в секундах
actions.move_to_element(target)
actions.pause(0.5)
actions.release()
actions.perform()
Я часто сталкиваюсь с необходимостью эмулировать движение мыши по определенной траектории. Например, при тестировании карт или интерактивных графиков:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def move_in_pattern(driver, element, points):
    actions = ActionChains(driver)
    actions.move_to_element(element)
    for x_offset, y_offset in points:
        actions.move_by_offset(x_offset, y_offset)
        actions.pause(0.1)
    actions.perform()
 
# Движение по кругу
center = driver.find_element(By.ID, "canvas-center")
radius = 50
points = [(int(radius * math.cos(math.radians(angle))), 
           int(radius * math.sin(math.radians(angle)))) 
          for angle in range(0, 360, 30)]
move_in_pattern(driver, center, points)
Еще одна область, где продвинутые техники незаменимы — работа с файлами. Selenium позволяет загружать файлы, указывая путь к ним через элемент ввода:

Python
1
2
file_input = driver.find_element(By.CSS_SELECTOR, "input[type='file']")
file_input.send_keys("/абсолютный/путь/к/файлу.jpg")
Но иногда элемент загрузки скрыт или стилизован JavaScript-библиотекой. В таких случаях я использую JavaScript для временного изменения видимости:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def upload_to_hidden_input(driver, file_input, file_path):
    # Делаем скрытый input видимым
    driver.execute_script(
        "arguments[0].style.display = 'block'; arguments[0].style.visibility = 'visible';", 
        file_input
    )
    
    # Загружаем файл
    file_input.send_keys(file_path)
    
    # Возвращаем исходные стили
    driver.execute_script(
        "arguments[0].style.display = 'none'; arguments[0].style.visibility = 'hidden';", 
        file_input
    )
При работе с одностраничными приложениями я часто манипулирую локальным хранилищем для настройки начального состояния:

Python
1
2
3
4
5
6
7
8
# Установка значения в localStorage
driver.execute_script("localStorage.setItem('user_token', 'test_token_123');")
 
# Чтение значения
token = driver.execute_script("return localStorage.getItem('user_token');")
 
# Очистка хранилища
driver.execute_script("localStorage.clear();")
Эта техника позволяет обойти логику авторизации или настроить приложение в определенное состояние без прохождения всех предшествующих шагов.

Одна из самых сложных проблем в автоматизации — обход CAPTCHA и других защит от ботов. Хотя я категорически против использования автоматизации для злонамеренных целей, иногда в тестовых средах приходится обходить такие системы:

Python
1
2
3
4
5
6
7
8
9
10
# Отключение WebDriver-флагов в Chrome
options = webdriver.ChromeOptions()
options.add_argument('--disable-blink-features=AutomationControlled')
 
# Установка случайного user-agent
user_agents = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15'
]
options.add_argument(f'--user-agent={random.choice(user_agents)}')
Для эмуляции мобильных устройств я использую специальные настройки драйвера:

Python
1
2
3
4
5
6
7
8
mobile_emulation = {
    "deviceMetrics": { "width": 360, "height": 640, "pixelRatio": 3.0 },
    "userAgent": "Mozilla/5.0 (Linux; Android 10; SM-G981B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Mobile Safari/537.36"
}
 
chrome_options = webdriver.ChromeOptions()
chrome_options.add_experimental_option("mobileEmulation", mobile_emulation)
driver = webdriver.Chrome(options=chrome_options)
Это позволяет тестировать адаптивный дизайн и мобильную версию сайта на настольном компьютере.
Для работы с несколькими вкладками и окнами я разработал ряд полезных функций:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def open_new_tab(driver, url):
    # Запоминаем текущие открытые вкладки
    original_window = driver.current_window_handle
    original_windows = driver.window_handles
    
    # Открываем новую вкладку через JavaScript
    driver.execute_script(f"window.open('{url}', '_blank');")
    
    # Ждем появления новой вкладки
    WebDriverWait(driver, 10).until(lambda d: len(d.window_handles) > len(original_windows))
    
    # Переключаемся на новую вкладку
    new_window = [w for w in driver.window_handles if w not in original_windows][0]
    driver.switch_to.window(new_window)
    
    return original_window  # Возвращаем хендл исходной вкладки для возврата
Перехват сетевых запросов — еще одна мощная техника, особенно полезная при тестировании API или когда нужно проверить, что приложение делает правильные запросы:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# Для Chrome с использованием CDP (Chrome DevTools Protocol)
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.webdriver import WebDriver
 
def enable_network_interception(driver):
    driver.execute_cdp_cmd('Network.enable', {})
    
    # Установка перехватчика запросов
    driver.execute_cdp_cmd('Network.setRequestInterception', {
        'patterns': [{
            'urlPattern': '*',
            'resourceType': 'XHR',
            'interceptionStage': 'HeadersReceived'
        }]
    })
    
    # Сохраняем перехваченные запросы
    intercepted_requests = []
    
    def intercept_request(request):
        nonlocal intercepted_requests
        request_data = request.get('request', {})
        intercepted_requests.append({
            'url': request_data.get('url'),
            'method': request_data.get('method'),
            'headers': request_data.get('headers', {})
        })
        
        # Позволяем запросу продолжиться
        driver.execute_cdp_cmd('Network.continueInterceptedRequest', {
            'interceptionId': request.get('interceptionId')
        })
    
    driver.on('Network.requestIntercepted', intercept_request)
    return intercepted_requests
Иногда требуется программное управление скроллингом для работы с "ленивой загрузкой". Вот несколько подходов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Плавный скролл до элемента
def smooth_scroll_to_element(driver, element):
    driver.execute_script(
        "arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", 
        element
    )
    time.sleep(1)  # Даем время на завершение анимации
 
# Бесконечный скролл с загрузкой новых элементов
def infinite_scroll_and_collect(driver, item_selector, max_items=100, scroll_pause=1):
    items_found = set()
    last_count = 0
    
    while len(items_found) < max_items:
        # Собираем элементы
        elements = driver.find_elements(By.CSS_SELECTOR, item_selector)
        for element in elements:
            item_id = element.get_attribute('id') or element.text
            items_found.add(item_id)
        
        # Если не нашли новых элементов после скролла, выходим
        if len(items_found) == last_count:
            break
            
        last_count = len(items_found)
        
        # Скролл вниз
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        time.sleep(scroll_pause)
    
    return list(items_found)
Отдельный класс задач — работа с WebRTC и потоковыми медиа. Например, для автоматизации видеоконференций можно эмулировать камеру и микрофон:

Python
1
2
3
4
5
# Для Chrome: эмуляция виртуальной камеры
options = webdriver.ChromeOptions()
options.add_argument('--use-fake-device-for-media-stream')
options.add_argument('--use-fake-ui-for-media-stream')  # Автоматически разрешать доступ
driver = webdriver.Chrome(options=options)
Для измерения производительности приложения использую встроенные метрики браузера:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def measure_page_load_time(driver, url):
    driver.get(url)
    
    # Получаем метрики производительности
    navigation_timing = driver.execute_script("""
        var performance = window.performance;
        var timing = performance.timing;
        return {
            'navigationStart': timing.navigationStart,
            'domComplete': timing.domComplete,
            'loadEventEnd': timing.loadEventEnd,
            'totalTime': timing.loadEventEnd - timing.navigationStart
        };
    """)
    
    return navigation_timing
Для отладки сложных сценариев автоматизации я создал специальный декоратор:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def debug_step(description):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f"STARTING: {description}")
            start_time = time.time()
            try:
                result = func(*args, **kwargs)
                print(f"SUCCESS: {description} ({time.time() - start_time:.2f}s)")
                return result
            except Exception as e:
                print(f"FAILED: {description} - {str(e)}")
                driver = args[0] if len(args) > 0 and hasattr(args[0], 'save_screenshot') else None
                if driver:
                    screenshot_path = f"error_{int(time.time())}.png"
                    driver.save_screenshot(screenshot_path)
                    print(f"Screenshot saved to {screenshot_path}")
                raise
        return wrapper
    return decorator

Паттерны проектирования для веб-автоматизации



За годы работы с автоматизацией я пришел к выводу, что хаотичный подход к написанию Selenium-скриптов приводит к кошмару поддержки. Даже небольшое изменение в UI может обрушить всю автоматизацию как карточный домик. Именно поэтому я стал фанатом структурированного подхода с использованием проверенных паттернов проектирования.

Безусловный лидер среди паттернов для веб-автоматизации — Page Object Model (POM). Суть этого паттерна проста: мы инкапсулируем логику взаимодействия с веб-страницей в отдельный класс. Вместо разбросанных по коду локаторов и действий мы создаем четкую абстракцию:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class LoginPage:
    def __init__(self, driver):
        self.driver = driver
        self.username_input = (By.ID, "username")
        self.password_input = (By.ID, "password")
        self.login_button = (By.CSS_SELECTOR, ".login-btn")
        self.error_message = (By.CLASS_NAME, "error-text")
    
    def open(self):
        self.driver.get("https://example.com/login")
        return self
    
    def login(self, username, password):
        self.driver.find_element(*self.username_input).send_keys(username)
        self.driver.find_element(*self.password_input).send_keys(password)
        self.driver.find_element(*self.login_button).click()
        return DashboardPage(self.driver)
    
    def get_error_message(self):
        return self.driver.find_element(*self.error_message).text
Использование такого подхода делает тесты более читаемыми и поддерживаемыми:

Python
1
2
3
4
5
6
def test_valid_login():
    driver = webdriver.Chrome()
    login_page = LoginPage(driver).open()
    dashboard = login_page.login("user@example.com", "password123")
    assert "Welcome" in dashboard.get_welcome_message()
    driver.quit()
Я часто развиваю POM до более гранулярного уровня, выделяя общие компоненты интерфейса в отдельные классы. Например, если на многих страницах повторяется шапка с поиском:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class HeaderComponent:
    def __init__(self, driver):
        self.driver = driver
        self.search_input = (By.ID, "search")
        self.search_button = (By.CSS_SELECTOR, ".search-btn")
    
    def search(self, query):
        self.driver.find_element(*self.search_input).send_keys(query)
        self.driver.find_element(*self.search_button).click()
        return SearchResultsPage(self.driver)
 
class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.header = HeaderComponent(driver)
Еще один паттерн, который я активно применяю — Factory Method для создания драйверов. Он позволяет централизовать логику инициализации и настройки браузера:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class WebDriverFactory:
    @staticmethod
    def get_driver(browser_type, headless=True):
        if browser_type.lower() == "chrome":
            options = webdriver.ChromeOptions()
            if headless:
                options.add_argument("--headless")
            return webdriver.Chrome(options=options)
        elif browser_type.lower() == "firefox":
            options = webdriver.FirefoxOptions()
            if headless:
                options.add_argument("--headless")
            return webdriver.Firefox(options=options)
        else:
            raise ValueError(f"Unsupported browser type: {browser_type}")
Для обработки ошибок я использую паттерн Retry, который автоматически повторяет операцию при возникновении определенных исключений:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def retry(max_attempts=3, exceptions=(StaleElementReferenceException,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    time.sleep(0.5 * (attempt + 1))  # Экспоненциальная задержка
            raise last_exception
        return wrapper
    return decorator
 
class ProductPage:
    # ...
    
    @retry(max_attempts=3)
    def add_to_cart(self):
        self.driver.find_element(*self.add_to_cart_button).click()
        return CartPage(self.driver)
Для асинхронного подхода в Selenium я комбинирую его с asyncio, что позволяет параллельно выполнять несколько автоматизаций:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
async def run_test_async(browser_type, test_func):
    # Запуск браузера в отдельном процессе
    driver = await asyncio.get_event_loop().run_in_executor(
        None, lambda: WebDriverFactory.get_driver(browser_type)
    )
    try:
        # Выполнение теста
        return await asyncio.get_event_loop().run_in_executor(
            None, lambda: test_func(driver)
        )
    finally:
        # Закрытие браузера
        await asyncio.get_event_loop().run_in_executor(
            None, driver.quit
        )
 
async def run_tests_in_parallel():
    test_configs = [
        ("chrome", test_login),
        ("firefox", test_registration),
        ("chrome", test_checkout)
    ]
    
    tasks = [run_test_async(browser, test) for browser, test in test_configs]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    
    # Обработка результатов...
    return results
Для более сложных взаимодействий со страницей я применяю паттерн Command, который инкапсулирует действие пользователя в отдельный класс. Это особенно полезно когда одни и те же действия нужно выполнять в разных контекстах:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Command:
    def execute(self, driver):
        raise NotImplementedError()
 
class ClickCommand(Command):
    def __init__(self, locator):
        self.locator = locator
    
    def execute(self, driver):
        driver.find_element(*self.locator).click()
 
class TypeTextCommand(Command):
    def __init__(self, locator, text):
        self.locator = locator
        self.text = text
    
    def execute(self, driver):
        driver.find_element(*self.locator).send_keys(self.text)
 
class CommandExecutor:
    def __init__(self, driver):
        self.driver = driver
        self.command_history = []
    
    def execute(self, command):
        command.execute(self.driver)
        self.command_history.append(command)
        
    def undo_last(self):
        # Логика отмены последней операции
        pass
Я также часто использую Builder Pattern для создания сложных объектов или состояний. Например, при тестировании формы с множеством полей:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class UserFormBuilder:
    def __init__(self, page):
        self.page = page
        self.user_data = {
            "first_name": "",
            "last_name": "",
            "email": "",
            "password": "",
            "age": 0,
            "country": ""
        }
    
    def with_name(self, first_name, last_name):
        self.user_data["first_name"] = first_name
        self.user_data["last_name"] = last_name
        return self
    
    def with_credentials(self, email, password):
        self.user_data["email"] = email
        self.user_data["password"] = password
        return self
    
    def with_demographics(self, age, country):
        self.user_data["age"] = age
        self.user_data["country"] = country
        return self
    
    def build(self):
        # Заполняем форму данными
        if self.user_data["first_name"]:
            self.page.type_first_name(self.user_data["first_name"])
        if self.user_data["last_name"]:
            self.page.type_last_name(self.user_data["last_name"])
        # И так далее...
        return self.page
Такой подход делает код более читаемым и гибким:

Python
1
2
3
4
5
6
7
registration_page = RegistrationPage(driver)
UserFormBuilder(registration_page) \
    .with_name("Иван", "Петров") \
    .with_credentials("ivan@example.com", "securePassword123") \
    .with_demographics(30, "Russia") \
    .build() \
    .submit_form()
Для создания цепочек действий я использую Fluent Interface. Этот паттерн позволяет писать код, который читается почти как обычное предложение:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class FluentPageActions:
    def __init__(self, driver):
        self.driver = driver
    
    def navigate_to(self, url):
        self.driver.get(url)
        return self
    
    def click(self, locator):
        self.driver.find_element(*locator).click()
        return self
    
    def type(self, locator, text):
        self.driver.find_element(*locator).send_keys(text)
        return self
    
    def wait_for(self, condition, timeout=10):
        WebDriverWait(self.driver, timeout).until(condition)
        return self
Удобство такого подхода очевидно:

Python
1
2
3
4
5
6
7
actions = FluentPageActions(driver)
actions \
    .navigate_to("https://example.com/login") \
    .type((By.ID, "username"), "user@example.com") \
    .type((By.ID, "password"), "password123") \
    .click((By.ID, "login-button")) \
    .wait_for(EC.presence_of_element_located((By.ID, "dashboard")))
Для логирования и мониторинга я разработал декоратор, который фиксирует все действия пользователя:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def log_action(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        self = args[0]  # Ссылка на экземпляр класса (self)
        class_name = self.__class__.__name__
        method_name = func.__name__
        
        # Формируем информативное сообщение
        action_description = f"{class_name}.{method_name}"
        if len(args) > 1:
            action_description += f" с аргументами: {args[1:]}"
        if kwargs:
            action_description += f" и параметрами: {kwargs}"
        
        # Логируем начало действия
        logging.info(f"НАЧАЛО: {action_description}")
        
        start_time = time.time()
        try:
            result = func(*args, **kwargs)
            # Логируем успешное завершение
            logging.info(f"УСПЕХ: {action_description} ({time.time() - start_time:.2f}s)")
            return result
        except Exception as e:
            # Логируем ошибку
            logging.error(f"ОШИБКА: {action_description} - {str(e)}")
            
            # Делаем скриншот
            if hasattr(self, 'driver'):
                timestamp = int(time.time())
                screenshot_path = f"error_{class_name}_{method_name}_{timestamp}.png"
                self.driver.save_screenshot(screenshot_path)
                logging.info(f"Скриншот сохранен: {screenshot_path}")
            
            raise
    return wrapper
Применяется он очень просто - достаточно добавить декоратор к методам страничных объектов:

Python
1
2
3
4
5
6
7
8
9
class LoginPage:
    # ...
    
    @log_action
    def login(self, username, password):
        self.driver.find_element(*self.username_input).send_keys(username)
        self.driver.find_element(*self.password_input).send_keys(password)
        self.driver.find_element(*self.login_button).click()
        return DashboardPage(self.driver)
Еще одна практика, которую я активно использую — создание базовых классов с общей функциональностью для всех страничных объектов:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
    
    def find(self, locator):
        return self.driver.find_element(*locator)
    
    def find_all(self, locator):
        return self.driver.find_elements(*locator)
    
    def click(self, locator):
        self.find(locator).click()
    
    def type(self, locator, text):
        element = self.find(locator)
        element.clear()
        element.send_keys(text)
    
    def is_visible(self, locator):
        try:
            return self.find(locator).is_displayed()
        except NoSuchElementException:
            return False
    
    def wait_for_visible(self, locator):
        return self.wait.until(EC.visibility_of_element_located(locator))
    
    def wait_for_clickable(self, locator):
        return self.wait.until(EC.element_to_be_clickable(locator))
Этот базовый класс существенно упрощает создание новых страничных объектов и уменьшает дублирование кода. Теперь наша реализация конкретной страницы становится значительно компактнее:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
class ProductPage(BasePage):
    def __init__(self, driver):
        super().__init__(driver)
        self.add_to_cart_button = (By.CSS_SELECTOR, ".add-to-cart")
        self.quantity_input = (By.ID, "quantity")
    
    def set_quantity(self, quantity):
        self.type(self.quantity_input, str(quantity))
        return self
    
    def add_to_cart(self):
        self.click(self.add_to_cart_button)
        return CartPage(self.driver)

Масштабирование и производительность



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

Параллельное выполнение тестов — первое, на что стоит обратить внимание. Python не славится поддержкой многопоточности из-за GIL, но для тестов Selenium это не проблема, поскольку узкое место обычно не в CPU, а во времени отклика браузера и сервера. Я использую pytest-xdist для распараллеливания:

Python
1
2
3
4
5
6
7
# Запуск в 4 параллельных процесса
[H2]pytest -n 4 tests/[/H2]
 
def test_parallel_suite(session_factory):
    # Фабрика сессий создает отдельный драйвер для каждого процесса
    driver = session_factory()
    # ...
Вместо создания одного драйвера на все тесты, я использую фабрику, которая генерирует изолированные экземпляры для каждого потока выполнения:

Python
1
2
3
4
5
6
7
8
9
@pytest.fixture(scope="function")
def session_factory():
    def _create_session():
        driver = WebDriverFactory.get_driver("chrome", headless=True)
        driver.set_window_size(1920, 1080)
        driver.implicitly_wait(10)
        return driver
    
    return _create_session
Headless-режим критически важен для масштабирования. Запуск браузера без графического интерфейса экономит память и CPU, а также решает проблему с X-сервером при запуске в CI-средах. Хотя я уже упоминал его ранее, стоит отметить несколько нюансов:

Python
1
2
3
4
5
options = webdriver.ChromeOptions()
options.add_argument("--headless")
options.add_argument("--disable-gpu")  # Иногда помогает на Windows
options.add_argument("--no-sandbox")  # Для запуска в Docker
options.add_argument("--disable-dev-shm-usage")  # Решает проблемы с памятью в CI
Ресурсоемкие операции, типа скриншотов и сложных JavaScript-скриптов, лучше минимизировать. Я заметил, что скриншоты всей страницы могут замедлять выполнение до 2 секунд каждый — и если у вас сотни тестов, это складывается в существенное время.

Docker — отличный инструмент для создания изолированных сред выполнения. Я упаковываю свои тесты в контейнеры для запуска в CI/CD-пайплайнах:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
FROM python:3.10-slim
 
# Установка Chrome и драйвера
RUN apt-get update && apt-get install -y \
    wget \
    gnupg \
    unzip \
    xvfb \
    && wget -q -O - [url]https://dl-ssl.google.com/linux/linux_signing_key.pub[/url] | apt-key add - \
    && echo "deb [arch=amd64] [url]http://dl.google.com/linux/chrome/deb/[/url] stable main" >> /etc/apt/sources.list.d/google.list \
    && apt-get update \
    && apt-get install -y google-chrome-stable
 
# Скачивание и установка ChromeDriver
RUN CHROME_VERSION=$(google-chrome --version | awk '{print $3}' | cut -d. -f1) \
    && CHROMEDRIVER_VERSION=$(wget -qO- https://chromedriver.storage.googleapis.com/LATEST_RELEASE_$CHROME_VERSION) \
    && wget https://chromedriver.storage.googleapis.com/$CHROMEDRIVER_VERSION/chromedriver_linux64.zip \
    && unzip chromedriver_linux64.zip \
    && mv chromedriver /usr/local/bin/ \
    && chmod +x /usr/local/bin/chromedriver
 
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
 
COPY . .
 
# Запуск тестов при старте контейнера
CMD ["pytest", "-xvs"]
Для действительно крупных проектов я использую облачные решения вроде Selenium Grid, BrowserStack или LambdaTest. Они позволяют запускать тесты параллельно на разных ОС и браузерах без необходимости настраивать собственную инфраструктуру:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_remote_driver(browser, version, platform, remote_url):
    capabilities = {
        "browserName": browser,
        "browserVersion": version,
        "platformName": platform,
        "selenoid:options": {
            "enableVNC": True,
            "enableVideo": False
        }
    }
    
    return webdriver.Remote(
        command_executor=remote_url,
        desired_capabilities=capabilities
    )
Еще одна техника оптимизации — умное ожидание вместо фиксированных задержек. Я часто вижу код с time.sleep(5), что гарантированно замедляет тесты:

Python
1
2
# Вместо time.sleep(5)
WebDriverWait(driver, 5).until(EC.visibility_of_element_located(locator))
Кэширование состояния между тестами может значительно сократить время выполнения. Например, вместо постоянного входа в систему можно сохранить cookies после первого логина:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@pytest.fixture(scope="session")
def authenticated_session():
    driver = WebDriverFactory.get_driver("chrome")
    driver.get("https://example.com/login")
    
    # Логин
    driver.find_element(By.ID, "username").send_keys("test_user")
    driver.find_element(By.ID, "password").send_keys("password")
    driver.find_element(By.ID, "login-button").click()
    
    # Сохраняем cookies
    cookies = driver.get_cookies()
    driver.quit()
    
    return cookies

Демо пример - система мониторинга цен



Давайте объединим все изученные техники в одном практическом проекте. Я создал систему мониторинга цен для отслеживания стоимости товаров в интернет-магазинах — задача, с которой часто сталкиваются в маркетинге и e-commerce.
Структура проекта следует принципам POM и включает базовые классы, страничные объекты, хранилище данных и планировщик:

Python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import pandas as pd
import time
import logging
import smtplib
from email.message import EmailMessage
import schedule
import json
from pathlib import Path
 
# Настройка логирования
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    filename='price_monitor.log'
)
 
class BasePage:
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
 
    def find(self, locator):
        return self.wait.until(EC.presence_of_element_located(locator))
 
class ProductPage(BasePage):
    def __init__(self, driver, url):
        super().__init__(driver)
        self.url = url
        self.price_locator = (By.CSS_SELECTOR, ".product-price .current-price")
        self.name_locator = (By.CSS_SELECTOR, "h1.product-title")
        
    def open(self):
        self.driver.get(self.url)
        return self
        
    def get_price(self):
        price_element = self.find(self.price_locator)
        price_text = price_element.text.replace('₽', '').replace(' ', '').strip()
        return float(price_text)
    
    def get_name(self):
        return self.find(self.name_locator).text
 
class PriceMonitor:
    def __init__(self, products_file, history_file, threshold=5.0):
        self.products_file = products_file
        self.history_file = history_file
        self.threshold = threshold  # Процент изменения для уведомления
        self.products = self._load_products()
        self.history = self._load_history()
        
    def _load_products(self):
        try:
            return json.loads(Path(self.products_file).read_text())
        except (FileNotFoundError, json.JSONDecodeError):
            return {}
            
    def _load_history(self):
        try:
            return pd.read_csv(self.history_file)
        except FileNotFoundError:
            return pd.DataFrame(columns=['product_id', 'timestamp', 'price', 'name'])
    
    def check_prices(self):
        options = Options()
        options.add_argument("--headless")
        driver = webdriver.Chrome(options=options)
        
        try:
            for product_id, url in self.products.items():
                logging.info(f"Проверка цены для {product_id}")
                page = ProductPage(driver, url).open()
                current_price = page.get_price()
                product_name = page.get_name()
                
                # Добавляем в историю
                timestamp = int(time.time())
                new_row = pd.DataFrame([{
                    'product_id': product_id,
                    'timestamp': timestamp,
                    'price': current_price,
                    'name': product_name
                }])
                
                self.history = pd.concat([self.history, new_row], ignore_index=True)
                
                # Проверяем изменение цены
                product_history = self.history[self.history['product_id'] == product_id]
                if len(product_history) > 1:
                    previous_price = product_history.iloc[-2]['price']
                    change_percent = abs((current_price - previous_price) / previous_price * 100)
                    
                    if change_percent > self.threshold:
                        self._send_notification(
                            product_id, product_name, previous_price, current_price, change_percent
                        )
            
            # Сохраняем обновленную историю
            self.history.to_csv(self.history_file, index=False)
            
        finally:
            driver.quit()
    
    def _send_notification(self, product_id, name, old_price, new_price, change):
        # Код для отправки уведомления (email, SMS, etc.)
        logging.info(f"Уведомление: {name} изменил цену с {old_price} на {new_price} ({change:.2f}%)")
 
# Запуск мониторинга по расписанию
monitor = PriceMonitor("products.json", "price_history.csv")
 
schedule.every(3).hours.do(monitor.check_prices)
 
if __name__ == "__main__":
    # Первый запуск сразу
    monitor.check_prices()
    
    # Далее по расписанию
    while True:
        schedule.run_pending()
        time.sleep(60)
Этот код соединяет в себе все ключевые элементы, которые мы обсуждали: безголовый режим браузера, Page Object Model, явные ожидания, работу с данными и автоматизацию процесса. Вы можете расширить его, добавив поддержку различных магазинов, более сложную аналитику изменения цен или интеграцию с мессенджерами для уведомлений.

Selenium Перехват сообщений веб-страницы
Здравствуйте! Как с помощью selenium отслеживать сообщения открытой веб-страницы? Например, на...

Веб скрапинг с использованием selenium
Есть файл с большим кол-вом сайтов,где с каждой первой страницы с надо скачать все картинки. Как...

Запуск веб драйвера Selenium через удалённый сервер Linux
Доброго дня, уважаемые коллеги. Не могу запустить скрипт на удалённом сервере через терминал. ...

Автоматизация сохранения страниц с возрастающим адресом с веб-сайта
Здравствуйте. Хочу решить одну проблему. Есть сайт, в нем есть набор картинок, каждая картинка - на...

Автоматизация запросов к веб-интерфейсу (RestAPI) Python3
Доброго времени суток, коллеги, Есть необходимость выгружать JSON-файл с данными за определенный...

Python+selenium
Пытаюсь сделать как в документации. Есть такая конструкция: &lt;div parentid=&quot;div_zvRmisipFbM&quot;...

python+selenium
Пытаюсь изучать python+selenium (python 2.7, selenium 2), тестирую портал. HTML код тестируюемой...

Selenium+python
Всем привет. Подскажите пожалуйста как работать с выпадающими списками? Пытаюсь сделать так ...

Поиск Selenium+Python
Здравствуйте, нужна помощь, есть задача, смысл такой - написать скрипт, который в 3х...

Добавление Selenium к Python
Помогите разобраться что я делаю не так Добавляю Селениум с помощью пип инстал селениум Питон...

Автоматический просмотр видео Python+Selenium
Есть задача (для себя) написать бот для просмотра видео на Перископе. Алгоритм: 1. Вставляешь...

Не могу понять почему в разных браузерах Python+Selenium даёт разные ошибки?
Работа с Хромом: from selenium import webdriver from selenium.common.exceptions import...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Debian 13: Установка Lazarus QT5
ВитГо 09.05.2026
Эта инструкция моя компиляция инструкций volvo https:/ / www. cyberforum. ru/ blogs/ 203668/ 10753. html и его же старой инструкции по установке Lazarus с gtk2. . .
Нейросеть на алгоритме "эстафета хвоста" как перспектива.
Hrethgir 06.05.2026
На десерт, когда запущу сервер. Статья тут https:/ / habr. com/ ru/ articles/ 1030914/ . Автор я сам, нейросеть только помогает в вопросах которые мне не известны - не знаю людей которые знали-бы. . .
Асинхронный приём данных из COM-порта
Argus19 01.05.2026
Асинхронный приём данных из COM-порта Купил на aliexpress термопринтер QR701. Он оказался странным. Поключил к Arduino Nano. Был очень удивлён. Наотрез отказывается печатать русские буквы. Чтобы. . .
попытка написать игровой сервер на C++
pyirrlicht 29.04.2026
попытка написать игровой сервер на плюсах с открытым бесконечным миром. возможно получится прикрутить интерпретатор питон для кастомизации игровой логики. что есть на текущий момент:. . .
Контроль уникальности выбранного документа-основания при изменении реквизита
Maks 28.04.2026
Алгоритм из решения ниже разработан на примере нетипового документа "ЗаявкаНаРемонтСпецтехники", разработанного в КА2. Задача: уведомлять пользователя, если указанная заявка (документ-основание). . .
Благородство как наказание
Maks 24.04.2026
У хорошего человека отношения с женщинами всегда складываются трудно. А я человек хороший. Заявляю без тени смущения, потому что гордиться тут нечем. От хорошего человека ждут соответствующего. . .
Валидация и контроль данных табличной части документа перед записью
Maks 22.04.2026
Алгоритм из решения ниже реализован на примере нетипового документа, разработанного в КА2. Задача: контроль и валидация данных табличной части документа перед записью с учетом регламента компании. . .
Отчёт о затраченных материалах за определенный период с макетом печатной формы
Maks 21.04.2026
Отчёт из решения ниже размещён в конфигурации КА2. Задача: разработка отчёта по затраченным материалам за определённый период, с возможностью вывода печатной формы отчёта с шапкой и подвалом. В. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru