LINUX.ORG.RU

rtfox-browser: форк undetected-chromedriver с прокси-туннелем, заменой прокси на лету и изоляцией воркеров

 , , rtfox-browser, ,


4

1

Парсинг сайтов: как мы сделали rtfox-browser — форк undetected-chromedriver с поддержкой SOCKS5, HTTP, HTTPS

⚠️ Статья носит познавательный и развлекательный характер. Автор не призывает к нарушению правил сервисов.

Всем привет! Недавно мы столкнулись с темой парсинга. Начали искать инструменты — BeautifulSoup, Selenium, Playwright, Puppeteer. Сразу скажу: мы не конкуренты этим библиотекам, далее объясним почему.

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

Как мы нашли отправную точку

Нам попалась библиотека undetected-chromedriver. Библиотека перестала поддерживаться разработчиком, но функционал сохранялся. При первом запуске сразу столкнулись с ошибкой — проблема была в патчинге ChromeDriver и его скачивании. После исправления запуск удался и капча была пройдена. В дальнейшем возникли проблемы с прокси и многопроцессорностью.

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

Как устроена цепочка Chrome → ChromeDriver → Selenium

Прежде чем рассказывать что мы сделали — разберём архитектуру изнутри.

Звено 1 — Chrome

Обычный браузер при запуске открывает WebSocket-сервер на каком-то порту и слушает команды DevTools Protocol (CDP). Через WebSocket можно отправить например:

{
  "id": 1,
  "method": "Page.navigate",
  "params": {"url": "https://google.com"}
}

Звено 2 — ChromeDriver

ChromeDriver — это отдельная программа, которая работает как переводчик между вашим кодом (Selenium) и браузером Chrome. Вы пишете на Python driver.get("https://google.com"), Selenium превращает это в HTTP-запрос и отправляет ChromeDriver. А ChromeDriver уже объясняет Chrome на его родном языке (DevTools Protocol) что нужно открыть страницу.

ChromeDriver одновременно является:

  • HTTP-сервером (слушает порт, принимает команды от Selenium)
  • WebSocket-клиентом (подключается к Chrome, отправляет CDP-команды)

Звено 3 — Selenium

Selenium — это библиотека для Python которая позволяет программно управлять браузером. Selenium не управляет браузером напрямую — он отправляет команды ChromeDriver, а тот уже разбирается с Chrome.

Полная цепочка запуска

Ваш код: driver = webdriver.Chrome()
         ↓
Selenium запускает ChromeDriver как отдельный процесс
         ↓
ChromeDriver запускает Chrome с флагами автоматизации

При запуске Chrome, ChromeDriver автоматически выставляет флаги автоматизации и сообщает что браузером управляет автоматическое ПО. Библиотека undetected-chromedriver как раз с этим и разбирается — она патчит ChromeDriver, убирает эти флаги и патчит сигнатуру.

Полная цепочка запроса

Ваш код: driver.get("https://google.com")
         ↓
Selenium → ChromeDriver: HTTP POST /session/abc/url
         ↓
ChromeDriver → Chrome: WebSocket команда Page.navigate
         ↓
Chrome → Google: HTTP GET /
         ↓
Google → Chrome: HTML страница
         ↓
Chrome → ChromeDriver: WebSocket ответ (готово)
         ↓
ChromeDriver → Selenium: HTTP 200 OK
         ↓
Selenium: возвращает управление вашему коду

Что мы реализовали

1. Автоматическое определение платформы и версии Chrome

Первым делом нам нужно было автоматически определять платформу (Windows, Linux, macOS) чтобы корректно получать версию установленного Chrome.

Пример для Linux:

google-chrome --version
# Google Chrome 124.0.6367.91

Из этой строки извлекается major-версия (124).

Получение версии ChromeDriver:

  • Для Chrome ≤ 114: обращается к старому CDN chromedriver.storage.googleapis.com
  • Для Chrome ≥ 115: использует новый сервис chrome-for-testing, парсит JSON с актуальными версиями

