LINUX.ORG.RU

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

 , , rtfox-browser, ,


3

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
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.