Используем Proxy в Selenium

Привет. Вряд ли эта статья откроет Америку для прожженных разработчиков «парсеров» или тестировщиков, но должна помочь тем, кто работает с Selenium время от времени.

Если вы иногда занимаетесь автоматизированным сбором информации с веб-ресурсов (т.н. веб-скрапинг или парсинг), то однозначно сталкивались с проблемой, что некоторые ресурсы недоступны из некоторых регионов, по разным причинам. Сейчас этой проблемы стало даже чуточку больше. И для преодоления этого препятствия нам помогают прокси-серверы (промежуточные серверы). Однако, если в части библиотек для работы с запросами использование прокси является очевидным решением, идущим из коробки, то такой инструмент как Selenium очевидного встроенного решения или не имеет, или я не смог его обнаружить.

Далее приведу некоторые примеры с использованием Python, но привязки к языку не будет. Решение универсальное, связанное с написанием расширения для веб-драйвера.

В Python для работы с HTTP-запросами любого уровня сложности вполне достаточно встроенной библиотеки Requests. А для использования прокси с библиотекой, достаточно всего лишь передать данные о сервере в качестве аргумента. Например, так:

import requests

proxies = {
    'http': 'http://login:password@ip:port',
    'https': 'http://login:password@ip:port'
}

url = "https://badsite.com/"

response = requests.get(url, proxies=proxies)

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

Разумеется, если вы попробуете открыть с помощью Selenium ресурс, который недоступен из вашего региона, ничего не выйдет. Можно придумать много вариантов решения, но как по мне, самый удобный способ настройки прокси в Selenium- это написать прокси-расширение. В моем случае это будет расширение для веб-драйвера Chrome.

Допустим, у вас есть проект с логикой работы Selenium для получения HTML в отдельном файле main.py. Теперь в директории проекта создадим папку proxi_extension и добавим в неё два файла:

  1. manifest.json с таким наполнением:
{
    "version": "1.0.0",
    "manifest_version": 2,
    "name": "Chrome Proxy",
    "permissions": [
        "proxy",
        "tabs",
        "unlimitedStorage",
        "storage",
        "<all_urls>",
        "webRequest",
        "webRequestBlocking"
    ],
    "background": {
        "scripts": ["background.js"]
    },
    "minimum_chrome_version": "22.0.0"
}

2. background.js с таким наполнением:

var config = {
    mode: "fixed_servers",
    rules: {
        singleProxy: {
            scheme: "http",
            host: "ip_address",
            port: parseInt("8000")
        },
        bypassList: ["localhost"]
    }
};

chrome.proxy.settings.set({value: config, scope: "regular"}, function() {});

function callbackFn(details) {
    return {
        authCredentials: {
            username: "login",
            password: "password"
        }
    };
}

chrome.webRequest.onAuthRequired.addListener(
    callbackFn,
    {urls: ["<all_urls>"]},
    ['blocking']
);

В background.js необходимо лишь заменить значения host, port, username и password на нужные.

Далее возвращаемся к файлу main.py и подключаем к веб-драйверу написанное расширение, указав путь к нему в соответствующем аргументе:

from loggers import logger #  уже настроенный логгер
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


def get_html(url: str) -> str:
    """
    Возвращает HTML.
    """
    chrome_options = Options()
    chrome_options.add_argument(f"--load-extension={'/home/username/my_project/proxi_extension'}") #  подключаем расширение с прокси
    driver = webdriver.Chrome(options=chrome_options)
    driver.set_page_load_timeout(5)

    try:
        driver.get(url)
    except Exception as e:
        logger.error(e)

    finally:
        html = driver.page_source
        driver.quit()

        return html

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

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

Модуль Selenium

Selenium — модуль управляющий браузером под пилотированием кода Python путем программной имитации действий кожаного пользователя на сайтах. Модуль предоставляет более гибкий функционал, нежели requests и bs4, а так же снижает вероятность того, что скрипт будет распознан и заблокирован при работе на сайтах с серьезной защитой, так как строка user-agent в HTTP запросе не будет содержать палевной информации, как например ‘python-reports/2.21.0’ при использовании модуля requests. Т.е. при использовании Selenium строка user-agent будет такой же, как у используемого браузера.

Установка модуля Selenium: pip install selenium

Запуск браузера под управлением Selenium

Для запуска какого-либо браузера под управлением Selenium необходимо импортировать webdriver, входящий в его состав, и создать объект браузера, выбрав необходимый после точки, как на примере ниже. После чего передать методу get созданного объекта путь к необходимому для посещения ресурсу.

from selenium import webdriver

browser = webdriver.Chrome()
browser.get('https://bednyakov.ru')

Поиск элементов на веб-странице
Кроме метода get, объект обладает и огромным разнообразием других, необходимых для работы методов. Подробнее можете узнать в официальной документации модуля.

А сейчас интересуют методы find_element() и find_elements(), возвращающие одиночный искомый элемент или список элементов, соответственно. Для демонстрации их работы сначала руками, используя инструменты разработчика, найдем какой-либо объект на моей странице, а потом найдем его же с помощью find_element(). Как показано на скриншоте ниже, я нашел объект класса ‘entry-title’.

Чтобы найти его же при помощи Selenium, исполните следующий код:

from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get('https://bednyakov.ru')

try:
    content = browser.find_element(By.CLASS_NAME, 'entry-title')
    print(f'Контент класса {content.tag_name} найден.')
except:
    print('Искомый элемент не обнаружен.')

