Другое

Selenium Shadow DOM: исправление элемента входа HBO Max

Узнайте, как найти элементы входа в HBO Max с Selenium при ошибке Shadow DOM NoSuchElementException. Полное руководство с рабочими решениями.

Автоматизация Selenium: Не удаётся найти элемент входа в HBO Max

Я разрабатываю скрипт автоматизации Selenium для входа в HBO Max на https://auth.hbomax.com/login, но сталкиваюсь с NoSuchElementException при попытке найти элемент ввода email для входа.

Сообщение об ошибке

selenium.common.exceptions.NoSuchElementException: Message: no such element: 
Unable to locate element: {"method":"css selector","selector":"[id="sign-in-email-input"]"}

Текущий код

python
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

def get_driver():
    # set options to make browsing easier
    options = webdriver.ChromeOptions()
    options.add_experimental_option("detach", True)
    options.add_argument("disable-infobars")
    options.add_argument("start-maximized")
    options.add_argument("disable-dev-shm-usage")
    options.add_argument("no-sandbox")
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_argument("disable-blink-features=AutomationControlled")

    driver = webdriver.Chrome()
    driver.get("https://auth.hbomax.com/login")
    return driver

def main():
    driver = get_driver()
    wait = WebDriverWait(driver, 10)

    # accept cookies
    wait.until(EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div[1]/div/div/div/section/div/button[1]/div/div/span"))).click()

    # unable to locate element
    wait.until(EC.presence_of_all_elements_located((By.XPATH, "//*[@id='sign-in-email-input']")))

Выборки, которые я пробовал безуспешно

  • By.ID, "sign-in-email-input"
  • By.ID, "sign-in-email"
  • By.XPATH, "//*[@id='sign-in-email']/div"
  • By.XPATH, "//*[@id='sign-in-email-input']"
  • By.XPATH, "/html/body/div[1]/div[2]/div[1]/div/main/div[2]/gi-login//gi-sign-in-flow/gi-sign-in-email//gi-track-analytics-events/div/gi-form/div/form/div[2]/div/gi-form-input/div/input"

HTML‑структура элемента входа

html
<label id="sign-in-email-label" for="sign-in-email-input">Email address</label>
<gi-form-input id="sign-in-email" class="hydrated">
  <div role="region" class="text-input-wrapper" dir="ltr">
    <input class="input" id="sign-in-email-input" type="email"
      name="email" dir="ltr" autocomplete="email"
      required data-testid="giweb.general.forms.emailInput.form-field"
      data-click-element-subtype="EMAIL_INPUT"
      data-click-element-type="FIELD_INPUT" aria-invalid="false"
      aria-required="true" aria-describedby="sign-in-email-error sign-in-email-hint">
  </div>
</gi-form-input>

Что может вызывать эту проблему, и как я могу успешно найти и взаимодействовать с элементом ввода email для входа в HBO Max с помощью Selenium?

Наличие ошибки NoSuchElementException при попытке найти элемент входа в HBO Max связано с тем, что сайт использует Shadow DOM в своих пользовательских веб‑компонентах. Современные веб‑приложения, такие как HBO Max, часто инкапсулируют формы внутри Shadow DOM, чтобы изолировать стили и поведение, из‑за чего стандартные локаторы Selenium не работают.

Содержание

Понимание проблемы Shadow DOM

В приведённой вами разметке поле ввода электронной почты находится внутри пользовательского веб‑компонента <gi-form-input>. Этот компонент, скорее всего, использует Shadow DOM для инкапсуляции своей внутренней структуры, из‑за чего элемент <input> недоступен через обычные запросы к DOM.

Согласно официальной документации Selenium, «Shadow DOM – это изолированное дерево DOM, скрытое внутри элемента. С выпуском версии 96 в браузерах Chromium Selenium теперь позволяет получить доступ к этому дереву с помощью удобных методов shadowRoot».

Shadow DOM создаёт отдельное, изолированное дерево DOM внутри основного документа, что объясняет, почему ваши стандартные локаторы, такие как By.ID и By.XPATH, не находят элемент.

Почему стандартные локаторы не работают

Стандартные локаторы Selenium работают только с основным деревом DOM, но когда элементы находятся внутри Shadow DOM:

  1. CSS‑селекторы не могут проникнуть через границу Shadow DOM.
  2. XPath‑запросы не могут пройти внутрь корней теней.
  3. Идентификаторы доступны только внутри своего контекста теней.

Как объясняет руководство LambdaTest, «Методы Selenium по умолчанию могут не работать напрямую с элементами Shadow DOM, потому что они скрыты от основного дерева DOM».

Множественные решения доступа к Shadow DOM

Метод 1: Использование JavaScriptExecutor (самый совместимый)

python
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains

def get_shadow_root(driver, element):
    """Получить корень теней элемента"""
    return driver.execute_script('return arguments[0].shadowRoot', element)

# В основной функции:
def main():
    driver = get_driver()
    wait = WebDriverWait(driver, 10)

    # Принять куки
    wait.until(EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div[1]/div/div/div/section/div/button[1]/div/div/span"))).click()

    # Найти элемент gi-form-input в основном DOM
    gi_form_input = wait.until(EC.presence_of_element_located((By.ID, "sign-in-email")))

    # Получить его корень теней
    shadow_root = get_shadow_root(driver, gi_form_input)

    # Найти поле ввода внутри корня теней
    email_input = shadow_root.find_element(By.ID, "sign-in-email-input")
    email_input.send_keys("your-email@example.com")

Метод 2: Использование метода get_shadow_root() из Selenium 4.0+

python
def main():
    driver = get_driver()
    wait = WebDriverWait(driver, 10)

    # Принять куки
    wait.until(EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div[1]/div/div/div/section/div/button[1]/div/div/span"))).click()

    # Найти элемент gi-form-input
    gi_form_input = wait.until(EC.presence_of_element_located((By.ID, "sign-in-email")))

    # Получить корень теней с помощью метода Selenium 4.0+
    shadow_root = gi_form_input.get_shadow_root()

    # Найти поле ввода
    email_input = shadow_root.find_element(By.ID, "sign-in-email-input")
    email_input.send_keys("your-email@example.com")

Метод 3: Доступ к многослойному Shadow DOM (если вложено)

python
def get_element_from_shadow(driver, host_element, shadow_element_path):
    """
    Доступ к элементам через несколько уровней Shadow DOM
    shadow_element_path – список ID или селекторов элементов
    """
    current_element = host_element
    for element_id in shadow_element_path:
        shadow_root = current_element.get_shadow_root()
        current_element = shadow_root.find_element(By.ID, element_id)
    return current_element

# Пример использования:
shadow_path = ["outer-shadow-host", "inner-shadow-host", "final-input-element"]
email_input = get_element_from_shadow(driver, gi_form_input, shadow_path)

Рекомендуемый подход для HBO Max

Учитывая структуру разметки и общие практики, наиболее надёжным решением является следующий подход:

python
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException

def get_driver():
    options = webdriver.ChromeOptions()
    options.add_experimental_option("detach", True)
    options.add_argument("disable-infobars")
    options.add_argument("start-maximized")
    options.add_argument("disable-dev-shm-usage")
    options.add_argument("no-sandbox")
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_argument("disable-blink-features=AutomationControlled")
    
    # Добавляем опции для лучшей работы с Shadow DOM
    options.add_argument("--disable-web-security")
    options.add_argument("--allow-file-access-from-files")
    
    driver = webdriver.Chrome()
    driver.get("https://auth.hbomax.com/login")
    return driver

def get_shadow_root(driver, element):
    """Вспомогательная функция для получения корня теней"""
    return driver.execute_script('return arguments[0].shadowRoot', element)

def main():
    driver = get_driver()
    wait = WebDriverWait(driver, 15)  # Увеличиваем таймаут
    
    try:
        # Принять куки
        wait.until(EC.presence_of_element_located((By.XPATH, "/html/body/div[1]/div[1]/div/div/div/section/div/button[1]/div/div/span"))).click()
        
        # Ожидание появления gi-form-input
        gi_form_input = wait.until(EC.presence_of_element_located((By.ID, "sign-in-email")))
        
        # Получить корень теней
        shadow_root = get_shadow_root(driver, gi_form_input)
        
        # Ожидание поля ввода внутри корня теней
        email_input = wait.until(
            EC.presence_of_element_located((By.ID, "sign-in-email-input"))
        )
        
        # Очистить и ввести email
        email_input.clear()
        email_input.send_keys("your-email@example.com")
        
        # Продолжить с полем пароля и кнопкой входа...
        
    except TimeoutException as e:
        print(f"Timeout при ожидании элемента: {e}")
        # Резервный вариант: попытка альтернативного селектора
        try:
            # Альтернативный подход с JavaScript
            email_input = driver.execute_script("""
                const formInput = document.getElementById('sign-in-email');
                return formInput.shadowRoot.querySelector('#sign-in-email-input');
            """)
            email_input.send_keys("your-email@example.com")
        except Exception as e2:
            print(f"Альтернативный подход не сработал: {e2}")
            raise

Лучшие практики автоматизации веб‑компонентов

  1. Используйте явные ожидания: всегда ждите появления элемента, прежде чем взаимодействовать с ним.
  2. Обрабатывайте динамическую загрузку: веб‑компоненты часто загружаются асинхронно, поэтому будьте терпеливы.
  3. Множественные стратегии: имейте резервные методы, если основной подход не работает.
  4. Кросс‑браузерная совместимость: тестируйте в разных браузерах, так как поддержка Shadow DOM может отличаться.
  5. Осведомлённость о версии: используйте Selenium 4.0+ для лучшей поддержки Shadow DOM.

Альтернативные решения

Использование плагина shadow-automation-selenium

Для сложных сценариев с Shadow DOM рассмотрите использование плагина shadow-automation-selenium, упомянутого в обсуждениях на Stack Overflow:

python
# Сначала установите плагин
# pip install shadow-automation-selenium

from shadow_automation import ShadowAutomation

shadow_automation = ShadowAutomation(driver)
element = shadow_automation.get_element_by_shadow_css('#sign-in-email-input', '#sign-in-email')

Использование CSS‑селекторов с Shadow DOM

python
# Подход с CSS‑селектором через выполнение JavaScript
email_input = driver.execute_script("""
    const shadowHost = document.querySelector('#sign-in-email');
    return shadowHost.shadowRoot.querySelector('#sign-in-email-input');
""")

Ключ к успешной автоматизации входа в HBO Max – это понимание того, что элементы формы инкапсулированы внутри Shadow DOM. Используя подходящие методы доступа к Shadow DOM, вы сможете успешно находить и взаимодействовать с этими элементами, несмотря на их изоляцию.

Заключение

  • Shadow DOM – причина проблемы: HBO Max использует пользовательские веб‑компоненты с Shadow DOM, из‑за чего стандартные локаторы не работают.
  • Selenium 4.0+ предоставляет встроенную поддержку: используйте get_shadow_root() или методы JavaScriptExecutor.
  • Существует несколько подходов: выберите в зависимости от версии Selenium и сложности задачи.
  • Явные ожидания критичны: веб‑компоненты часто загружаются асинхронно.
  • Проверяйте и валидируйте: всегда убедитесь, что автоматизация работает надёжно.

Для HBO Max рекомендуем сначала найти элемент gi-form-input в основном DOM, затем получить его корень теней и внутри него найти вложенное поле ввода. Такой шаблон применим к многим современным веб‑приложениям, использующим пользовательские компоненты.

Авторы
Проверено модерацией
Модерация