Selenium и Playwright в Python: рабочие приёмы из практики
Что выбрать для автоматизации браузера, где Selenium проигрывает Playwright по ожиданиям и как перестать ловить StaleElementReferenceException и таймауты на CI.

Задача, на которой ломается очевидное решение
Вам нужно нажать кнопку, дождаться загрузки данных и прочитать таблицу. Пишете driver.find_element(...).click(), следом читаете элемент — и получаете StaleElementReferenceException или пустую строку. Причина в том, что клик отправил AJAX-запрос, а DOM ещё не перерисовался. Очевидное решение — time.sleep(3) — на локальной машине проходит, на CI под нагрузкой падает через раз, потому что рендер занял 4 секунды.
Разберём, как обе библиотеки решают проблему ожиданий, где Selenium требует ручной работы, а Playwright делает её за вас, и по каким критериям выбирать между ними. Речь о Selenium 4.x и Playwright 1.4x для Python.
Главная разница — модель ожиданий
Selenium исторически знает два вида ожиданий: неявные (implicitly_wait) и явные (WebDriverWait + expected_conditions). Неявные применяются глобально и не смешиваются с явными без сюрпризов: их совместное использование даёт непредсказуемые таймауты, это прямо отмечено в документации Selenium. Практический вывод — выберите что-то одно, и лучше явные.
Playwright устроен иначе: у него автоожидание встроено в каждое действие. Перед кликом он сам проверяет, что элемент существует, видим, стабилен (не движется), включён и способен принять событие. Отдельный wait нужен реже.
Если вы пишете time.sleep() между действиями браузера — это не пауза, это отложенный флейки-тест. Ждите не время, а состояние.Selenium: явные ожидания вместо sleep
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
driver = webdriver.Chrome()
driver.get("https://example.com/orders")
wait = WebDriverWait(driver, timeout=10)
# Ждём кликабельности, а не просто наличия в DOM
button = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#load-orders")))
button.click()
# Ждём, пока в таблице появится хотя бы одна строка
wait.until(
lambda d: len(d.find_elements(By.CSS_SELECTOR, "table.orders tr")) > 0
)
rows = driver.find_elements(By.CSS_SELECTOR, "table.orders tr")
print(f"Загружено строк: {len(rows)}")
driver.quit()Ключевой момент — element_to_be_clickable, а не presence_of_element_located. Элемент может присутствовать в DOM, но быть перекрыт спиннером или отключён. Клик по такому элементу даст ElementClickInterceptedException.
Playwright: то же самое короче
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com/orders")
# click сам дождётся видимости и кликабельности кнопки
page.click("#load-orders")
# локатор ленивый: ждёт появления строк при обращении к count()
rows = page.locator("table.orders tr")
rows.first.wait_for() # дождаться первой строки
print(f"Загружено строк: {rows.count()}")
browser.close()Локатор в Playwright — не снимок элемента, а описание того, как его найти. Он вычисляется заново при каждом действии, поэтому StaleElementReferenceException здесь просто нет как класса ошибки.
Что выигрываете и чем платите
| Критерий | Selenium 4.x | Playwright 1.4x |
|---|---|---|
| Ожидания | Ручные, явные | Автоожидание из коробки |
| Установка браузеров | Selenium Manager (автоматически с 4.6) | playwright install |
| Изоляция сессий | Новый driver на сессию | Browser context — дёшево и быстро |
| Перехват сетевых запросов | Через CDP, неудобно | page.route — нативно |
| Async-режим | Нет нативного | async_api из коробки |
| Зрелость экосистемы | Огромная, много интеграций | Моложе, но растёт быстро |
| Grid / распределённый запуск | Selenium Grid — стандарт индустрии | Своего Grid нет, нужен внешний оркестратор |
Изоляция: где Playwright экономит ресурсы
В Selenium каждая независимая сессия — это отдельный процесс браузера. Двадцать параллельных тестов — двадцать процессов Chrome, и оперативная память кончается быстро. В Playwright один запущенный браузер порождает несколько контекстов*, каждый со своими куками и localStorage, но без накладных расходов на новый процесс.
# Playwright: три изолированных пользователя в одном браузере
browser = p.chromium.launch()
ctx_admin = browser.new_context()
ctx_user = browser.new_context()
ctx_guest = browser.new_context()
# у каждого свои cookies, общий процесс браузераПерехват сети вместо парсинга DOM
Частая ошибка — ждать, пока данные отрисуются в таблице, и парсить HTML. Если данные приходят JSON'ом, дешевле перехватить сам ответ. В Playwright это нативно:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# Ловим ответ конкретного API-запроса
with page.expect_response("**/api/orders**") as resp_info:
page.goto("https://example.com/orders")
response = resp_info.value
data = response.json()
print(f"Заказов в ответе: {len(data['items'])}")
browser.close()В Selenium то же требует работы через Chrome DevTools Protocol и заметно многословнее. Если ваша задача — вытащить данные, а не проверить рендер, перехват ответа надёжнее любого CSS-селектора: вёрстка меняется чаще, чем схема API.
Когда что выбирать
- Playwright — новый проект, нужны асинхронность, перехват сети, стабильные ожидания, много параллельных изолированных сессий. Меньше кода на те же задачи.
- Selenium — уже есть кодовая база на нём, нужен Selenium Grid для распределённого запуска, требуется поддержка экзотического браузера или драйвера, вокруг которого выросла инфраструктура.
- Смешивать в одном проекте обычно не стоит: два набора абстракций для одного и того же — лишняя когнитивная нагрузка на команду.
Что ломается на практике
- Смешение implicit и explicit wait в Selenium. Таймауты складываются непредсказуемо. Оставьте
implicitly_wait(0)и работайте только черезWebDriverWait. - Хрупкие XPath вида
//div[3]/span[2]. Ломаются при любой правке вёрстки. Ищите по data-атрибутам или тексту. - Headless-режим на CI ведёт себя иначе. Другие размеры окна, отсутствие некоторых шрифтов, иная скорость. Проверяйте с явно заданным
viewport. - Гонка при навигации. После
click(), ведущего на новую страницу, в Selenium нужно дождаться нового состояния явно; в Playwrightexpect_navigationили автоожидание закрывают это. - Забытый
quit()/close(). Зомби-процессы браузера съедают память на раннере. Оборачивайте вtry/finallyили контекстный менеджер.
Что настроить дальше
После выбора инструмента займитесь запуском в контейнере: официальный образ Playwright уже содержит браузеры и системные зависимости, для Selenium используйте образы seleniarm/selenium-hub или официальные selenium/*. На CI меряйте флейки-рейт — долю тестов, падающих при повторном запуске без изменений кода; если он выше нескольких процентов, ищите оставшиеся sleep и ожидания по времени. Инвалидация состояния между тестами (куки, storage) — отдельная тема, здесь мы её не разворачивали.
Для Playwright следующий шаг — освоить трейсинг (trace): он записывает снимки DOM на каждом действии и резко ускоряет разбор упавших тестов. Для Selenium — настроить Grid или Selenoid, если нужен параллельный запуск на нескольких машинах.
Магистр Excel и Google Sheets: ВПР, СУММЕСЛИМН, макросы VBA и скрипты Apps Script.
Спросить за 20 ₽Источники: Playwright for Python — официальная документация, Selenium — Waiting Strategies
Частые вопросы
У меня Selenium 3, а примеры для 4.x — что изменится?
Пример падает с TimeoutException — что делать?
Playwright не запускается, ошибка про отсутствие браузера
Что стабильнее в проде под нагрузкой?
Можно ли использовать один и тот же код для Chrome и Firefox?
Selenium устарел, стоит ли ещё его учить?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.