Playwright: Лучшая альтернатива Selenium. Первое знакомство

Друзья, приветствую.

Наверняка, если вы работали с автоматизацией браузера на Python, вам знаком такой инструмент, как Selenium. Более продвинутые пользователи могут быть знакомы с SeleniumBase или SeleniumUndetect, а также с другими библиотеками, написанными на основе Selenium.

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

В контексте Selenium, по моему мнению, таким инструментом является Playwright. Я обратил внимание, что в рунете о нем достаточно мало информации, и данной статьей, а если вам будет интересно, то и серией публикаций — я хочу вас познакомить с этим инструментом.

Что такое Playwright

В первую очередь, Playwright — это инструмент для комплексного тестирования современных веб-приложений, но, как я понимаю, собрались мы тут для другого.

Сам инструмент мультиязычный и примерно одинаково работает на Python, Node.js и Java. Поэтому, если вы не Python-программист, данная информация вам тоже будет интересна. Однако, так как мой основной язык программирования Python, я буду рассматривать версию для Python.

Playwright поддерживает следующие браузеры: Chromium (сегодня покажу вам, как запустить Chrome), WebKit и Firefox. Кстати, инструмент ещё и мультиплатформенный, так что вы сможете его свободно запустить на Windows, Linux и macOS.

В каком формате можно использовать Playwright

Плагин для Pytest

Форматов использования несколько. Первый и, наверное, основной формат работы, который закладывали разработчики в Playwright — это формат тестирования (pytest).

Тут все просто. Из коробки, на данный момент, вы устанавливаете не просто библиотеку Playwright, а полноценный плагин для Pytest. Благодаря этому вы, без особого труда, можете запускать тесты.

Данный формат я описывать сегодня не буду.

Формат Selenium

Это то, что, как мне кажется, будет вам интереснее всего. В данном случае вы пишете обычный код, как бы делали это на том же Selenium, и получаете тот результат, который вам нужен.

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

Чем Playwright лучше Selenium?

  1. Асинхронность из коробки

Об этом я уже писал выше. В Playwright реализовано полноценное асинхронное API, благодаря чему его легко интегрировать в проекты, поддерживающие асинхронность, например, Telegram-бот на aiogram с функционалом парсера.

  1. Современная архитектура

Playwright был разработан с учетом последних технологий и подходов, что позволяет ему обеспечивать более стабильное и быстрое выполнение тестов. Selenium, будучи старше, иногда сталкивается с устаревшими архитектурными решениями.

  1. Поддержка нескольких браузеров

Playwright поддерживает автоматизацию браузеров Chromium, Firefox и WebKit (движок Safari) «из коробки», что позволяет тестировать приложения на всех основных движках браузеров. Selenium также поддерживает несколько браузеров, но требует дополнительных драйверов и конфигураций.

  1. Одновременные контексты (контейнеры)

Playwright позволяет создавать несколько независимых контекстов внутри одного браузера. Это упрощает тестирование сценариев, которые требуют изоляции данных между тестами, например, для тестирования мультиаккаунтов. О контекстах более подробно поговорим далее.

  1. Поддержка сетевых интерсепторов и эмуляции

Playwright предоставляет мощные возможности для перехвата сетевых запросов и ответов, что позволяет эмулировать различные сетевые условия и сценарии. Selenium такие функции предоставляет через сторонние библиотеки.

  1. Простота установки и использования

Playwright легко установить и начать использовать благодаря встроенным драйверам для браузеров и простому API. Selenium требует установки отдельных браузерных драйверов и зачастую более сложен в настройке.

  1. Автоматическое ожидание элементов

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

  1. Супер-удобный синтаксис для поиска и работы с элементами

Тут просто приведу один пример из Playwright:

await page.get_by_role("button", name="Вход").click()

Таким образом, мы просто указываем, что на странице есть некая кнопка с надписью «Вход» и на нее можно кликнуть.

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

Очень удобно и гибко реализовано ожидание, как я писал выше, не только встроенное, но и дополнительное.

И тут бы я мог ещё долго расхваливать преимущества Playwright, но уверен, что вы все ждете практики. Так приступим к ней.

Чем мы сегодня займемся?

Так как это вводная статья, сильно грузить вас техническими деталями и моментами я не буду.

Сегодня я покажу вам, как установить Playwright, как выполнить стартовую настройку, например, подставить свой UserAgent или установить окно браузера на FullScreen.

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

Если эта статья получит достаточный отклик и вам будет интересно более глубоко разобраться в этой теме — дайте знать об этом в комментариях, через свои лайки или подписки, и тогда я напишу ещё несколько более глубоких разборов данного инструмента.

Полный исходник кода, который я сегодня буду описывать, как и мой эксклюзивный контент, который я не публикую на Хабре, вы найдете в моем телеграм-канале «Легкий путь в Python».

Приступим к практике!

Установка

Для начала нам необходимо выполнить установку Playwright, а затем через специальную команду установить драйверы для браузеров (все «из коробки», не волнуйтесь).

Для установки воспользуемся pip:

pip install pytest-playwright

После этого необходимо установить драйверы (браузеры) для Playwright. Для установки всех драйверов напишем в терминале:

playwright install

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

playwright install chromium

Так как сегодня я буду показывать работу на примере Chromium (Chrome), я установлю только его.

Что примечательно, если вы будете использовать Playwright в разных проектах, вам будет достаточно один раз установить браузеры, и они автоматически будут подтягиваться во все проекты.

Для обновления Playwright и его браузеров можно воспользоваться следующей командой:

pip install pytest-playwright playwright -U

Вот системные требования:

  • Python версии 3.8 или выше.

  • Windows 10+, Windows Server 2016+ или Windows Subsystem for Linux (WSL).

  • macOS 13 Ventura или macOS 14 Sonoma.

  • Debian 11, Debian 12, Ubuntu 20.04 или Ubuntu 22.04, Ubuntu 24.04 на архитектуре x86–64 и arm64.

Напишем первый код

Теперь, когда Playwright установлен, мы можем начать писать свой первый код.

Писать я буду с использованием контекстного менеджера with и в асинхронном режиме, но для вас дам первый простой пример работы в синхронном режиме.

from playwright.sync_api import sync_playwright


def sync_work():
    # открыть соединение
    with sync_playwright() as p:
        # инициализация браузера (без видимого открытия браузера)
        # browser = p.chromium.launch()

        # инициализация браузера (с явным открытием браузера)
        browser = p.chromium.launch(headless=False)
        # инициализация страницы
        page = browser.new_page()
        # переход по url адресу:
        page.goto('https://whatmyuseragent.com/')
        # сделать скриншот
        page.screenshot(path='./demo.png')
        browser.close()

        
sync_work()

При помощи этого простого кода мы перешли на сайт https://whatmyuseragent.com/, сделали скриншот и закрыли страницу.

Значение headless у браузера стоит по умолчанию как True. Тем самым, не передавая данный аргумент, браузер будет открыт в фоновом режиме. Напоминаю, что стандартному Selenium для такого необходимо было бы передавать опции и там отдельно прописывать аргумент.

Теперь сделаем то же самое, но через асинхронный режим.

from playwright.async_api import async_playwright
import asyncio


async def async_work():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)
        page = await browser.new_page()
        await page.goto('https://whatmyuseragent.com/')
        await page.screenshot(path='./demo2.png')
        await browser.close()

        
asyncio.run(async_work())

Асинхронная работа с Playwright чуть удобнее, так как позволяет упростить код в проекте и реализовать те вещи, которые в синхронном режиме сделать сложнее, но если в вашем проекте асинхронность не важна, работать можно и в синхронном режиме.

Обратите внимание. Тут мы импортировали асинхронное API. Далее, мы использовали не просто with, а async with. Кроме того, методы у нас стали асинхронными, а, следовательно, при использовании появилась необходимость в передаче await перед методами.

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

Так выглядят асинхронные методы

Так выглядят асинхронные методы

Так выглядят синхронные методы

Так выглядят синхронные методы

Для того чтоб использовать не Chronium, а GoogleChrome, необходимо в инициализацию браузера добавить аргумент channel со значением chrome

browser = await p.chromium.launch(channel='chrome', headless=False)

Другие поддерживаемые значения: «chrome», «chrome‑beta», «chrome‑dev», «chrome‑canary», «msedge», «msedge‑beta», «msedge‑dev», «msedge‑canary».

Ожидания в Playwrght

Как вы понимаете, тут у нас большая и важная тема. Так как это вводная статья — я дам общее представление об этом.

Все логика, как вы уже могли заметить, идет не вокруг конкретного драйвера (браузера), как это было у Selenium, во вокруг страницы.

То есть, мы использовали не:

browser.screenshot(path='./demo.png')

а

page.screenshot(path='./demo.png')

Вокруг этой концепции будет строиться дальнейшая логика взаимодействия с элементами на странице.

Для ожиданий элементов в Playwright можно использовать устаревший подход через wait_for_(url, event, function, selector и т.д.):

56d23c0fa8dd1cd43c03b2bc25b9a6e5.jpg

Теперь разработчики Playwright настаивают на подходе Assertions и я с ними соледарен. Подробно можно рассмотреть тут https://playwright.dev/python/docs/actionability. Общая суть сводится к тому, что вы подвязываетесь к какому-то элементу на странице, дожидаетесь его, а после выполняете то или иное действие.

Сейчас запись ожидания выглядит так:

expect(span).to_be_attached()

Принцип тут следующий. Сначала вы находите элемент на странице одним из доступных способом (рассмотрим их далее). Например:

locator = page.get_by_label("Subscribe to newsletter")

А затем вы указываете проверку на появление (привязку) данного элемента к странице. Например:

await expect(locator).to_be_checked()

Данная конструкция дожидается пока элемент будет привязан к странице. Дополнительным аргументом можно передать время ожидания. Подробнее мы это рассмотрим далее, когда разберемся с поиском элементов на странице.

Expect импортируется из async_api / sync_api

from playwright.sync_api import sync_playwright, expect
from playwright.async_api import async_playwright, expect

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

Поиск элементов на странице через Playwright

Для поиска элементов на странице Playwright предлагает суперудобный функционал. Искать можно через: frame, alt_text, label, placeholder, role, test_id, text, title, locator, nth.

Если вы немного знакомы с CSS и HTML, то представляете, насколько это удобно. Суть такая: если, например, вы хотите найти элемент по плейсхолдеру, к примеру, это «Введите свое имя», то вам необходимо использовать такую конструкцию:

await page.get_by_placeholder("Введите свое имя")

В HTML в таком случае вы бы увидели запись:

Далее, как я уже приводил в пример, можно искать просто по роли. К примеру, нужно найти кнопку «Войти». В этом случае мы будем выполнять поиск по роли «button». Запись будет выглядеть так:

btn = await page.get_by_role("button", name="Войти")

То есть все достаточно интуитивно понятно. Но самое интересное — это метод locator.

await page.locator()

Сам метод синхронный, если речь идет просто про поиск данных. Суть в том, что он объединил в себе вообще все случаи с By.CSS, By.CLASSNAME, By.XPATH и прочее.

То есть вам достаточно вызвать метод и в его аргумент просто передать CSS-селектор, название нужного тега, XPATH и так далее — и он найдет! Причем не важно, один у вас такой элемент или таких элементов много.

Тут единственное, что если вы захотите воспользоваться методом expect для проверки наличия элемента (доступности) на странице, то вам необходимо будет забрать из массива первый или последний элемент. Для этого достаточно указать .last или .first в проверке.

Давайте теперь заберем с указанного сайта, на котором мы делали тесты, наш user_agent, выполним проверку на то, что он найден, и распечатаем его текст (тут тоже есть некоторые фишки).

c7fe6e379ea18289e320263af2c4dd36.jpg

Я вижу, что на данном сайте user_agent записан в блоке с ID ua. Отлично. Теперь я буду использовать метод locator и укажу, что мы ищем элемент с id="ua":

import asyncio
from playwright.async_api import async_playwright, expect


async def async_work():
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            channel='chrome',
            headless=False
        )
        page = await browser.new_page()
        await page.goto('https://whatmyuseragent.com/')

        el = page.locator("#ua")
        await expect(el).to_be_visible()
        print(el)
        await browser.close()


asyncio.run(async_work())

Получился у меня следующий код. Тут, через решетку, я указал, что ищу элемент с ID=ua. Напоминаю, что для указания класса мы бы передавали точку. Результат у меня получился таким:

 selector='#ua'>

Не то, что мы бы хотели видеть, не так ли? Для того чтобы работать с полученным элементом после поиска на странице в Playwright, также предусмотрены методы. Например, мы можем получить text_content, inner_text, inner_html и прочее.

Давайте заберем inner_text и распечатаем его.

el = page.locator("#ua")
await expect(el).to_be_visible()
inner_html = await el.inner_text()
print(inner_html)

Результат такой:

Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36

На данном сайте inner_html не отличается от inner_text, но если вы, например, привыкли работать через bs4, то можете методом inner_html получить необходимый HTML, а при помощи bs4 доставать с него данные. Надеюсь, что общую суть вы уловили.

То есть мы сначала находим элемент, а после делаем с ним какие-то действия.

Если вы знаете, что таких элементов несколько, то вы можете воспользоваться асинхронным (или синхронным) методом all. Давайте, например, заберем все надписи из верхнего меню.

Мы видим, что каждый такой элемент — это ссылка с классом nav-item nav-link active. Давайте воспользуемся методом locator и специальным селектором, чтобы получить все такие элементы.

4f63c8c3ff2b0d08aa33c12cae0a88b9.jpg

Для получения такого селектора мы можем воспользоваться панелью разработчика и просто скопировать его с первой ссылки.

790913e402bc966e0614d6d17042854a.jpg

Получился такой результат: #navbarNavAltMarkup > div > a:nth-child(1).

Так как нам необходимо получить все ссылки из navbar, сократим селектор: #navbarNavAltMarkup > div > a.

В результате получим такую запись:

all_links = await page.locator('#navbarNavAltMarkup > div > a').all()

Тем самым мы указываем, что хотим достать все такие элементы.

Теперь нам достаточно пройти по каждому такому элементу стандартным циклом for и забрать текст.

all_links = await page.locator('#navbarNavAltMarkup > div > a').all()
for link in all_links:
    text = await link.inner_text()
    print(text)

Как вы видите, все достаточно просто и интуитивно понятно. Для того чтобы забрать ссылку, можно использовать следующую запись:

for link in all_links:
    text = await link.inner_text()
    url = await link.get_attribute('href')
    print(f'{text} — {url}')

Тут, как вы видите, мы использовали интуитивно понятный метод get_attribute. Обратите внимание, метод использовался как locator.get_attribute, а не как page.get_attribute. Это важно понимать.

Контекст в Playwright

Контекст в Playwright занимает большую и значимую роль, так как именно контекст позволяет нам привязывать UserAgent, менять прокси, подвязывать куки и делать много других очень полезных штук.

Так как сегодня у нас ознакомительная статья, я просто продемонстрирую вам, как на примере контекста можно управлять размером экрана браузера.

import asyncio
from playwright.async_api import async_playwright, expect


async def async_work():
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            channel='chrome',
            headless=False,
            args=["--start-maximized"]
        )
        context = await browser.new_context(no_viewport=True)
        page = await context.new_page()
        await page.goto('https://whatmyuseragent.com/')

        el = page.locator("#ua")
        await expect(el).to_be_visible()
        ua = await el.inner_text()
        print(ua)
        await browser.close()


asyncio.run(async_work())

Тут нужно пояснить. Во-первых, мы ввели контекст, который у нас стал некой надстройкой над браузером. И далее мы, как бы, наследуемся уже от контекста.

В данном случае мы ввели его для того, чтобы указать, что нам не требуется фиксированное окно просмотра (предустановленное в Playwright). Тем самым мы получили возможность установить свой размер экрана.

Для этого я добавил аргументом "--start-maximized". Данная запись работает как options в Selenium, только значения передаются списком.

Теперь давайте установим другой UserAgent. Для этого я использую библиотеку fake_useragent. Предварительно установим:

pip install fake_useragent

Далее получим UserAgent и подставим его в контекст. Код получится таким:

import asyncio
from playwright.async_api import async_playwright, expect
from fake_useragent import FakeUserAgent


async def async_work():
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            channel='chrome',
            headless=False,
            args=["--start-maximized"]
        )
        user_agent = FakeUserAgent().random
        print('UA полученный: ', user_agent)
        context = await browser.new_context(no_viewport=True, user_agent=user_agent)
        page = await context.new_page()
        await page.goto('https://whatmyuseragent.com/')

        el = page.locator("#ua")
        await expect(el).to_be_visible()
        ua = await el.inner_text()
        print('UA на странице: ', ua)
        await browser.close()


asyncio.run(async_work())

Далее я выполняю его и вижу такой результат:

UA полученный:  Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0
UA на странице:  Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36 Edg/121.0.0.0

Контексту, как и предустановленным настройкам прокси, куки, user_profile, если вам это будет нужно, можно будет посвятить одну большую статью, в которой я более подробно и глубоко разберу все аспекты данной темы.

Скрытие автоматизации

Напоследок, я хотел бы показать вам опцию, которая позволяет скрыть факт автоматизации с помощью Playwright. К сожалению, эта возможность отсутствует «из коробки».

Для проверки скрытия автоматизации воспользуемся сайтом: https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html.

import asyncio
from playwright.async_api import async_playwright


async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch(
            headless=False,
            args=["--start-maximized"]
        )

        context = await browser.new_context(no_viewport=True)
        page = await context.new_page()

        await page.goto('https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html')
        await page.wait_for_timeout(5000)
        await context.close()


if __name__ == '__main__':
    asyncio.run(main())

f121d5dcbeb1cdc908595023390b9d52.jpg

Обратите внимание. Тут я использовал запись await page.wait_for_timeout(5000), она работает аналогично записи await asyncio.sleep(5).

Теперь, чтобы обойти эту защиту, достаточно добавить один аргумент (--disable-blink-features=AutomationControlled):

browser = await p.chromium.launch(headless=False,
args=["--start-maximized", 
      '--disable-blink-features=AutomationControlled']
)

Проверяем и видим, что защита была успешно обойдена.

ab17f1daa73b0629d82a98f436549cac.jpg

Заключение

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

Если вас интересует эта тема и вы хотите глубже изучить возможности Playwright, подписывайтесь на мои обновления и следите за новыми статьями.

Напоминаю, что полный исходник кода можно найти в моем телеграмм канале «Легкий путь в Python».

Удачи в изучении Playwright!

© Habrahabr.ru