Современный веб-скрейпинг с Python и Selenium
Опубликовано: 2022-03-11Веб-скрапинг использовался для извлечения данных с веб-сайтов почти с момента рождения Всемирной паутины. Раньше парсинг в основном производился на статических страницах — с известными элементами, тегами и данными.
Однако в последнее время передовые технологии веб-разработки несколько усложнили задачу. В этой статье мы рассмотрим, как мы можем выполнять очистку данных в случае, если новые технологии и другие факторы препятствуют стандартной очистке.
Традиционный анализ данных
Поскольку большинство веб-сайтов создают страницы, предназначенные для чтения человеком, а не для автоматического чтения, веб-скрапинг в основном состоял из программного анализа данных разметки веб-страницы (например, щелчок правой кнопкой мыши, просмотр исходного кода), а затем обнаружения статических шаблонов в этих данных, которые позволили бы программе «считывать» различные фрагменты информации и сохранять их в файл или базу данных.
Если бы данные отчета нужно было найти, часто данные были бы доступны путем передачи либо переменных формы, либо параметров с URL-адресом. Например:
https://www.myreportdata.com?month=12&year=2004&clientid=24823
Python стал одним из самых популярных языков парсинга веб-страниц, отчасти благодаря различным веб-библиотекам, которые были созданы для него. Одна популярная библиотека, Beautiful Soup, предназначена для извлечения данных из файлов HTML и XML, обеспечивая поиск, навигацию и изменение тегов (т. е. дерева синтаксического анализа).
Парсинг на основе браузера
Недавно у меня был парсинг-проект, который казался довольно простым, и я был полностью готов использовать традиционный парсинг для его обработки. Но по мере того, как я углублялся в это, я обнаружил препятствия, которые нельзя было преодолеть традиционными методами.
Три основные проблемы помешали мне использовать стандартные методы парсинга:
- Сертификат. Для доступа к той части веб-сайта, где находились данные, требовалось установить сертификат. При доступе к начальной странице появилось приглашение выбрать правильный сертификат из тех, что установлены на моем компьютере, и нажать «ОК».
- фреймы. На сайте использовались фреймы, которые мешали моему обычному парсингу. Да, я мог бы попытаться найти все URL-адреса iframe, а затем создать карту сайта, но это казалось громоздким.
- JavaScript. Доступ к данным осуществлялся после заполнения формы с параметрами (например, идентификатор клиента, диапазон дат и т. д.). Обычно я пропускаю форму и просто передаю переменные формы (через URL или как скрытые переменные формы) на страницу результатов и смотрю результаты. Но в данном случае форма содержала JavaScript, который не позволял мне нормально обращаться к переменным формы.
Итак, я решил отказаться от своих традиционных методов и посмотреть на возможный инструмент для парсинга на основе браузера. Это будет работать иначе, чем обычно — вместо того, чтобы переходить непосредственно на страницу, загружать дерево синтаксического анализа и извлекать элементы данных, я вместо этого «веду себя как человек» и использую браузер, чтобы перейти на нужную мне страницу, а затем очистить данные - таким образом, обходя необходимость иметь дело с упомянутыми барьерами.
Селен
В целом, Selenium хорошо известен как среда тестирования веб-приложений с открытым исходным кодом, позволяющая специалистам по контролю качества выполнять автоматизированные тесты, выполнять воспроизведение и реализовывать функции удаленного управления (что позволяет использовать множество экземпляров браузера для нагрузочного тестирования и несколько типов браузеров). В моем случае это казалось полезным.
Мой любимый язык для парсинга веб-страниц — Python, так как он имеет хорошо интегрированные библиотеки, которые обычно могут обрабатывать все необходимые функции. И, конечно же, для Python существует библиотека Selenium. Это позволило бы мне создать экземпляр «браузера» — Chrome, Firefox, IE и т. д. — а затем представить, что я сам использую браузер, чтобы получить доступ к данным, которые я искал. И если бы я не хотел, чтобы браузер действительно появлялся, я мог бы создать браузер в «безголовом» режиме, сделав его невидимым для любого пользователя.
Настройка проекта
Чтобы начать экспериментировать, мне нужно было настроить свой проект и получить все необходимое. Я использовал машину с Windows 10 и убедился, что у меня относительно обновленная версия Python (это была версия 3.7.3). Я создал пустой скрипт Python, затем загрузил библиотеки, которые, по моему мнению, могут потребоваться, используя PIP (установщик пакетов для Python), если у меня еще не загружена библиотека. Вот основные библиотеки, с которых я начал:
- Запросы (для выполнения HTTP-запросов)
- URLLib3 (обработка URL)
- Beautiful Soup (на случай, если Selenium не справится со всем)
- Selenium (для браузерной навигации)
Я также добавил в скрипт некоторые параметры вызова (используя библиотеку argparse), чтобы можно было экспериментировать с различными наборами данных, вызывая скрипт из командной строки с разными параметрами. К ним относятся идентификатор клиента, с месяца/года и до месяца/года.
Задача 1 – Сертификат
Первый выбор, который мне нужно было сделать, это какой браузер я собирался использовать Selenium. Поскольку я обычно использую Chrome, и он построен на проекте Chromium с открытым исходным кодом (также используется браузерами Edge, Opera и Amazon Silk), я решил сначала попробовать его.
Я смог запустить Chrome в скрипте, добавив необходимые мне компоненты библиотеки, а затем выполнив пару простых команд:
# Load selenium components from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait, Select from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException # Establish chrome driver and go to report site URL url = "https://reportdata.mytestsite.com/transactionSearch.jsp" driver = webdriver.Chrome() driver.get(url)
Поскольку я не запускал браузер в безголовом режиме, браузер действительно появился, и я мог видеть, что он делает. Он сразу же попросил меня выбрать сертификат (который я установил ранее).
Первой проблемой, которую нужно было решить, был сертификат. Как выбрать правильный и принять его, чтобы попасть на сайт? В моем первом тесте скрипта я получил это приглашение:
Это было нехорошо. Я не хотел вручную нажимать кнопку OK каждый раз, когда запускал свой скрипт.
Как оказалось, мне удалось найти обходной путь — без программирования. Хотя я надеялся, что Chrome может передавать имя сертификата при запуске, этой функции не существовало. Однако в Chrome есть возможность автоматически выбирать сертификат, если в реестре Windows существует определенная запись. Вы можете настроить его так, чтобы он выбирал первый сертификат, который он увидит, или указать более конкретные параметры. Поскольку у меня был загружен только один сертификат, я использовал общий формат.
Таким образом, с этим набором, когда я сказал Selenium запустить Chrome и появилось приглашение сертификата, Chrome «автоматически выберет» сертификат и продолжит работу.
Проблема 2 — фреймы
Итак, теперь я был на сайте, и появилась форма, предлагающая мне ввести идентификатор клиента и диапазон дат отчета.