Скачивание и распаковка:

  • Формирует URL для скачивания ZIP-архива
  • Скачивает во временную папку
  • Распаковывает архив
  • Ищет файл chromedriver (или .exe)
  • Перемещает его в папку проекта /drivers

Патч бинарника — что такое cdc-сигнатура?

В оригинальном ChromeDriver есть строка {window.cdc_adoqpoas...} — её используют сайты для обнаружения ботов. Патч делает следующее:

  • Ищет в бинарнике паттерн {window.cdc.*?;}
  • Заменяет его на заглушку {console.log("ok")}

Благодаря этому сайты не могут определить что управление идёт через Selenium.

2. Многопроцессорность

С оригинальной библиотекой при запуске 10 процессов каждый запускал один и тот же chromedriver. Как мы уже знаем, Selenium запускает chromedriver на своём порту. Каждый новый воркер просто перезапускал chromedriver, создавал новый порт — и прошлый воркер падал.

Решение простое: мы не даём воркерам взаимодействовать с одним chromedriver. У нас есть эталонная версия (пропатченная). Каждый воркер получает только свою копию и работает с ней. При завершении копия удаляется.

Также реализована межпроцессорная блокировка:

  • Если 100 воркеров одновременно вызовут ensure_driver(), они не будут скачивать драйвер 100 раз
  • Первый процесс захватывает файловую блокировку (через fcntl на Linux/Mac или msvcrt на Windows)
  • Остальные ждут пока первый не скачает и не пропатчит драйвер
  • Затем все используют уже готовый файл

worker_id — опциональный параметр. Если не передан, UUID назначается автоматически. Передавайте явно только если нужны предсказуемые имена в логах:

# с явным именем
driver = uc.Chrome(worker_id="worker_1")

# UUID назначится автоматически
driver = uc.Chrome()

3. Прокси с авторизацией (SOCKS5, HTTP, HTTPS)

Chrome из коробки не умеет работать с SOCKS5 и авторизацией — только HTTP/HTTPS, и при этом требует заполнять данные во всплывающем окне. Вот как мы решили эту проблему.

Мы запускаем локальный прокси-сервер с двумя потоками:

  • Поток-слушатель — принимает трафик от Chrome на localhost
  • Поток-отправитель — подключается к внешнему прокси и отправляет логин/пароль

Chrome получает флаг --proxy-server=127.0.0.1:56789 и шлёт туда весь трафик, думая что это обычный прокси без авторизации. А наш локальный прокси сам добавляет логин и пароль и форвардит запросы на внешний прокси-сервер.

Полная картина:

Chrome получает флаг --proxy-server=127.0.0.1:56789
         ↓
Chrome думает: "Это обычный прокси без авторизации"
         ↓
Chrome отправляет HTTP-запрос на локальный порт 56789
         ↓
Поток-слушатель принимает запрос
         ↓
Поток-отправитель:
  - подключается к внешнему прокси (1.2.3.4:1080)
  - отправляет логин и пароль
  - пересылает HTTP-запрос
         ↓
Получает ответ от внешнего прокси
         ↓
Возвращает ответ Chrome через поток-слушатель

Поддерживаемые протоколы: SOCKS5, HTTP, HTTPS.

Прокси можно передать тремя способами:

# Dict style
driver = uc.Chrome(proxy={
    "host": "1.2.3.4",
    "port": 1080,
    "type": "socks5",   # socks5 | http | https
    "user": "my_login",
    "pass": "my_password",
})

# Keyword style
from rtfox_browser.proxy_types import ProxyType

driver = uc.Chrome(
    proxy_host="1.2.3.4",
    proxy_port=1080,
    proxy_type=ProxyType.SOCKS5,
    proxy_user="my_login",
    proxy_pass="my_password",
)

