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:
Теперь самая интересная часть — установка драйвера браузера. 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 Пытаюсь сделать как в документации. Есть такая конструкция:
<div parentid="div_zvRmisipFbM"... 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...
|