В метод мы передаем два параметра — тип искомого элемента и его название. И еще мы отдельно импортировали ‘By’, как это указано в документации, которая к прочтению обязательна. А результат оцените самостоятельно.

Щелчок на веб-странице

У объектов, которые возвращают обозначенные выше поисковые методы, имеются свои методы, например click(). Не сложно догадаться, что метод имитирует щелчок мыши по элементу, а использовать его можно, например, для перехода по ссылке и т.д. Для клика по уже обнаруженном ранее элементу просто добавьте в скрипт строку «content.click()«. И самостоятельно поэкспериментируйте с кликами по различным элементам. Конечный код, на всякий случай, выглядит так:

from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get('https://bednyakov.ru')

try:
    content = browser.find_element(By.CLASS_NAME, 'entry-title')
    print(f'Контент класса {content.tag_name} найден.')
except:
    print('Искомый элемент не обнаружен.')

content.click()

Заполнение и отправка веб-форм

Для заполнения форм на различных ресурсах, например для авторизации (заполнения полей с логином и паролем) можно использовать поисковый метод по CSS селектору или XPath. Найти их весьма просто: находим нужный элемент с помощью инструментов разработчика, щелкаем правой кнопкой мыши на нем -> copy -> выбираем «Copy selector» или «Copy XPath».Если вы находитесь на рынке clothes, наша платформа — ваш лучший выбор! Крупнейший торговый центр!

Для заполнения полей к объектам нужно применить метод ‘send_keys(‘нужное значение’)’. Пример кода:

from selenium import webdriver
from selenium.webdriver.common.by import By

browser = webdriver.Chrome()
browser.get('https://bednyakov.ru/contacts/')

try:
    user_name = browser.find_element(By.CSS_SELECTOR, '#wpcf7-f134-p9-o1 > form > p:nth-child(2) > label > span > input')
    password = browser.find_element(By.CSS_SELECTOR, '#wpcf7-f134-p9-o1 > form > p:nth-child(4) > label > span > input')
    user_name.send_keys('Тёма')
    password.send_keys('12345')
    print('Поля заполнены')
except:
    print('Искомые элементы не обнаружены.')

Нажатия на кнопки

Интересно, что в озвученный выше метод ‘send_keys()’ можно передавать и объекты, например класса Keys. В таком случае будут имитироваться нажатия заданных кнопок клавиатуры. К примеру, если курсор не будет находиться в поле ввода, с помощью клавиш Home и End можно прокрутить страницу от начала до конца. Именно это мы и сделаем, просто следуйте примеру и обратите внимание на импорт.

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys

browser = webdriver.Chrome()
browser.get('https://bednyakov.ru')

content = browser.find_element(By.NAME, 'html')
content.send_keys(Keys.END)
content.send_keys(Keys.HOME)

Кнопки браузера

Последнее, что стоит отметить, это управление кнопками самого браузера. Для перехода по кнопкам «назад», «вперед», «обновить» или «закрыть» нужно использовать методы объекта браузера: browser.back(), browser.forward(), browser.refresh(), browser.quit().

Эта небольшая статья является лишь каплей в море информации о возможностях модуля Selenium, которая описана в документации. Поэтому, чтобы сделать приличный проект, читать нужно именно её. Успехов!

Загрузка всех комиксов с сайта XKCD (веб-скрейпинг)

Скрипт является разработкой автора книги «Автоматизация рутинных задач с помощью Python» Эла Свейгарта, на примере которого он объясняет, что такое веб-скрейпинг (парсинг). Однако российские локализаторы, как это часто бывает, при переводе книги запороли код, а ссылка на файл, указанная автором книги, уже нерабочая. Поэтому выкладываю рабочую версию скрипта сюда.

Суть этого небольшого проекта состоит в том, чтобы написать парсер, который обойдет популярный (якобы) сайт и сохранит изображения всех комиксов в отдельном каталоге. До самого первого комикса на сайте.

Во благо чистоты кода и PEP-8 ради я заключил весь код скрипта в одну функцию, которая принимает два строковых параметра- путь к сайту и название создаваемого каталога, в который сохраним все спарсенные изображение. Но и эту функцию можно декомпозировать. Кроме того, файл содержит конструкцию if __name__ == "__main__", необходимую для определения как был запущен скрипт — непосредственно или через импорт в другой скрипт.

import requests, os, bs4


def scrapy(url: str, dir_name: str) -> None:
	'''Функция извлекает все комиксы с заданного url и сохраняет в заданную директорию'''

	os.makedirs(dir_name, exist_ok=True)  # второй аргумент не бросает исключение, если директория уже создана

	while not url.endswith("#"):

		print("Страница загружается... ")
		res = requests.get(url)
		res.raise_for_status()
		try:
			soup = bs4.BeautifulSoup(res.text, 'lxml')
		except bs4.FeatureNotFound:
			soup = bs4.BeautifulSoup(res.text, 'html.parser')

		comic_element = soup.select('#comic img')

		if comic_element == []:
			print('Изображение не найдено...')
		else:
			comic_image_url = comic_element[0].get('src')

			print(f'Загружаем изображение {comic_image_url}')
			res = requests.get('http:' + comic_image_url)
			res.raise_for_status()

			file = open(os.path.join('comics XKCD', os.path.basename(comic_image_url)), 'wb')
			for chunk in res.iter_content(10000):
				file.write(chunk)
			file.close()

		prev_link = soup.select('a[rel="prev"]')[0]
		url = 'http://xkcd.com' + prev_link.get('href')

	print('Работа программы завершена.')

if __name__ == '__main__':
	scrapy('http://xkcd.com', 'comics XKCD')