# ProxyConfig style
from rtfox_browser.proxy_types import ProxyConfig, ProxyType

config = ProxyConfig.from_args(
    host="1.2.3.4",
    port=1080,
    proxy_type=ProxyType.SOCKS5,
    user="my_login",
    password="my_password",
)
driver = uc.Chrome(proxy=config)

Важная особенность: локальный туннель запускается всегда — даже если прокси не передан (прозрачный режим). Это позволяет менять прокси на лету в любой момент сессии.

4. Замена прокси на лету

Одна из ключевых фич — замена прокси без перезапуска браузера:

driver = uc.Chrome()
driver.get("https://2ip.ru/")

# Переключаемся на новый прокси
driver.proxy_replacement(
    host="1.2.3.4",
    port=1080,
    proxy_type=ProxyType.SOCKS5,
    user="my_login",
    password="my_password",
)
driver.refresh()

# Или через dict
driver.proxy_replacement({
    "host": "1.2.3.4",
    "port": 1080,
    "type": "http",
})

# Отключить прокси — прямое соединение
driver.proxy_replacement(None)

По умолчанию после замены очищаются кэш и куки браузера, чтобы Chrome открыл свежие соединения через новый прокси. Передайте clear_connections=False чтобы пропустить этот шаг.

5. Изолированное логирование через loguru

Loguru из коробки поддерживает глобальный логер. Нам это не подходило — для прокси нужен свой изолированный логер с возможностью включать через флаг, для ChromeDriver свой, для пользователя свой.

Решение — создаём новый экземпляр логера и добавляем фильтры:

_lib_logger = Logger(
    core=Core(),
    exception=None,
    depth=0,
    record=False,
    lazy=False,
    colors=False,
    raw=False,
    capture=True,
    patchers=[],
    extra={},
)

_lib_logger.add(sys.stderr, level="ERROR")

def get_logger(debug: bool = False, debug_proxy: bool = False):
    _lib_logger.remove()

    _lib_logger.add(
        sys.stderr,
        level="DEBUG" if debug else "ERROR",
        format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | "
               "<cyan>Chrome</cyan> - <level>{message}</level>",
        filter=lambda r: r["extra"].get("source") != "proxy"
    )

    _lib_logger.add(
        sys.stderr,
        level="DEBUG" if debug_proxy else "ERROR",
        format="<blue>{time:HH:mm:ss}</blue> | <level>{level: <8}</level> | "
               "<magenta>PROXY</magenta> - <level>{message}</level>",
        filter=lambda r: r["extra"].get("source") == "proxy"
    )
    return _lib_logger

В Chrome и прокси привязываем источник:

self.logger = self.logger_str.bind(source="chrome")

6. Модуль капчи с автозагрузкой солверов

Мы реализовали автоматическую загрузку солверов из папки solvers/. Это значит: есть класс с логикой прохождения капчи — просто кладёшь .py файл в папку и метод автоматически появляется.

BaseCaptchaSolver — базовый класс:

from abc import ABC, abstractmethod

class BaseCaptchaSolver(ABC):
    name: str = None
    _service = None

    @property
    def driver(self):
        return self._service.driver

    @property
    def api_key(self):
        return self._service.api_key

    @abstractmethod
    def solve(self, **kwargs) -> bool:
        ...

Чтобы написать свой солвер достаточно:

  1. Унаследоваться от BaseCaptchaSolver
  2. Задать name
  3. Реализовать метод solve()

Loader проходит по всем .py файлам в папке solvers/, находит классы-солверы и регистрирует их по имени.

Итог

Четыре основные проблемы решены:

  • ✅ Обход CloudFlare из коробки
  • ✅ SOCKS5 / HTTP / HTTPS прокси с авторизацией
  • ✅ Замена прокси на лету без перезапуска браузера
  • ✅ Многопроцессорность с изоляцией воркеров

Плюс добавили изолированное логирование и модуль капчи с плагин-архитектурой.

