Python. Настройка selenium для парсинга сайтов

Alexandr Sokolov
5 min readMay 31, 2021

--

https://www.selenium.dev

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

Любую проблему можно решить введением дополнительного уровня абстракции, кроме проблемы чрезмерного количества уровней абстракции

Kevlin Henney

Инструмент selenium-wire частично решает эти задачи. Однако для работы selenium-wire поднимает прокси на пайтоне, что добавляет еще один промежуточный слой. А установленные куки каждый раз отправляются на сервер, полностью игнорируя заголовок Set-Cookie.

И все таки есть способ установить прокси с авторизацией (и не только) без введения дополнительного уровня абстракции.

Выбор браузера

Ghostdriver не подходит, потому что не все фишки JS поддерживает, из-за этого вид страницы может поменяться, но самое страшное это получить окно с просьбой обновить браузер.

Chromium не трогаем тоже, потому что имеются трудности с поиском портативной версии.

И наш выбор это Firefox.

Скачать портативную версию Firefox можно по ссылке https://ftp.mozilla.org/pub/firefox/releases/. Я беру “72.0.2/linux-x86_64/en-US/”.

Для работы с браузером потребуется geckodriver, это прослойка между кодом на пайтон и самим браузером. Берем последнюю версию из репозитория https://github.com/mozilla/geckodriver/releases.

После распаковки требуется дать права на запуск geckodriver и firefox/firefox-bin.

Настройка профиля

Начнем с настройки профиля и отключим предупреждения связанные с сертификатами SSL

from selenium.webdriver import FirefoxProfile
class
Profile(FirefoxProfile):
accept_untrusted_certs = True
assume_untrusted_cert_issuer = False

Следует отключить WebRTC, эта технология, в некоторых случаях, может привести к раскрытию реального IP адреса.

def __init__(self, profile_directory=None):
super().__init__(profile_directory)

# отключаем WebRTC
self.set_preference("media.peerconnection.enabled", False)

Далее нужно отключить запросы от сайтов на отправку push-уведомлений, чтобы подобные окна не перекрывали нам основной контент

self.set_preference("dom.webnotifications.enabled", False)

Отключаем navigator.webdriver, этот параметр позволяет отслеживать, что браузер запущен в режиме удаленного управления. Если этого не сделать, то, например, не выйдет пройти капчу от Cloudflare. Он будет детектить вас как бота и не пустит на сайт, постоянно заставляя решить капчу.

self.set_preference("dom.webdriver.enabled", False)

И отключаем предупреждение при открытии окна ‘about:config’

self.set_preference("browser.aboutConfig.showWarning", False)

С настройкой профиля закончили.

from pathlib import Path
from
selenium.webdriver import Firefox
BASEDIR = Path(__file__).parent
class Profile: ...
class Client(Firefox):
def __init__(self):
profile = Profile()
super().__init__(firefox_profile=profile,
firefox_binary=BASEDIR.joinpath("bin", "firefox", "firefox-bin"),
executable_path=BASEDIR.joinpath("bin", "geckodriver"))

Установка прокси, куки и заголовки

С этими тремя параметрами связаны следующие трудности

  • Установить прокси для FF возможно стандартными средствами, однако в таком случае не будет поддержки прокси с авторизацией по логину и паролю. Так как selenium не поддерживает окна для ввода паролей, а FF не поддерживает установку этих параметров в настройках.
  • Куки же установить можно только для той страницы, которая на данный момент открыта. Т.е. чтобы поставить куку для сайта его сначала нужно загрузить, потом установить куку и перезагрузить.
  • Настройка заголовков не поддерживается вовсе.

Я решил эти проблемы через написание своего аддона, именно поэтому в настройках профиля отключено подтверждение перехода на страницу настроек. Вот ссылка чтобы скачать его https://addons.mozilla.org/ru/firefox/addon/selenium-helper/.

