Как ускорить selenium python
. что делать в Selenium, если страница загружается слишком долго?
Суть проблемы
В предыдущей статье я рассказал, как Selenium ожидает завершения загрузки страницы.
Однако иногда эта стратегия приводит к провалу. Бывают такие ситуации, когда свойство document.readyState либо очень долго не может попасть в состояние complete, либо вообще никогда не достигает этого состояния.
Представьте себе, что на странице приложения есть большая картинка, которая загружается с ооооочень медленного сервера. Вся страница уже давным-давно загрузилась, с ней можно работать, но из-за этой дурацкой картинки браузер продолжает крутить колёсико загрузки, а Selenium продолжает ждать…
Вот реальный пример, который демонстрирует эту проблему:
На моей машине выполнение этого фрагмента кода занимает от 20 до 40 секунд (без учёта времени на запуск браузера). Причина как раз в том, что на страницу грузится большая картинка (
7 мегабайт). При этом нужная кнопка для переключения на английскую версию сайта становится доступна уже через несколько секунд, но Selenium ждёт, пока загрузится вся страница целиком.
Можно ли что-нибудь сделать, чтобы Selenium не ждал так долго?
Таймаут ожидания загрузки
В таком варианте код выполняется примерно за 4 секунды (без учёта времени на запуск браузера).
Быстро – ещё не значит правильно 🙂
Дело в том, что элемент с идентификатором menu есть как на первой странице, так и на второй. В тот момент, когда выполняется клик по кнопке с идентификатором en (переключение на английскую версию сайта), элемент с идентификатором menu тоже присутствует на странице. И Selenium, вместо того, чтобы после клика ждать загрузки второй страницы, немедленно “находит” этот элемент на первой странице.
Ну, раз уж мы отобрали у Selenium и взяли на себя ответственность за ожидание загрузки страницы, надо брать ответственность и за “выгрузку” страницы тоже. То есть перед ожиданием появления элемента, который должен найтись на следующей странице, нужно сначала подождать, пока исчезнет элемент, находящийся на текущей странице. Например, исчезнет та самая кнопка, по которой кликали:
Теперь этот фрагмент кода выполняется примерно 10 секунд (без учёта времени на запуск браузера), и это правильно – около 5 секунд на каждую страницу.
Недостаток описанного выше способа заключается в том, что приходится оборачивать в блок try-catch все вызовы команд, которые могут привести к началу загрузки новой страницы. А это может быть вообще говоря любая команда. То есть исключение TimeoutException нужно ожидать буквально везде. Это ужасно!
К счастью, есть другой способ.
Стратегия ожидания загрузки
В процессе обработки страницы браузер меняет это свойство, отражая информацию о текущем этапе загрузки:
Конечно, и в этом случае тоже приходится брать на себя ответственность за ожидание “выгрузки” страниц.
Вот тот же самый пример, на этот раз без таймаутов, но с изменённой стратегией ожидания:
В этом варианте сценарий тоже отрабатывает примерно за 10 секунд (без учёта времени на запуск браузера).
P.S. Я думаю, вы поняли, что с использованием только неявных (implicit) ожиданий описанные выше трюки сделать не получится.
Автор: Алексей Баранцев
Если вам понравилась эта статья, вы можете поделиться ею в социальных сетях (кнопочки ниже), а потом вернуться на главную страницу блога и почитать другие мои статьи.
Ну а если вы не согласны с чем-то или хотите что-нибудь дополнить – оставьте комментарий ниже, может быть это послужит поводом для написания новой интересной статьи.
Создаем многопоточный веб-парсер с Python и Selenium
После прочтения этой статьи вы сможете:
Настройка проекта
Сначала клонируем репозиторий с кодом нашего проекта. Для этого из командной строки выполним следующие команды:
Приведенные выше команды могут отличаться в зависимости от вашей среды разработки.
Установите ChromeDriver глобально. (Мы же используем версию 85.0.4183.87).
Разбор работы скрипта
Скрипт перебирает и собирает информацию с первых 20 страниц сайта Hacker News о последних статьях, используя Selenium для автоматизации взаимодействия с сайтом и Beautiful Soup для анализа HTML.
Функция connect_to_base() пытается подключиться к сайту Hacker News, а затем использует функционал явного ожидания explicit wait Selenium, чтобы убедиться, что элемент с id = ‘hnmain’ загружен на страницу, прежде чем продолжить и получить из него нужные нам данные.
Просмотрите документацию Selenium для получения дополнительной информации о порядке использования explicit wait.
После запуска скрипта нам потребовалось ожидать около 355 секунд (почти 6 минут) до окончания его работы:
Имейте в виду, что контент может быть не на всех 20 страницах, поэтому прошедшее время может отличаться в вашем случае. Когда этот скрипт запускался, контент присутствовал на 18 страницах (около 530 записей).
Это достаточно большое время. Ну что ж, сначала добавим к нашему коду возможность базового тестирования.
Тестируем наш код
Ниже представлен код файла тестов, который находится в папке test/test_scraper.py:
Убедимся, что все работает как надо:
Настраиваем многопоточность
А теперь самое интересное! Внеся всего лишь несколько изменений в код нашего сценария, мы можем ускорить процесс его выполнения:
Почему в нашем примере мы используем многопоточность вместо многопроцессорности?
Скарпинг веб-страниц в большей степени связан с выполнением операций ввода-вывода I/O, поскольку получение по сети HTML кода (I/O) происходит медленнее, чем непосредственно его парсинг (ЦП). Чтобы узнать об этом больше, а также о разнице между параллелизмом parallelism (многопроцессорностью) и параллелизмом concurrency (многопоточностью), ознакомьтесь со статьей Speeding Up Python with Concurrency, Parallelism, and asyncio.
Запустим наш усовершенствованный парсер:
С кодом нашего парсера, работающего в многопоточном режиме, можно ознакомиться по ссылке.
Чтобы еще более ускорить процесс, мы можем запустить Chrome в headless режиме, передав в качестве аргумента в командной строке значение headless :
Заключение
Немного модифицировав исходный код нашего веб-скарпера мы смогли распараллелить его работу, что позволило сократить время выполнения сценария от 385 секунд до чуть более 35 секунд. В нашем случае этот подход позволил увеличить быстродействие скрипта на 90%, что является достаточно эффективным решением.
Надеюсь, материалы этой статьи помогут вам в вашей работе.
Ускоряем Selenium-тесты
Отключаем native events
Примерная суть native events такова: когда тест натыкается на строку то операционке отправлятся некое событие типа “Введи в поле вооон с теми координатами слово ‘hello’”. Координаты расчитываются как-то сами собой и всё выглядит так, будто пользователь медленно вводит текст с клавиатуры. Проблема в том, что это затрачивает колоссальное количество времени, а нам это не надо.
Чтобы отключить эту фишку надо запускать Firefox с предварительно настроенным профайлом:
Вот так мы получили быстрый драйвер.
Если у Вас есть уже готовый профайл для запуска тестов, то можно создать в нём файл user.js:
Либо немного поменять метод создания драйвера
Отключаем анимацию
Анимация на сайте — это, конечно, вещь, но во время тестов — это зло. Приходится постоянно ждать, когда эта анимация закончится, вставлять какие-то костыли и т.д. И да, анимация сама по-себе съедает часть времени теста. Вообщем, отключаем анимацию на примере jQuery
Дальше работаем с инстансом класса EventFiringWebDriver. В примере я переопределил только метод afterNavigateTo(), который вызывается автоматически после каждого вызова driver.get(someUrl). В реальной жизни этого явно недостаточно. Например, тест жмёт на кнопку и происходит автоматичекий редирект на другую страницу. В этом случае вызова afterNavigateTo() не произойдёт, поэтому приходится переопределять дополнительно beforeClickOn(), чтобы отключить анимацию при первом клике на какой-нибудь элемент после загрузки новой страницы.
Запускаем тесты в параллельном режиме
Изначально, я посматривал в сторону Selenium Grid, но у меня даже дэмку не получилось запустить, да и вообще grid показался мне не очень удобным: нужно запускать хаб и сервер. Поэтому запускаем тесты в двух и более броузерах средствами Maven и TestNG.
Добавляем в pom.xml
Если Вы инжектируете в тесты потоконебезопасные инстансы со scope=«singleton», то необходимо поменять на scope=«prototype». В лучшем случае на данном этапе можно отделаться только конфигурацией конфигов (например Spring’a). Если каждый ваш тест использует уникальное имя пользователя для логина, то всё должно заработать с пол-пинка.
Итоги
Раньше в проекте, над которым я работаю, исполнение Selenium-тестов занимало примерно 5 минут, теперь же — 1,5 минуты.
Как ускорить парсинг данных с Python/Selenium?
В текущем варианте парсинг осуществляется с chromedriver. Практически имею около 100.000 ссылок, по которым находятся таблицы. У каждой таблицы имеется кнопка «Подробнее», которую сейчас нажимает парсер, копирует содержимое попапа, закрывает его и т.д.
В общем чтобы пропарсить наверное миллион таких строк у меня уйдет месяц непрерывной работы селениума. Ищу способ как-то ускорить это.
Проблема, установил небольшие задержки, которые нужны в аккурат дать подгрузиться попапу и дать ему закрыться, иначе возникают ошибки element is not found.
В общем, спасайте. Подскажите как это реально делается, чтобы ускорить работу хотя бы в 10 раз. (за пол часа он прошел около 400 страниц, спарсив около 2000 строк). Это как пройтись мне самому, нажать на каждую ссылку «Подробнее», но копирование отдать скрипту. Это вряд ли можно назвать полной автоматизацией. тем более с такими объемами (не оцениваю их как большие).
Существуют ли «реальные» бустеры таких операций? Я понимаю, что селениум сделан для тестирования или хотя бы для парсинга страниц, где нет кучи попапов, которые все надо прокликать.
upd: после постинга продолжил гуглить и в одном обсуждении нашел следующее:
javascript tables is exactly why I went with selenium for some sites. However, rather than parsing directly with selenium, I was passing driver.page_source (raw html containing whatever javascript generated) to bs4 and parsing with bs4. I was shocked to find out that this round about method was faster than using selenium.find_element_by_XXXXX methods without ever invoking bs4.
На 100к ссылок, особенно если требуется их обходить достаточно часто (или на сервере ресурсов мало), есть уже смысл задумать о более кастомных (читай, напилить руками низкоуровневый механизм), но более быстрых механизмах. Как-то запросы на получение AJAX данных через curl. Или если данные получаться в рамтайме на клиенте через замудренный JS, то применить SpiderMonkey, V8 либо другие серверных движки.
В общем чтобы пропарсить наверное миллион таких строк у меня уйдет месяц непрерывной работы селениума
Делал на кластере из PhantomJS парсер который должен был за 15 минут обходит чуть больше 1к страниц и парсить из них разные хитрые таблички. Требовалось что-то около десяти инстансов PhantomJS, 20 Гб ОЗУ и 16 ядер ЦПУ. На таком кластере 100к за сутки переварит реально.
Когда требование по времени ужесточилось до 5 минут, напилил на SpiderMonkey.
Нужно использовать wait(). Тогда дальше код будет выполняться когда на странице появиться нужный элемент.
где нет кучи попапов, которые все надо прокликать
Наличие/отсутствие попапов не играет роли. Все, что появляется в DOM, все можно отработать. Регулярно тягаю данные с яндекс ворстата. Много там разных хитрых обработчиков. Но все силами PhantomJS-а через webdriver решается рано или поздно.
«Требовалось что-то около десяти инстансов PhantomJS»
Можете показать кусок кода (или направить на реализацию подобного решения), о котором вы говорите? Если я запускаю «тупо» 2 одинаковых скрипта (естественно на разный список ссылок), то я вижу, что первый работает нормально, а второй «плетется», иногда подтормаживая, или вообще останавливаясь. Не знаю точно в чем проблема: соединение, настройки удаленного сервера, или какие-то другие факторы.
«Нужно использовать wait().»
Расставил везде где нужно по time.sleep(1) или wait.until. Запинаний не было.
«Наличие/отсутствие попапов не играет роли. Все, что появляется в DOM, все можно отработать.». Это понятно, что все появляется в DOM. Сейчас в моем примере сервер отдает целый шаблон с html-тегами (а не просто массив данных), который при открытии появляется или наоборот удаляется. Все это ведь надо прокликать, так или иначе. Иначе как дать появится данным в дереве?
Bjornie: >Можете показать кусок кода (или направить на реализацию подобного решения), о котором вы говорите?
У меня PHP. На python-е думаю отличий будет не сильно много. Архитектура такая. Есть класс который запускает заданное количество PhantomJS. Поскольку последний может из коробки работать через webdriver, то интансы запускаются в фоном режиме, при этом каждый из них слушает строго свой заданный локальный порт. Кроме того каждый из них запускается строго через прокси (что бы были заходы с разных IP + на случай бана), у каждого своя прокся. После чего приложение когда нужно соединяется с этим фантомами и отправляет в них требуемые задания. Задачи на загрузку складываются в очередь redis, скрипт который заполняет очередь запускается строго в одном экземпляре (гарантируется через семафоры) и заполняет очередь только если она пустая (тогда задачи не дублируются). Это скрипт запускается кроном. Если другой скрипт (назовем его воркер). Он так же пускается по крону каждую минуту. Он забирает из очереди redis одно задание, отправляет его фантому, парсит страницу, складывает результат в базу, завершает работу. Кусок кода (кластер стартует через startWebDriverCluster):
Ускоряем и стабилизируем автотесты на codeception + selenium
Как прогнать несколько часов автотестов за 5 минут и при этом, чтобы это было стабильно и не вызывало головной боли при каждой сборке? Без лишней воды и вступлений предоставляю вашему внимаю сборник костылей и подпорок элегантных архитектурных решений, без которых невозможно добиться высокой скорости и стабильности автотестов.
Входные данные: фреймворк codeception, сервер с selenium, код на PHP, который нужно протестировать и наша команда Kolesa Group.
Кратко перечислю сборник элегантных архитектурных решений, а затем остановимся на каждом из них подробнее.
Параллельный запуск
В codeception есть возможность поделить тесты на группы с помощью нотации @ group в phpdoc метода или же всего класса. После распределения таким образом тесты на группы происходит их запуск в отдельном процессе, с помощью https://robo.li/.
Как выполнить этот запуск смотрим тут https://robo.li/tasks/Testing/#codecept.
Небольшая подсказка: group($group) set group option. Can be called multiple times.
Сколько делать одновременных потоков — каждый сам должен понять, все зависит от ресурсов сервера, на котором крутятся тесты. На нашем проекте может одновременно крутиться десятки потоков.
Нужно не забыть перезапускать упавшие тесты также в несколько потоков. Если этого не делать, то будет ситуация когда прогон тестов из 10 потоков прошедший за 5 минут будет ждать перезапуска, например, 3 тестов, которые в один поток могут выполняться больше минуты.
Заполнение форм через Javascript
В какой-то момент выяснилось, что заполнение полей форм, а в тестах приходится много заполнять полей всяких разных форм, занимает очень много времени и чтобы сэкономить драгоценное время применена заплатка в виде заполнения поля на javascript.
Мы в Kolesa Group переписали метод fillField библиотеки codeception.
Собственно вот часть кода, отражающая, суть:
Сами причины почему selenium долго заполняет поля текстом не ясны, к сожалению
Перезапуск упавших тестов
Это настолько очевидное и банальное казалось бы, но в самом фреймворке нет такой возможности, на то он и фреймворк как говорится, пусть люди сами делают что хотят.
Кто то скажет, что перезапуск тестов это зло и что вы все неправильно делаете, но реальность такова, что тесты все же падают, и довольно часто. И мы просто смирились с этим и адаптировались к этому.
Перезапуск перезапуску рознь, во-первых каждое падение и перезапуск должно фиксироваться с максимально возможным количеством сопутствующих данных, чтобы разработчик мог оценить и понять причину падений.
Во-вторых нужно иметь под рукой список тестов отсортированных по количеству падений, чтобы понимать над каким тестом стоит провести исправительную работу.
Перезапуск должен быть только тех тестов которые упали — то что не упало не перезапускаем, чтобы не тратить время. Тесты не связаны между собой и запуск одного отдельного теста не должен вызывать проблем.
Перезапуск сессии с selenium
Когда у вас запущено много потоков, а сервер с selenium один и он уставший, может проскальзывать ошибка под названием «Session timed out or not found», и тут нам помогает перезапуск сессии с selenium.
Вот код который делает перезапуск сессии:
robots.txt это текстовой файл находящийся на домене который будет тестироваться, запрос на него нужен, чтобы точно знать, что сессия в браузере рабочая и можно начать выполнение теста, этот блок кода должен выполняться перед каждым тестом.
Подробнее про текстовой файл будет в следующем пункте.
Очистка контекста в браузерных тестах
Чтобы очистить контекст текущей сессии браузера в selenium нужно сделать запрос на любую другую страницу, чтобы поменялась структура документа. Чтобы не нагружать серверы ненужным рендерингом, запрос делается на любой текстовой файл, и так сложилось, что этим файлом у нас стал robots.txt.
Собственно зачем делать эту так называемую очистку контекста? Она нужна для того, чтобы тесты не смогли использовать разметку страницы из предыдущего теста. Часто бывало такое, что браузер находил элементы, которых нет на текущей странице или же, наоборот, не находил то, что нужно было найти и падал с ошибкой. Это происходит из-за того, что сценарий тестов на PHP не дожидается загрузки новой страницы и начинает делать всякие seeElement, waitForText в уже имеющейся(старой) структуре документа и в результате мы не понимаем, что произошло и почему упал тест. И тут то нас спасает предварительный запрос на robots.txt, после него мы имеем чистое место для начала нового тестового сценария.
Перезапуск потока
Проблемы с запуском и прохождением тестов могут быть не только на стороне сервера selenium (вот это неожиданность), но и на исполняющей стороне, там где запущен php процесс, обычно это CI сервер.
Так как на CI сервере происходит очень много всего, то по разным причинам php процессы, внезапно, могут обрываться и тем самым у нас получится что целая группа тестов останется не пройденной и придется ждать ее отдельного перезапуска. Тут к нам спешат на помощь Чип и Дейл проверка состояния процесса и его перезапуск если он вдруг завершится.
Чтобы сделать перезапуск процесса пришлось переопределить метод \Robo\Task\Base\ParallelExec::run
Т.е. по сути там проверка есть ли отчет по данной группе тестов или нет.
Кто уже сталкивался с браузерными тестами или вообще с автотестированием, наверное, знают из каких костылей и подпорок могут состоять эти системы. Я представил большую часть подпорок со стороны кода.
Как видно, большой упор делается на автоматический перезапуск, и если сборка упала, то с высокой долей вероятности это ошибка в коде и нужно смотреть на упавший тест.