Библиотека доступна на GitLab и PyPI:

Установка и использование

Установка

pip install rtfox-browser

Требования: Python >= 3.9

Базовый запуск

import rtfox_browser as uc

driver = uc.Chrome()
driver.get("https://example.com")
driver.quit()

Запуск с прокси

import rtfox_browser as uc

driver = uc.Chrome(proxy={
    "host": "1.2.3.4",
    "port": 1080,
    "type": "socks5",   # socks5 | http | https
    "user": "my_login",
    "pass": "my_password",
})
driver.get("https://example.com")
driver.quit()

Запуск нескольких процессов

from multiprocessing import Pool
import rtfox_browser as uc

def run_worker(worker_id):
    driver = uc.Chrome(
        worker_id=worker_id,
        proxy={
            "host": "1.2.3.4",
            "port": 1080,
            "type": "socks5",
            "user": "my_login",
            "pass": "my_password",
        }
    )
    driver.get("https://example.com")
    driver.quit()

if __name__ == "__main__":
    with Pool(4) as pool:
        pool.map(run_worker, ["w1", "w2", "w3", "w4"])

Замена прокси на лету

import rtfox_browser as uc
from rtfox_browser.proxy_types import ProxyType

driver = uc.Chrome()
driver.get("https://2ip.ru/")

driver.proxy_replacement(
    host="1.2.3.4",
    port=1080,
    proxy_type=ProxyType.SOCKS5,
    user="my_login",
    password="my_password",
)
driver.refresh()

# Вернуться к прямому соединению
driver.proxy_replacement(None)

Решение капчи

import rtfox_browser as uc
from rtfox_browser.captcha import CaptchaService

driver = uc.Chrome()
driver.get("https://example.com")

captcha = CaptchaService(api_key="YOUR_2CAPTCHA_KEY", driver=driver)
print(captcha.available())  # ['ebay_hcaptcha', 'aws_image']
captcha.ebay_hcaptcha()

Обработка ошибок прокси

from rtfox_browser.exceptions import (
    ProxyError,
    ProxyConnectionError,
    ProxyAuthError,
    ProxyTimeoutError,
    ProxyInvalidAddressError,
    ProxyConfigError,
)

try:
    driver = uc.Chrome(proxy={...})
except ProxyConfigError:
    print("Invalid or incomplete proxy config")
except ProxyInvalidAddressError:
    print("Malformed host or port")
except ProxyAuthError:
    print("Wrong credentials")
except ProxyTimeoutError:
    print("Proxy not responding")
except ProxyConnectionError:
    print("Could not connect to proxy")
except ProxyError:
    print("General proxy failure")

Включение отладочных логов

import rtfox_browser as uc

# Логи ChromeDriver
driver = uc.Chrome(log_debug=True)

# Логи прокси
driver = uc.Chrome(log_debug_proxy=True)

# Все логи
driver = uc.Chrome(log_debug=True, log_debug_proxy=True)


Проверено: maxcom ()
Последнее исправление: Dark_bear (всего исправлений: 5)

https://ru.wikipedia.org/wiki/Многопроцессорность

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

Многопроцессность вполне употребяемый термин в IT-среде. См. например многопроцессность в питоне

Сама статья познавательная, спасибо.

MirandaUser2 ★★
()

exe

Что за тип файлов такой? Впервые слышу.
Я как опытный пользователь ОС Линукс ни разу с такими не сталкивался. Поэтому сомневаюсь, что это касается тематики форума.

Mishahack
()
Последнее исправление: Mishahack (всего исправлений: 1)
Ответ на: комментарий от Mishahack

Я как опытный пользователь ОС Линукс ни разу с [exe] не сталкивался.

s/пытны/днобоки/

Слышал про SMB? Это один из столпов Linux-инфраструктуры. Так вот, у первых SMB-серверов изначально расширение было exe.

