xpath в selenium

Как найти XPath объектов в Selenium при помощи Python

Перевод статьи «Use Selenium with Python to Target the XPath of a Particular Object».

Содержание

Введение

В начале этого года мне нужно было написать программу для друга, которая бы собирала данные одной из коллекций 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, поэтому мои примеры кода могут быть не самыми эффективными или изящными, но они выполняет свою задачу.

Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.

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() должен делать следующее:

  1. Использовать XPath внутри каждой карточки, чтобы получить имя NFT через get_attribute("innerHTML") (innerHTML указывает на текстовое содержимое элемента, которое включает его ID-значение в конце имени). Далее метод partition разделяет на 3 части строки, содержащие в названиии #, и забирает последнюю из них. Эта часть является значением ID. Последний элемент трех частей находится через [-1].
  2. Получить цену NFT (тоже используя get_attribute("innerHTML")) через второй XPath в карточке NFT.
  3. И, наконец, возвращать эти два значения вместе в виде нового объекта с ключами 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
          }

В этой версии кода есть три основных отличия.

  1. Вместо использования self.driver.find_element в этом коде используется nft_data.find_element. Подстановка nft_data вместо self.driver позволяет ограничить поиск XPath конкретным элементом.
  2. Внутри каждого метода find_element, ссылающегося на By.XPATH, в начале XPath есть (.). Таким образом, '//div[contains(@class, "Item_itemName__ckoHR")]' поменялся на ‘.//div[contains(@class, "Item_itemName__ckoHR")]'.
  3. Наконец, мне предложили использовать .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 и цену.

Как видно из скриншота, я получила массив элементов, и каждый элемент в массиве отличался 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 конкретным элементом страницы, вместо поиска по всей странице.

1 комментарий к “Как найти XPath объектов в Selenium при помощи Python”

  1. Пингбэк: 110 вопросов на собеседовании по Selenium

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *