2025, Oct 19 06:16

Почему headless Chrome в Selenium падает и как починить

Скрипт в Selenium работает в Chrome, но падает в headless? Причина — Access Denied. Показываем, как задать User-Agent, проверить DOM и выровнять поведение.

Безголовый режим Selenium нередко ведет себя не так, как обычное окно браузера. Типичный признак: один и тот же скрипт, который стабильно работает в видимом сеансе Chrome, в headless-режиме падает уже на первом поиске элемента. Ниже — практический пример: поле ввода по ID находится при открытом браузере, но в headless Chrome обращение к нему приводит к ошибке. Разберемся, как добиться одинакового поведения в обоих режимах.

Рабочий пример в видимом браузере

Сниппет ниже открывает страницу схем безопасности ВПП FAA, вводит код в поле с id=ident, отправляет форму, пробегает таблицу результатов и возвращает первую ссылку с нужным доменом.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
import webbrowser
def locate_faa_diagram_url(icao):
    browser = webdriver.Chrome()
    browser.get("https://www.faa.gov/airports/runway_safety/diagrams/")
    search_box = browser.find_element(By.ID, "ident")
    search_box.send_keys(icao)
    search_box.send_keys(Keys.RETURN)
    grid = browser.find_element(By.XPATH, "//table")
    anchors = grid.find_elements(By.TAG_NAME, "a")
    for a in anchors:
        link_url = a.get_attribute("href")
        if link_url:
            if "aeronav.faa" in link_url:
                print(link_url)
                return link_url
webbrowser.open(locate_faa_diagram_url("ATL"))

Тот же сценарий в headless-режиме — и ошибка

В безголовом режиме скрипт спотыкается на поиске поля ввода по ID. Ни сворачивание окна, ни задание размера окна проблему не решают.

from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
from selenium import webdriver
import webbrowser
def locate_faa_diagram_url_headless(icao):
    opts = Options()
    opts.add_argument("--headless")
    opts.add_argument("--window-size=1920x1080")
    browser = webdriver.Chrome(options=opts)
    browser.get("https://www.faa.gov/airports/runway_safety/diagrams/")
    search_box = browser.find_element(By.ID, "ident")  # здесь ошибка в headless
    search_box.send_keys(Keys.RETURN)
    grid = browser.find_element(By.XPATH, "//table")
    anchors = grid.find_elements(By.TAG_NAME, "a")
    for a in anchors:
        link_url = a.get_attribute("href")
        if link_url:
            if "aeronav.faa" in link_url:
                print(link_url)
                return link_url
webbrowser.open(locate_faa_diagram_url_headless("ATL"))

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

Что происходит на самом деле

В headless-режиме сайт блокирует доступ и возвращает страницу Access Denied вместо ожидаемого контента. Драйвер пытается найти элемент по ID уже в другом документе, поэтому поиск и падает.

<html>
 <head>
  <title>Access Denied</title>
 </head>
 <body>
  <h1>Access Denied</h1>
  You don't have permission to access "http://www.faa.gov/airports/runway_safety/diagrams/" on this server.
  <p>Reference #18.56633b8.1754209447.14b9f2c8</p>
  <p>https://errors.edgesuite.net/18.56633b8.1754209447.14b9f2c8</p>
 </body>
</html>

Несоответствие связано с тем, как headless Chrome идентифицирует себя и как его обрабатывают антибот- и CDN-слои. В безголовом режиме обычно отправляется немного иной User-Agent и иные значения по умолчанию (например, размер вьюпорта), из‑за чего часть инфраструктуры блокирует доступ или отдает урезанную разметку. Указание распространенного User-Agent снижает различия между обычным и headless-запуском.

Как убедиться, что вас блокируют

Сохраните исходник страницы сразу после перехода и посмотрите, что реально пришло. Если вместо ожидаемой страницы там документ Access Denied, поиск элементов будет падать, потому что DOM уже другой.

from bs4 import BeautifulSoup
# разместите это сразу после browser.get(...)
dom = BeautifulSoup(browser.page_source)
with open("faa_dump.html", "w", encoding="utf-8") as fh:
    fh.writelines(dom.prettify())

Решение: задать User-Agent в headless Chrome

Добавьте строку User-Agent в параметры Chrome. Так headless станет ближе к обычному настольному Chrome, и страница загрузится так же, как в видимом сеансе.

from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.chrome.options import Options
from selenium import webdriver
import webbrowser
def fetch_diagram_link_headless(icao):
    opts = Options()
    opts.add_argument("--headless")
    opts.add_argument("--window-size=1920x1080")
    opts.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36")
    browser = webdriver.Chrome(options=opts)
    browser.get("https://www.faa.gov/airports/runway_safety/diagrams/")
    search_box = browser.find_element(By.ID, "ident")
    search_box.send_keys(icao)
    search_box.send_keys(Keys.RETURN)
    grid = browser.find_element(By.XPATH, "//table")
    anchors = grid.find_elements(By.TAG_NAME, "a")
    for a in anchors:
        link_url = a.get_attribute("href")
        if link_url:
            if "aeronav.faa" in link_url:
                print(link_url)
                return link_url
webbrowser.open(fetch_diagram_link_headless("ATL"))

Если для вас важен конкретный вьюпорт, учтите: стандартные флаги изменения размера окна в современном headless Chrome срабатывают не всегда. В качестве альтернативы можно явно задать параметры экрана флагом: --screen-info={0,0 1920x1080}.

Почему это важно

Безголовая автоматизация — основа для CI-пайплайнов и серверного скрейпинга. Если сайт распознает headless-сеансы или обращается с ними иначе, тесты и сбор данных начинают незаметно расходиться с реальным поведением. Приведение User-Agent к «обычному» уменьшает расхождения и спасает от хрупких скриптов, которые проходят локально, но падают в автоматизации. Проверка реального DOM, который вы получили, не дает тратить время на «призрачные» проблемы селекторов, когда корень — в ограничениях доступа на стороне сайта.

Выводы

Если скрипт работает в видимом браузере, но падает в headless на тех же селекторах, сначала проверьте, какой HTML вы действительно получили. Если приходит Access Denied или урезанная разметка, настройте headless-сеанс под «обычный» браузер — задайте распространенный User-Agent. Соблюдайте функциональный порядок действий (включая ввод значений до отправки форм) и не полагайтесь только на window-size в headless; при необходимости задавайте параметры экрана явно. Так безголовые прогоны остаются предсказуемыми и ближе к поведению реальных пользователей.

Статья основана на вопросе с StackOverflow от RJTTAZ и ответе Ajeet Verma.