kaldeon ★★
()
Последнее исправление: kaldeon (всего исправлений: 5)
Ответ на: комментарий от kaldeon

тем временем, portable executable - единственный надёжный кроссдистрибутивный формат исполняемых файлов

mittorn ★★★★★
()
Ответ на: комментарий от kaldeon

Он как «опытный пользователь» явно что то слышал

Dark_bear
() автор топика
Ответ на: комментарий от MirandaUser2

Многопроцессорность это наличие двух и более процов в одном компе.

А данная статья выглядит похожей на продукцию чатгпт.

firkax ★★★★★
()
Ответ на: комментарий от kaldeon

Слышал про SMB? Это один из столпов Linux-инфраструктуры.

Ты про тот SMB, который сетевой протокол майкрософта или какой-то другой? Если про тот, то утверждение про «один из столпов» крайне сомнительно, хотя его реализация по имени Samba, конечно же, используется много где. Столп – это NFS.

hobbit ★★★★★
()
Последнее исправление: hobbit (всего исправлений: 1)
Ответ на: комментарий от Mishahack

Это файл, скомпилированный средой исполнения mono.

Aceler ★★★★★
()

Нам нужно было обойти

Дальше не читал. Ненужно.

a1ba ★★★★
()

Selenium — это библиотека для Python

Не только под python, он поддерживает много языков.

С оригинальной библиотекой при запуске 10 процессов каждый запускал один и тот же chromedriver ... Каждый новый воркер просто перезапускал chromedriver, создавал новый порт — и прошлый воркер падал.

Вообще не понял в чём проблема. Selenium прекрасно работает с потоками.

Chrome из коробки не умеет работать с SOCKS5 и авторизацией

Как раз Chrome умеет. Но вместо --proxy-server нужно использовать --proxy и --proxy-auth

Rodegast ★★★★★
()

какую проблему решает эта статья? какие-то манипуляции не предусмотренные авторами сайтов?

unclestephen ★★★★★
()
Ответ на: комментарий от unclestephen

какие-то манипуляции не предусмотренные авторами сайтов?

Вот прям мою недодуманную мысль высказал.

DrRulez ★★★★★
()
Ответ на: комментарий от Rodegast

Selenium прекрасно работает с потоками.

Это не является потокобезопасно, лучше использовать процессы

Но вместо –proxy-server нужно использовать –proxy и –proxy-auth

–proxy и –proxy-auth нет таких флагов в дефолтом хроме

Dark_bear
() автор топика
Ответ на: комментарий от Dark_bear

Это не является потокобезопасно, лучше использовать процессы

Откуда такие выводы?

нет таких флагов в дефолтом хроме

Нет в chromium в chrome есть.

Rodegast ★★★★★
()
Ответ на: комментарий от Rodegast

Нет в chromium в chrome есть.

Google Chrome 123.0.6312.86 - нету в man такого. Может быть в более свежих добавили?