Изучив форму в инструментах разработчика (F12), я заметил, что форма представлена в iframe. Итак, прежде чем я мог начать заполнять форму, мне нужно было «переключиться» на нужный iframe, где существовала форма. Для этого я задействовал функцию переключения Selenium, например:
# Switch to iframe where form is frame_ref = driver.find_elements_by_tag_name("iframe")[0] iframe = driver.switch_to.frame(frame_ref)
Хорошо, теперь в правом фрейме я смог определить компоненты, заполнить поле идентификатора клиента и выбрать раскрывающиеся списки дат:
# Find the Customer ID field and populate it element = driver.find_element_by_name("custId") element.send_keys(custId) # send a test id # Find and select the date drop-downs select = Select(driver.find_element_by_name("fromMonth")) select.select_by_visible_text(from_month) select = Select(driver.find_element_by_name("fromYear")) select.select_by_visible_text(from_year) select = Select(driver.find_element_by_name("toMonth")) select.select_by_visible_text(to_month) select = Select(driver.find_element_by_name("toYear")) select.select_by_visible_text(to_year)
Проблема 3 — JavaScript
На форме оставалось только «нажать» кнопку «Найти», чтобы начался поиск. Это было немного сложно, так как казалось, что кнопка «Найти» управляется JavaScript и не является обычной кнопкой типа «Отправить». Проверив его в инструментах разработчика, я нашел изображение кнопки и смог получить его XPath, щелкнув правой кнопкой мыши.
Затем, вооружившись этой информацией, я нашел элемент на странице и кликнул по нему.
# Find the 'Find' button, then click it driver.find_element_by_xpath("/html/body/table/tbody/tr[2]/td[1]/table[3]/tbody/tr[2]/td[2]/input").click()
И вуаля, форма отправлена и данные появились! Теперь я мог просто очистить все данные на странице результатов и сохранить их по мере необходимости. Или я мог?
Получение данных
Во-первых, мне пришлось обрабатывать случай, когда поиск ничего не нашел. Это было довольно просто. Он будет отображать сообщение в форме поиска, не выходя из нее, что-то вроде «Нет записей». Я просто искал эту строку и тут же останавливался, если находил ее.
Но если результаты приходили, данные представлялись в div со знаком плюс (+), чтобы открыть транзакцию и показать все ее детали. Открытая транзакция показывала знак минус (-), который при нажатии закрывал div. Нажатие на знак плюс вызовет URL-адрес, чтобы открыть его div и закрыть любой открытый.
Таким образом, необходимо было найти все знаки плюса на странице, собрать URL-адрес рядом с каждым, а затем пройтись по ним, чтобы получить все данные для каждой транзакции.
# Loop through transactions and count links = driver.find_elements_by_tag_name('a') link_urls = [link.get_attribute('href') for link in links] thisCount = 0 isFirst = 1 for url in link_urls: if (url.find("GetXas.do?processId") >= 0): # URL to link to transactions if isFirst == 1: # already expanded + isFirst = 0 else: driver.get(url) # collapsed +, so expand # Find closest element to URL element with correct class to get tran type tran_type=driver.find_element_by_xpath("//*[contains(@href,'/retail/transaction/results/GetXas.do?processId=-1')]/following::td[@class='txt_75b_lmnw_T1R10B1']").text # Get transaction status status = driver.find_element_by_class_name('txt_70b_lmnw_t1r10b1').text # Add to count if transaction found if (tran_type in ['Move In','Move Out','Switch']) and (status == "Complete"): thisCount += 1
В приведенном выше коде поля, которые я извлек, были типом транзакции и статусом, а затем добавлены к подсчету, чтобы определить, сколько транзакций соответствует указанным правилам. Однако я мог бы получить другие поля в деталях транзакции, такие как дата и время, подтип и т. д.
Для этого проекта количество было возвращено вызывающему приложению. Однако эти и другие извлеченные данные также могли быть сохранены в плоском файле или базе данных.
Дополнительные возможные препятствия и решения
При очистке современных веб-сайтов с помощью собственного экземпляра браузера могут возникнуть многочисленные другие препятствия, но большинство из них можно решить. Вот некоторые из них:
Попытка найти что-то до того, как оно появится
Просматривая себя, как часто вы обнаруживаете, что ждете появления страницы, иногда в течение многих секунд? То же самое может произойти и при программной навигации. Ищешь класс или другой элемент — а его нет!
К счастью, у Selenium есть возможность ждать, пока он не увидит определенный элемент, и может истечь время ожидания, если элемент не появляется, например:
element = WebDriverWait(driver, 10). until(EC.presence_of_element_located((By.ID, "theFirstLabel")))
Прохождение капчи
Некоторые сайты используют Captcha или что-то подобное для предотвращения нежелательных роботов (которыми они могут считать вас). Это может затормозить парсинг веб-страниц и замедлить его.
Простые подсказки (например, «сколько будет 2 + 3?») обычно легко читаются и разбираются. Однако для более продвинутых барьеров существуют библиотеки, которые могут помочь попытаться их взломать. Некоторые примеры: 2Captcha, Death by Captcha и Bypass Captcha.
Структурные изменения сайта
Веб-сайты предназначены для изменений — и они часто меняются. Вот почему при написании скрипта парсинга лучше помнить об этом. Вам следует подумать о том, какие методы вы будете использовать для поиска данных, а какие не использовать. Рассмотрите методы частичного сопоставления вместо того, чтобы пытаться сопоставить целую фразу. Например, веб-сайт может изменить сообщение с «Нет записей» на «Нет записей», но если ваше совпадение соответствует «Нет записей», все должно быть в порядке. Кроме того, подумайте, следует ли сопоставлять XPATH, идентификатор, имя, текст ссылки, имя тега или класса или селектор CSS — и что с наименьшей вероятностью изменится.
Резюме: Python и Selenium
Это была краткая демонстрация, чтобы показать, что почти любой веб-сайт можно парсить, независимо от того, какие технологии используются и какие сложности возникают. По сути, если вы можете просматривать сайт самостоятельно, его, как правило, можно очистить.
Теперь, в качестве предостережения, это не означает, что каждый веб-сайт должен быть очищен. Некоторые из них имеют законные ограничения, и было множество судебных дел, решающих законность парсинга определенных сайтов. С другой стороны, некоторые сайты приветствуют и поощряют извлечение данных со своего веб-сайта, а в некоторых случаях предоставляют API для упрощения работы.
В любом случае, лучше ознакомиться с условиями перед началом любого проекта. Но если вы все же пойдете вперед, будьте уверены, что вы сможете выполнить эту работу.
Рекомендуемые ресурсы для сложного парсинга веб-страниц:
- Расширенный веб-скрейпинг Python: лучшие практики и обходные пути
- Масштабируемый парсер своими руками: как создавать и запускать парсеры в больших масштабах