Аддон настраивается через страницу конфигурации, потому что иного способа передать настройки я не нашел. Нужно сделать следующее

  • Установить дополнение
  • Перейти на страницу настроек
  • Заполнить поля и нажать на кнопку

Со вторым пунктом требуется потрудиться, так как для открытия страницы настроек дополнения необходимо знать внутренний UUID дополнения, который браузер присваивает дополнению после установки.

Для получения внутреннего UUID опишем функцию парсинга настроек из ‘about:config’

from functools import lru_cache

from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.keys import Keys



class Client(Firefox):
...

@lru_cache()
def _get_preference(self, name):

def get_search_box_with_wait_about_config_approved(_attemps=1):
try:
search_box = self.find_element_by_id("about-config-search")
except NoSuchElementException:
if _attemps > 1:
raise
input("Approve warning message and press Enter to continue...")
return get_search_box_with_wait_about_config_approved(_attemps + 1)
else:
return search_box

self.get("about:config")

search_box = get_search_box_with_wait_about_config_approved()
search_box.clear()
search_box.send_keys(name)
search_box.send_keys(Keys.ENTER)

search_result = self.find_elements_by_xpath("//table[@id='prefs']/tr/td/span")
return search_result[0].text

Добавляем метод для получения установленных дополнений

import json
def
get_installed_addons(self):
return json.loads(self._get_preference("extensions.webextensions.uuids"))

И дописываем init

class Client(Firefox):
def __init__(self, proxy=None, cookies=None, headers=None):
profile = Profile()
super().__init__(firefox_profile=profile,
firefox_binary=BASEDIR.joinpath("bin", "firefox", "firefox-bin"),
executable_path=BASEDIR.joinpath("bin", "geckodriver"))

if any((proxy, cookies, headers)):
uuid = self.install_addon("selenium_helper.xpi")
internal_uuid = self.get_installed_addons()[uuid]
self.get(f"moz-extension://{internal_uuid}/data/options.html")

Итак, если при инициализации класса нам передали proxy и/или куки и/или заголовки, то установим аддон, получим его внутренний uuid и откроем страницу с настройками.

С прокси все достаточно просто, принимать будем строку в формате ‘type://username:password@host:port’ (у этого формата есть название, но я его не помню). А вот для кук есть условия: должен быть список словарей — это раз, у каждого элемента должен быть ключ url — это два.

Подробнее об этом можно почитать тут https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/cookies/set

Поэтому напишем метод, который будет проверять, что у всех элементов списка есть ключ url.

def raise_if_url_not_set_for(self, cookie):
all(x["url"] for x in cookie)

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

Из прокси нужно тоже сделать словарь

proxy = urllib.parse.urlparse(proxy)
proxy = {
"type": proxy.scheme,
"host": proxy.hostname,
"port": proxy.port,
"username": proxy.username,
"password": proxy.password
}

Ну и теперь отправляем все эти словари в метод ‘saveOptionsEx’ который доступен на странице настроек.

if any((proxy, cookies, headers)):
if cookies:
self.raise_if_url_not_set_for(cookies)
uuid = self.install_addon("selenium_helper.xpi")
internal_uuid = self.get_installed_addons()[uuid]
self.get(f"moz-extension://{internal_uuid}/data/options.html")
if proxy:
proxy = urllib.parse.urlparse(proxy)
proxy = {
"type": proxy.scheme,
"host": proxy.hostname,
"port": proxy.port,
"username": proxy.username,
"password": proxy.password
}

js = f"""saveOptionsEx({json.dumps(proxy)}, {json.dumps(cookies)}, {json.dumps(headers)});"""
self.execute_script(js)
self.get("about:blank")

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

if __name__ == '__main__':
browser = Client()
try:
browser.get("https://whoer.net/")
input("Enter")
finally:
browser.close()

Первый запуск ломается

AttributeError: ‘PosixPath’ object has no attribute ‘strip’

Второй запуск — отлично. Теперь поставим куки

browser = Client(cookies=[{"url": "https://whoer.net/", "name": "test", "value": "1"}])

И получается следующее: вылетаем в исключение NS_ERROR_FILE_UNRECOGNIZED_PATH — это раз и, что самое страшное, у нас останется висеть окно браузера — это два.

Окно браузера осталось потому что ошибка возникла при иницализации класса, а инстанс класса создается до блока try..except. Мы не можем засунуть ее в этот блок, так как не будем уверены, что в блоке finally доступен объект browser, что потребует дополнительных проверок. Нужно убрать логику из init и больше никогда так не делать.

Вынесем логику настройки аддона в метод post_init

class Client(Firefox):
def __init__(self):
profile = Profile()
# profile.set_preference("general.useragent.override", "user-agent")
super().__init__(firefox_profile=profile,
firefox_binary=str(BASEDIR.joinpath("bin", "firefox", "firefox-bin")),
executable_path=str(BASEDIR.joinpath("bin", "geckodriver")))

def post_init(self, proxy=None, cookies=None, headers=None):
if any((proxy, cookies, headers)):
if cookies:
self.raise_if_url_not_set_for(cookies)
uuid = self.install_addon("selenium_helper.xpi")
internal_uuid = self.get_installed_addons()[uuid]
self.get(f"moz-extension://{internal_uuid}/data/options.html")
if proxy:
proxy = urllib.parse.urlparse(proxy)
proxy = {
"type": proxy.scheme,
"host": proxy.hostname,
"port": proxy.port,
"username": proxy.username,
"password": proxy.password
}

js = f"""saveOptionsEx({json.dumps(proxy)}, {json.dumps(cookies)}, {json.dumps(headers)});"""
self.execute_script(js)
self.get("about:blank")

И соответствующим образом изменится и код запуска

if __name__ == '__main__':
browser = Client()
try:
browser.post_init(cookies=[{"url": "https://whoer.net/", "name": "test", "value": "1"}])
browser.get("https://whoer.net/")
input("Enter")
finally:
browser.close()

А NS_ERROR_FILE_UNRECOGNIZED_PATH заключается в требовании указать абсолютный путь к файлу дополнения для метода install_addon()

uuid = self.install_addon(str(Path("selenium_helper.xpi").absolute()))

Запускаем, открывается страница whoer.net, переключаемся на расширенный вывод и в разделе HTTP headers находим куки.

Отлично. Попробуем поставить все

if __name__ == '__main__':
browser = Client()
try:
browser.post_init(
proxy="http://***:***@***:8000",
cookies=[{"url": "https://whoer.net/", "name": "test", "value": "1"}],
headers={"User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27"})
browser.get("https://whoer.net/")
input("Enter")
finally:
browser.close()

Вам нужно лишь заменить прокси. Прокси без авторизации тоже поддерживаются.

Иные способы добиться того же результата

Как я уже сказал прокси можно поставить стандартными средствами

from selenium.webdriver import Firefox
from selenium.webdriver.common.proxy import ProxyType

proxy = "host:3128"
proxy = Proxy({
'proxyType': ProxyType.MANUAL,
'httpProxy': proxy,
'ftpProxy': proxy,
'sslProxy': proxy,
'noProxy':'localhost,127.0.0.1'})

driver = Firefox(proxy=proxy)
driver.get('https://whoer.net/')

Логин с паролем придется вводить руками.

Куки поставить также можно https://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webdriver.WebDriver.add_cookie

driver = Firefox()
driver.get('https://whoer.net/')
driver.add_cookie({"test": "1"})
driver.get('https://whoer.net/')

Ну и если из заголовков требуется только User-Agent менять, то это настраивается через профиль.

Sign up to discover human stories that deepen your understanding of the world.

--

--

Alexandr Sokolov
Alexandr Sokolov

Written by Alexandr Sokolov

Software Engineer. Python Developer.

No responses yet

Write a response