Почему парсер получает 403 ошибку в Docker и как это исправить

    Запуск веб-скрапинга с использованием undetected_chromedriver внутри Docker-контейнера часто приводит к мгновенной блокировке (403 Forbidden, Guru Meditation), даже если на хосте тот же скрипт работает идеально. Проблема не в IP-адресе, а в уникальных отпечатках контейнера, которые система защиты распознаёт как бота. В этой статье мы разберём причины блокировки и дадим пошаговое решение для Docker с Python 3.11, pyvirtualdisplay (Xvfb) и Chrome.

    Причины блокировки в Docker

    Даже после отключения AutomationControlled, подмены User-Agent и настройки локалей, контейнер выдаёт себя через:

    • WebGL Vendor/Renderer - софтверный SwiftShader (отличается от реального GPU);
    • deviceMemory и hardwareConcurrency - стандартные значения, выдающие виртуальную среду;
    • Параметры экрана - отсутствие реального дисплея (Xvfb) оставляет следы;
    • Заголовки запросов - порядок заголовков может отличаться от браузера реального пользователя.

    Пошаговое решение для Docker

    1. Настройка Dockerfile с Chrome и зависимостями

    Установите все необходимые пакеты для работы Chrome и Xvfb:

    FROM python:3.11-slim
    
    RUN apt-get update && apt-get install -y \
        wget gnupg unzip \
        xvfb x11-utils \
        fonts-liberation \
        libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 \
        libcups2 libdrm2 libdbus-1-3 libxkbcommon0 \
        libxcomposite1 libxdamage1 libxrandr2 \
        libgbm1 libpango-1.0-0 libcairo2 \
        && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
        && echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' >> /etc/apt/sources.list.d/google.list \
        && apt-get update && apt-get install -y google-chrome-stable \
        && rm -rf /var/lib/apt/lists/*

    2. Запуск Xvfb и настройка виртуального дисплея

    Используйте pyvirtualdisplay для создания фейкового экрана с правильными параметрами:

    from pyvirtualdisplay import Display
    from undetected_chromedriver import Chrome, ChromeOptions
    
    display = Display(visible=False, size=(1920, 1080))
    display.start()
    
    options = ChromeOptions()
    options.add_argument('--no-sandbox')
    options.add_argument('--disable-dev-shm-usage')
    options.add_argument('--window-size=1920,1080')
    options.add_argument('--disable-gpu')  # отключаем GPU, чтобы не было софтверного рендеринга
    
    driver = Chrome(options=options)

    3. Маскировка отпечатков через CDP

    После запуска драйвера примените следующие команды Chrome DevTools Protocol:

    import random
    
    # Подмена WebGL Vendor/Renderer
    driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', {
        'width': 1920,
        'height': 1080,
        'deviceScaleFactor': 1,
        'mobile': False,
        'screenWidth': 1920,
        'screenHeight': 1080,
    })
    
    # Подмена deviceMemory и hardwareConcurrency
    driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
        'source': '''
            Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
            Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
        '''
    })
    
    # Подмена User-Agent (реальный браузер)
    driver.execute_cdp_cmd('Network.setUserAgentOverride', {
        'userAgent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    })

    4. Добавление случайных задержек и движений мыши

    Имитируйте поведение человека с помощью ActionChains:

    from selenium.webdriver.common.action_chains import ActionChains
    
    # Случайное движение мыши перед загрузкой страницы
    action = ActionChains(driver)
    action.move_by_offset(random.randint(100, 500), random.randint(100, 500))
    action.perform()
    time.sleep(random.uniform(0.5, 1.5))
    
    driver.get('https://target-site.com')

    5. Проверка локалей и таймзоны

    Убедитесь, что в Dockerfile или при запуске контейнера заданы правильные параметры. Добавьте в docker-compose или при запуске:

    environment:
      - TZ=Europe/Moscow
      - LANG=ru_RU.UTF-8
      - LANGUAGE=ru_RU:ru
      - LC_ALL=ru_RU.UTF-8

    Альтернативный подход: без браузера

    Если вам не нужен полноценный браузер, рассмотрите requests с подменой заголовков или playwright в headless-режиме. Playwright лучше маскируется в Docker из коробки. Пример с Playwright:

    from playwright.sync_api import sync_playwright
    
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True, args=['--no-sandbox'])
        context = browser.new_context(
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            viewport={'width': 1920, 'height': 1080}
        )
        page = context.new_page()
        page.goto('https://target-site.com')
        print(page.title())
        browser.close()

    Заключение

    Блокировка парсера в Docker возникает из-за отпечатков виртуальной среды. Основные шаги для решения: правильная настройка Xvfb, подмена WebGL и hardware-параметров через CDP, имитация пользовательского поведения. Если браузер не обязателен - используйте Playwright или requests с качественной маскировкой заголовков.

    Часто задаваемые вопросы