Я полагаю, что ТС была важна поддержка разных версий Google Chrome/Chromium, чтобы иметь разные fingerprint`ы. Для этого же запуск под разными ОС нативных версий.

MirandaUser2 ★★
()
Ответ на: комментарий от unclestephen

Статья описывает с какими проблемами мы столкнулись, и как их решили.

Не обязательно сразу «манипуляции», есть тестирование

Dark_bear
() автор топика
Ответ на: комментарий от DrRulez

Ну как зачем. Автоматизированный спам конечно же.

a1ba ★★★★
()
Ответ на: комментарий от DrRulez

Очевидно web scrapping. Вполне себе бизнес. Даже вакансии на HH присутствуют.

MirandaUser2 ★★
()
Ответ на: комментарий от MirandaUser2

нету в man такого. Может быть в более свежих добавили?

Там много чего нету :D

Rodegast ★★★★★
()

Не думал что когда-то это напишу, НО, это реально же иишный мусор. Начиная от слов типа «воркер» в заголовке и заканчивая всем остальным.

Я за бан.

Loki13 ★★★★★
()
Ответ на: комментарий от Loki13

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

mittorn ★★★★★
()
Ответ на: комментарий от Mishahack

exe - какой-то устаревший формат, иногда wine помогает.

nempyxa ★★
()
Ответ на: комментарий от mittorn

Вытащили с того света – это как раз нормально. Вот только @Dark_bear так и не ответил, зачем тут явные упоминания exe.

Тут два варианта. Либо просто решение кроссплатформенное, тогда надо про это упомянуть и причесать пути и имена файлов под *никсовую практику, мы всё-таки на сайте про операционную систему Linux и другие Unix-системы. Либо действительно какой-то экстраординарный случай, требующий плясок с wine/Mono/etc. Тогда надо уточнить, почему нативщина не работает. (Но по контексту непохоже, я думаю, это всё-таки про случай 1.)

hobbit ★★★★★
()
Ответ на: комментарий от hobbit

Ну все эти драйвера конечно имеют бинарники (ну и собираются само собой) под Linux/macos тоже обычно (ну я командую лисой через них с линукса).

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

recei
()
Ответ на: комментарий от hobbit

меня системно слал лесом (сбер)мегамаркет - просто не открывал контент. яндекс очень любит капчу спрашивать, хотя куки есть.

nempyxa ★★
()
Ответ на: комментарий от hobbit

Да здесь 1 вариант, и решение кросс платформенное.

Когда пользователь создает экземпляр. Мы проверяем есть ли хром драйвер. Если нет логика такая, определение платформы. В зависимости от платформы, будет разная логика.

Windows — PowerShell macOS — запускаем Google Chrome –version Linux — ищем google-chrome, chromium и т.д. через shutil.which.

Когда мы определили платформу, и версию. Формируется ссылка для скачивания хром драйвера Далее драйвер распаковываться, и ложиться в либу. В линукс это бинарник, виндовс это .exe . Также меняется механизм блокировки

Вот только @Dark_bear так и не ответил, зачем тут явные упоминания exe.

Поправил

Dark_bear
() автор топика
Последнее исправление: Dark_bear (всего исправлений: 1)
Ответ на: комментарий от nempyxa

Возможно была старая версия хрома. Или прокси

У меня обычно проблем не возникало никогда

Dark_bear
() автор топика
Ответ на: комментарий от Dark_bear

определение платформы. В зависимости от платформы, будет разная логика.

Чудненько, вы не только спамите, вы еще и спамите с протрояненых машин)

goingUp ★★★★★
()
Ответ на: комментарий от goingUp

Нет))) Если человек переживает, за целостность данных. Есть же ссылка на открытый код

Dark_bear
() автор топика
Ответ на: комментарий от goingUp

Ну даже не смешно. Почему-то, кажется админам, что злые «спамеры» спамят 24/7 и обязательно с ботентов. Но порядки дикого запада (востока) давно прошли и нормальные, вменяемые люди с парсерами парсят с «обычной» для обычного пользователя скоростью, т.е., соблюдая временные лимиты. Но! В интернетах есть люди, которые выкладывают данные (совершенно свободно доступные) в сеть, но в удобоваримом формате, нет, не продают их, а требуют просто миллион справок, разрешений итд. Пока все проходит этот миллион согласований, может минуть и полгода-год. Могут ли простые люди это изменить? Нет. Могут ли нанять 100500 мартышек которые ручками все скачают - могут. Но зачем, если можно пользоваться парсером.

recei
()

На лоре есть отдельная такая темка, люди любят осуждать других, хотя сами не сделали нечто классное. Норм проект, удобно, не надо думать как проксировать соединение))

REDDERa
()
Ответ на: комментарий от unclestephen

Если maxcom пропустил статью, значит для чего-то она нужна) Нельзя же перечить ему, он создателб сего:)

REDDERa
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.