Перевод статьи «Use Selenium with Python to Target the XPath of a Particular Object».
Содержание
- Введение
- Библиотека Selenium Python
- Что такое XPath?
- Проблема: XPath ищет по всему документу
- Решение: ограничение поиска XPath внутри определенного элемента
- Тестирование отрефакторенного кода
- Заключение
Введение
В начале этого года мне нужно было написать программу для друга, которая бы собирала данные одной из коллекций NFT на сайте NFTrade. Он хотел получить полный список всех доступных NFT в коллекции и стоимость каждого из них в долларах США, определенную по текущей рыночной цене криптовалюты BNB, за которую продается NFT. Кроме того, он хотел, чтобы эти данные были представлены в виде строк в CSV-файле, который он мог бы затем удобно сортировать и обрабатывать.
К сожалению, у сайта NFTrade нет публичного API, поэтому вместо того, чтобы писать скрипт на Node.js для получения данных через HTTP-запросы, я создала небольшой скрипт для перехода на страницу сайта и парсинга (сбора) данных с нее.
Не имея опыта написания парсера, я решила написать программу на Python, поскольку он является очень популярным языком для решения подобных задач. Пока я создавала этот парсер, требования проекта развивались и усложнялись, и я узнала множество новых полезных приемов работы с Python.
После того как я решила использовать связку Selenium Python для запуска экземпляра Selenium WebDriver и парсинга данных с NFTrade, я столкнулась с проблемой: данные собирались для каждого NFT через цикл для просмотра и извлечения данных, но каждый раз, когда цикл выполнялся, он собирал данные только из первого NFT в списке.
Я была в тупике и обратилась за помощью (как всегда) к Stack Overflow.
При извлечении данных со страницы с помощью XPath в Selenium WebDriver, для поиска внутри определенного элемента, а не по всей странице, перед XPath необходимо добавить точку (.
). В данной статье я покажу вам, как это использовать.
ПРИМЕЧАНИЕ: обычно я не занимаюсь разработкой на Python, поэтому мои примеры кода могут быть не самыми эффективными или изящными, но они выполняет свою задачу.
БЕСПЛАТНО СКАЧАТЬ КНИГИ в телеграм канале "Библиотека тестировщика"
Selenium Python
Для своего проекта я решила использовать библиотеку Selenium Python, так как она может парсить данные с сайтов с динамически загружаемой информацией, что необходимо для NFTrade. Пользователь заходит в коллекцию NFT, и по мере того, как он прокручивает страницу вниз, в окне браузера периодически подгружаются новые NFT с помощью JavaScript.
Selenium Python управляет Selenium WebDriver и работает с браузером так же, как это делает пользователь. Несмотря на то, что изначально он использовался для автоматизированного end-to-end тестирования ПО, его также можно использовать для сбора данных с динамических веб-страниц.
Подробнее о действиях с элементами, и настройке Selenium на Python смотрите также: “Обработка WebElements в Selenium Python”.
Что такое XPath?
Среди множества полезных методов, входящих в библиотеку Selenium, есть методы find_element(), find_elements(), и By.XPATH .
find_element
делает то, что следует из его названия: находит элемент (или элементы), используя стратегию By
и локатор.
By
принимает id элементов, имена, атрибуты, XPaths, имена классов и т.д. А XPath – это синтаксис, используемый для навигации по элементам и атрибутам в стандартном XML-документе (веб-странице).
Из-за структуры сайта NFTrade я использовала XPath, чтобы находить все отдельные NFT на странице и собирать данные о каждом из них, для последующего добавления этих данных в CSV-файл.
Проблема: XPath ищет по всему документу
После того как я написала код для запуска Selenium WebDriver, перехода на сайт NFTrade и загрузки NFT в браузере, откуда я хотела собрать данные, у меня появился список информации о NFT, который мне нужно было упростить, оставив только те данные, которые нужно было перенести в CSV-файл.
Нужно взять только значения id NFT (часть имени карточки NFT) и цену продажи в BNB.
Внутри метода __main__
в Python-скрипте я собрала данные со страницы с помощью метода get_cards()
. Затем я хотела просмотреть собранный массив данных и извлечь только те, которые требовались по условию, для каждой NFT-карточки с помощью метода get_nft_data()
.
Вот код метода __main__
(файл for_sale_scraper.py
):
if __name__ == '__main__': scraper = ForSaleNFTScraper(); cards = scraper.get_cards(max_card_count=200) card_data = [] for card in cards: info = (scraper.get_nft_data(card)) card_data.append(info) # вывод данных карточки для проверки, что получаем нужные данные для каждой pprint(card_data)
Далее код метода get_nft_data()
:
def get_nft_data(self, nft_data): """Извлечение и вывод необходимых данных NFT.""" nft_name_element = self.driver.find_element(By.XPATH, '//div[contains(@class, "Item_itemName__ckoHR")]') nft_name = nft_name_element.get_attribute("innerHTML") nft_id = nft_name.partition('#')[-1] nft_price_element = nft_data.find_element(By.XPATH, '//div[contains(@class, "Item_itemPriceValueTxt__lblqJ")]') nft_price = nft_price_element.get_attribute("innerHTML") return { 'id': int(nft_id), 'price': nft_price }
Метод get_nft_data()
должен делать следующее:
- Использовать XPath внутри каждой карточки, чтобы получить имя NFT через
get_attribute("innerHTML")
(innerHTML
указывает на текстовое содержимое элемента, которое включает его ID-значение в конце имени). Далее методpartition
разделяет на 3 части строки, содержащие в названиии#
, и забирает последнюю из них. Эта часть является значением ID. Последний элемент трех частей находится через[-1]
. - Получить цену NFT (тоже используя
get_attribute("innerHTML")
) через второй XPath в карточке NFT. - И, наконец, возвращать эти два значения вместе в виде нового объекта с ключами id и price.
Я хотела, чтобы это работало для каждой карточки NFT, которую я добавляла в список card_data
. На практике же я получила 200 элементов, которые содержали информацию об ID и цене только самой первой карточки в моем списке.
Решение: ограничение поиска XPath внутри определенного элемента
После нескольких неудачных вариаций приведенного выше кода и безуспешного поиска в многочисленных постах Stack Overflow я наконец написала свой собственный пост на SO, попросив помощи у сообщества разработчиков.
Спустя чуть более 30 минут после публикации я получила ответ, который помог мне снова продвинуться вперед.
Ниже приведен исправленный код метода get_nft_data()
, который действительно получает данные из каждого отдельного NFT в процессе выполнения цикла. Я также добавила несколько комментариев между строками кода, чтобы объяснить, что происходит на каждом шаге.
def get_nft_data(self, nft_data): """Извлечение и вывод необходимых данных NFT.""" # получение полного имени карточки "NFT_CARD #1234" по XPATH nft_name_element = nft_data.find_element(By.XPATH, './/div[contains(@class, "Item_itemName__ckoHR")]') nft_name = nft_name_element.text # парсинг только значения ID из имени nft_id = nft_name.partition('#')[-1] # получение значения текущей стоимости NFT через XPath nft_bnb_sale_price = nft_data.find_elements(By.XPATH, './/div[contains(@class, "Item_itemPriceValueTxt__lblqJ")]') # если есть цена, достать ее if nft_bnb_sale_price: nft_price = nft_bnb_sale_price[0].text else: # если значения цены нет, присвоить None nft_price = None return { 'id': int(nft_id), 'nft_price': nft_price }
В этой версии кода есть три основных отличия.
- Вместо использования
self.driver.find_element
в этом коде используетсяnft_data.find_element
. Подстановкаnft_data
вместоself.driver
позволяет ограничить поиск XPath конкретным элементом. - Внутри каждого метода
find_element
, ссылающегося наBy.XPATH
, в начале XPath есть (.
). Таким образом,'//div[contains(@class, "Item_itemName__ckoHR")]'
поменялся на ‘.//div[contains(@class, "Item_itemName__ckoHR")]'
. - Наконец, мне предложили использовать
.text
для получения названия NFT и цены BNB вместо того, чтобы каждый раз прописывать более длинный.get_attribute("innerHTML")
для получения текста в NFT, что улучшило читабельность кода.
Точка (.
) дополнительно ограничивает поиск XPath внутри определенного элемента (или “контекстного узла”). Если (.
) не нет, то XPath будет искать по всему документу, поэтому при каждом запуске цикла он всегда находил значения из первого элемента NFT.
В решении также упоминалось, что есть вероятность того, что некоторые NFT, собранные со страницы, могут быть без цены (NFTrade отображает все NFT в коллекции, а не только те, которые выставлены на продажу). Рекомендовалось поместить код, который получает nft_price
, в блок if / else
, чтобы, если цена есть, она была собрана и возвращена, если же ее нет, то вернулся бы None и не вызвалась ошибка.
Вот этот код для проверки цены продажи:
# если есть цена, достать ее if nft_bnb_sale_price: nft_price = nft_bnb_sale_price[0].text else: # если значения нет, присвоить None nft_price = None
Тестирование отрефакторенного кода
Когда отрефакторенный метод get_nft_data()
был готов, пришло время протестировать его в моем Python-скрипте.
Напомню, что мой метод __main__
выглядел следующим образом:
if __name__ == '__main__': scraper = ForSaleNFTScraper(); cards = scraper.get_cards(max_card_count=200) card_data = [] for card in cards: info = (scraper.get_nft_data(card)) card_data.append(info) # вывод данных карточки, для проверки, что получаем нужные данные для каждой pprint(card_data)
На этот раз, когда скрипт был запущен из командной строки с помощью команды python for_sale_scraper.py
, получился следующий вывод:
Как видно из скриншота, я получила массив элементов, и каждый элемент в массиве отличался id
и nft_price
. Теперь метод get_nft_data()
работал корректно, находя следующий NFT в массиве card_data
с каждой последующей итерацией цикла и извлекая данные для каждой соответствующей карточки.
Успех!
Преодолев эту проблему, я была готова перейти к следующим этапам: конвертации цен NFT в BNB в USD по текущему курсу и их сбору в таблицу CSV.
Заключение
Когда мне потребовалось составить таблицу всех NFT, выставленных на продажу в определенной коллекции на NFTrade, я воспользовалась Python для создания парсера и при этом изучила множество новых методов решения проблем.
Мне удалось загрузить и собрать все данные NFT с веб-страницы с помощью Selenium Python, но я сильно застряла при работе с собранными данными, пытаясь извлечь из них ID и цену для каждого NFT.
К счастью, сообщество Stack Overflow помогло и рассказало, как ограничить поиск XPath в Selenium WebDriver конкретным элементом страницы, вместо поиска по всей странице.