В данной статье вы изучите продвинутую технику веб-автоматизации в Python. Мы используем Selenium с браузером без графического интерфейса, экспортируем отобранные данные в CSV файлы и перенесем ваш отобранный код в класс Python.
Original

Оглавление

  1. Мотивация: отслеживаем музыкальные привычки
  2. Установка и Настройка Selenium
  3. Пробный запуск браузера в режиме Headless
  4. Получаем удовольствие от мелодии
  5. Исследуем каталог
  6. Создание класса
  7. Собираем структурированные данные
  8. Что дальше и чему мы научились?

1. Мотивация: отслеживаем музыкальные привычки

Предположим, что вы время от времени слушаете музыку на bandcamp.com или soundcloud и вам хочется вспомнить название песни, которую вы услышали несколько месяцев назад.
Конечно, вы можете покопаться в истории вашего браузера и проверить каждую песню, но это весьма болезненная затея… Все, что вы помните, это то, что вы услышали песню несколько месяцев назад и она в жанре электроника.

«Было бы классно», думаете вы «Если бы у меня запись моей истории прослушиваний. Я мог бы просто взглянуть на электронную музыку, которую я слушал пару месяцев назад и найти эту песню!»

Сегодня мы создадим простой класс Python под названием BandLeader, который подключается к bandcamp.com, стримит музыку из раздела «Найденное» на главной странице, и отслеживает вашу историю прослушиваний.

История прослушиваний будет сохранена на диске в CSV файле. Далее, вы можете в любой момент просматривать CSV файл в вашей любимой программе для работы с таблицами, или даже в Python.

Если у вас есть опыт в веб-парсинга в Python, то вы знакомы с созданием HTTP запросами и использованием API Python для навигации в DOM. Сегодня мы затронем все эти пункты, за одним исключением.

Сегодня вы используете браузер в режиме без графического интерфейса (режим «командной строки») для выполнения запросов HTTP.

Консольный браузер – это обычный веб браузер, который работает без видимого пользовательского интерфейса. Как вы могли догадаться, он может делать больше, чем выполнять запросы: проводить рендер HTML (правда, вы этого не будете видеть), хранить информацию о сессии, даже проводить асинхронные сетевые связи на коде JavaScript.

Если вы хотите автоматизировать современную сеть, консольные браузеры – неотъемлемая часть.

Бесплатный бонус: Скачайте основу проекта Python+Selenium с полным исходным кодом, который вы можете использовать как основу для вашего веб-парсинга в Python и автоматических приложениях.

2. Установка и Настройка Selenium

Первый шаг, перед тем как написать первую строчку кода – это установка Selenium с поддержкой WebDriver для вашего любимого браузера. Далее в статье мы будем работать с Firefox Selenium, но Chrome также будет отлично.

По выше указанным ссылкам имеется полное описание процесса установки драйверов для Selenium.

Возможно, что путь ~/.local/binнаходится в вашем исполнении PATH, вот как вы должны установить Firefox WebDriver, называемый geckodriver, на ОС Linux:

$ wget https://github.com/mozilla/geckodriver/releases/download/v0.19.1/geckodriver-v0.19.1-linux64.tar.gz
$ tar xvfz geckodriver-v0.19.1-linux64.tar.gz
$ mv geckodriver ~/.local/bin

Далее, нужно установить Selenium при помощи pip, или как вам удобнее. Если вы создали виртуальное пространство для этого проекта, просто введите:

$sudo pip install selenium

Время для пробного запуска.

3. Пробный запуск браузера

Чтобы убедиться, что все работает, попробуйте выполнить простой поиск в интернете через DuckDuckGo. Используйте свой предпочитаемый интерпретатор Python и введите:

>>>from selenium.webdriver import Firefox
>>>from selenium.webdriver.firefox.options import Options
>>>opts = Options()
>>>opts.set_headless()
>>>assert opts.headless  # без графического интерфейса.
>>>browser = Firefox(options=opts)
>>>browser.get('https://duckduckgo.com')

Итак, вы только что создали Firefox без графического интерфейса, направленный на https://duckduckgo.com. Вы создали экземпляр «Options» и применили его для активации мода headless в конструкторе Firefox. Это похоже на введение команды firefox – headless в командной строке.

Веб-парсинг на Python: скриншот Duck Duck Go

Теперь, когда страница загружена, вы можете запросить DOM при помощи методов, определенных в вашем созданном объекте браузера.

Но откуда мы знаем, что запрашивать? Лучший способ – это открыть ваш веб браузер и использовать его инструменты разработки для исследования содержимого страницы. Сейчас вы можете получить форму поиска для отправки запроса, чтобы выполнить запрос. Проверив главную страницу DuckDuckGo, вы увидите, что элемент поисковой формы<input> имеет атрибут ID «search_form_input_homepage«. Вот, что вам нужно:

>>>search_form = browser.find_element_by_id('search_form_input_homepage')
>>>search_form.send_keys('real python')
>>>search_form.submit()

Вы нашли поисковую форму, используя метод send_keys для заполнения, затем метод submit для выполнения поиска для «Real Python». Вы можете проверить результат:

>>> results = browser.find_elements_by_class_name('result')
>>> print(results[0].text)

Real Python - Real Python
Get Real Python and get your hands dirty quickly so you spend more time making real applications. Real Python teaches Python and web development from the ground up ...
https://realpython.com

Похоже, все работает. Чтобы избежать появления невидимых экземпляров браузера, нужно закрыть объект браузера перед окончанием сессии в Python:

>>> browser.close()
>>> quit()

4. Получаем удовольствие на мелодию

Вы удостоверились в том, что можете управлять браузером в режиме headless (без головы т.е. без графического интерфейса), используя Python, теперь перейдем к практике.

  1. Вам нужно включать музыку;
  2. Вам нужно искать и просматривать музыку;
  3. Вам нужна информация о проигрываемой музыке.

Для начала, отправимся на https://bandcamp.com и покопаемся в инструментах разработки вашего браузера. Вы увидите большую и яркую кнопку воспроизведения внизу экрана с HTML атрибутом class, который содержит значение «playbutton«. Проверим, как это работает:

Современная Веб-Автоматизация при Помощи Python и Selenium

>>> opts = Option()
>>> opts.set_headless()
>>> browser = Firefox(options=opts)
>>> browser.get('https://bandcamp.com')
>>> browser.find_element_by_class('playbutton').click()

Вы должны слышать музыку! Запустите трек и оставьте его, вернитесь обратно в веб браузер. Рядом с кнопкой воспроизведения находится окно поиска. Еще раз, нужно проверить эту секцию. Вы найдете, что каждый видимый и доступный трек имеет значение класса «discover-item«, а каждый объект кликабельный. В Python, проверка выполняется следующим образом:

>>> tracks = browser.find_elements_by_class_name('discover-item')
>>> len(tracks)  # 8
>>> tracks[3].click()

Следующий трек должен воспроизвестись. Это первый шаг в исследовании bandcamp при помощи Python! Вы тратите несколько минут, переключая разные треки в пространстве Python, но скудная библиотека из восьми песен может быстро надоесть.

5. Изучаем каталог

Вернувшись в свой браузер, вы увидите кнопку для изучения всех треков, связанных с окном поиска bandcamp. Сейчас все должно выглядеть знакомым: каждая кнопка имеет значение класса»item-page«. Последняя кнопка — “далее” показывает следующие восемь композиций в каталоге. Нужно выполнить следующее:

>>> next_button = [e for e in browser.find_elements_by_class_name('item-page')
                   if e.text.lower().find('next') > -1]
>>> next_button.click()

Отлично! Теперь, может вам захочется просмотреть новые треки, и вы подумаете «Я просто перепишу переменные моих треков, так же, как я делал это минуту назад». Но здесь мы с вами столкнемся с хитростями.

Во первых, bandcamp разработали свой сайт так, чтобы пользователям было удобно им пользоваться, а не для скриптов Python для доступа к программному обеспечению. Вызывая next_button.click(), реальный веб браузер отвечает, выполняя какой-нибудь код JavaScript. Если вы попробуете сделать это в своем браузере, то увидите, что некоторое время уходит на эффект анимации каталога песен при прокрутке. Если вы попробуете переписать переменные композиций перед окончанием анимации, то можете получить не все треки, или получить те, которые вам не нужны.

Решение? Вы можете просто заснуть на секунду, или, если вы проделываете работу в оболочке Python, вы, вероятно, даже не заметите этого – в конце концов, на набор данных также уходит время.

Еще один небольшой момент, который можно обнаружить только путем проб и ошибок. Попробуйте запустить такой же код еще раз:

>>> tracks = browser.find_elements_by_class_name('discover-item')
>>> assert(len(tracks) == 8)

Вы обнаружите кое-что странное. len(tracks)не равен 8, даже если пачка состоит из восьми треков. Углубившись, вы обнаружите, что ваш список состоит из треков, которые были показаны ранее. Чтобы получить треки, которые будут видны в браузере, нужно сделать небольшую фильтрацию результатов.

Попробовав несколько вариантов, вы решите оставить трек, только если его координата х на странице попадают в определенные рамки содержимого элемента. Контейнер каталога имеет значение класса «discover-results«. Выполним следующее:

>>> discover_section = self.browser.find_element_by_class_name('discover-results')
>>> left_x = discover_section.location['x']
>>> right_x = left_x + discover_section.size['width']
>>> discover_items = browser.find_element_by_class_name('discover_items')
>>> tracks = [t for t in discover_items
              if t.location['x'] >= left_x and t.location['x'] < right_x]
>>> assert len(tracks) == 8

6. Создание класса

Если вас утомляет переписывать одни и те же команды снова и снова в пространстве Python, вам нужно выгрузить часть из них в модуль. Простейший класс для вашей работы с bandcamp должен выполнять следующие задачи:

  1. Инициализация headless браузера и выполнить направление на bandcamp;
  2. Хранение списка доступных треков;
  3. Поддержка поиска треков;
  4. Воспроизведение, пауза, переключение треков;

Четыре в одном. Вот простой код:

from selenium.webdriver import Firefox
from selenium.webdriver.firefox.options import Options
from time import sleep, ctime
from collections import namedtuple
from threading import Thread
from os.path import isfile
import csv


BANDCAMP_FRONTPAGE='https://bandcamp.com/'

class BandLeader():
    def __init__(self):
        # Инициализация браузера.
        opts = Options()
        opts.set_headless()     
        self.browser = Firefox(options=opts)
        self.browser.get(BANDCAMP_FRONTPAGE)

        # Список треков.
        self._current_track_number = 1
        self.track_list = []
        self.tracks()

    def tracks(self):
        '''
        Запрос страницы для получения списка доступных треков
        '''

        # Режим сна, пока браузер обрабатывает и выполняет все анимации
        sleep(1)

        # Получаем хранилище для списка видимых треков.
        discover_section = self.browser.find_element_by_class_name('discover-results')
        left_x = discover_section.location['x']
        right_x = left_x + discover_section.size['width']

        # Фильтруем объекты в списке, чтобы получить только те, которые мы можем включать.
        discover_items = self.browser.find_elements_by_class_name('discover-item')
        self.track_list = [t for t in discover_items
                           if t.location['x'] >= left_x and t.location['x'] < right_x]

        # Вывод доступных треков на экран.
        for (i,track) in enumerate(self.track_list):
            print('[{}]'.format(i+1))
            lines = track.text.split('\n')
            print('Album  : {}'.format(lines[0]))
            print('Artist : {}'.format(lines[1]))
            if len(lines) > 2:
                print('Genre  : {}'.format(lines[2]))

    def catalogue_pages(self):
        '''
        Вывод доступных в настоящее время страниц в каталоге.
        '''
        print('PAGES')
        for e in self.browser.find_elements_by_class_name('item-page'):
            print(e.text)
        print('')


    def more_tracks(self,page='next'):
        '''
        Продвижение каталога, обновление списка композиций, мы 
        можем передать число для продвижения любых доступных страниц.
        '''

        next_btn = [e for e in self.browser.find_elements_by_class_name('item-page')
                    if e.text.lower().strip() == str(page)]

        if next_btn:
            next_btn[0].click()
            self.tracks()

    def play(self,track=None):
        '''
        Запускаем трек. 
        Если не указан номер трека, запустится выбранная только что композиция
        '''

       if track is None:
            self.browser.find_element_by_class_name('playbutton').click()
       elif type(track) is int and track <= len(self.track_list) and track >= 1:
            self._current_track_number = track
            self.track_list[self._current_track_number - 1].click()


    def play_next(self):
        '''
        Воспроизводится следующий доступный трек
        '''
        if self._current_track_number < len(self.track_list):
            self.play(self._current_track_number+1)
        else:
            self.more_tracks()
            self.play(1)


    def pause(self):
        '''
        Пауза воспроизведения.
        '''
        self.play()

Довольно аккуратно. Вы можете импортировать этот код в ваше пространство Python и запустить bandcamp! Но постойте, разве мы не затеяли это все потому, что нам нужно собирать информацию о вашей истории прослушиваний?

7. Собираем структурированные данные

Наша последняя с вами задача, это отслеживать прослушанные вами треки. Как нам это сделать? Что буквально означает слушать что-либо? Если вы пролистываете каталог, переключая треки спустя пару секунд, это считается за прослушивание песни? Скорее всего, нет. Нам нужно включить фактор длительности прослушивания в сбор данных.

Сейчас нам нужно:

  1. Собрать структурированную информацию о воспроизводимых композициях;
  2. Хранить базу данных треков;
  3. Сохранить и восстановить эту базу данных на диск и из диска.

Используем namedtuple для сортировки отслеживаемой вами информации. Кортежи с названиями хороши для показа связки атрибутов без привязки к ним. Это немного похоже на запись базы данных.

TrackRec = namedtuple('TrackRec', [
    'title', 
    'artist',
    'artist_url', 
    'album',
    'album_url', 
    'timestamp'  # When you played it
])

Чтобы собрать эту информацию, добавьте метод в класс BandLeader. Проверка при помощи инструментов разработки браузера откроет необходимые элементы HTML и атрибуты для выбора всей необходимой информации. Кстати, вам понадобится только информация о недавно прослушанных треках, если в это время музыка воспроизводилась. К счастью, страница плеера добавляет класс «playing» к кнопке воспроизведения, когда музыка играет, и убирает его, когда воспроизведение прекращается. Держа в голове данные нюансы, нужно прописать несколько методов:

def is_playing(self):
    '''
    Выдает True, если трек воспроизводится в данный момент.
    '''
    playbtn = self.browser.find_element_by_class_name('playbutton')
    return playbtn.get_attribute('class').find('playing') > -1


def currently_playing(self):
    '''
    Возвращает запись воспроизводимого в данный момент 
    трека или None, если ничего не воспроизводится.
    '''
    try:
        if self.is_playing():
            title = self.browser.find_element_by_class_name('title').text
            album_detail = self.browser.find_element_by_css_selector('.detail-album > a')
            album_title = album_detail.text
            album_url = album_detail.get_attribute('href').split('?')[0]
            artist_detail = self.browser.find_element_by_css_selector('.detail-artist > a')
            artist = artist_detail.text
            artist_url = artist_detail.get_attribute('href').split('?')[0]
            return TrackRec(title, artist, artist_url, album_title, album_url, ctime())

    except Exception as e:
        print('there was an error: {}'.format(e))

    return None

Для точного измерения, можно также модифицировать метод play для продолжения отслеживания играющего в данный момент трека:

def play(self, track=None):
    '''
    Запуск трека. Если номер композиции не 
    поддерживается, воспроизведется предыдущий трек.
    '''

    if track is None:
        self.browser.find_element_by_class_name('playbutton').click()
    elif type(track) is int and track <= len(self.track_list) and track >= 1:
        self._current_track_number = track
        self.track_list[self._current_track_number - 1].click()

    sleep(0.5)
    if self.is_playing():
        self._current_track_record = self.currently_playing()

Далее, нам нужно создать какую-либо базу данных. Хотя она может плохо масштабироваться при больших объемах, в простом списке все должно пройти хорошо.

Добавляем строку:

self.database = []

В методе инициализации класса BandCamp __init__ . Так как нам нужно дать возможность тому, чтобы прошло определенное время перед входом в объект TrackRec в базе данных, мы используем инструменты threading нашего Python, для запуска отдельных процессов, которые поддерживают базу данных в фоновом режиме.

Мы привяжем метод _maintain() к экземплярам BandLeader, которые запустят раздельный поток. Новый метод будет периодически проверять значение self._current_track_record и добавлять его в базу данных, если это новый трек.

Мы запустим поток, когда класс станет подтвержденным, путем добавления кода в __init__.

# Новый init.
def __init__(self):
    # Инициализация браузера.
    opts = Options()
    opts.set_headless()     
    self.browser = Firefox(options=opts)
    self.browser.get(BANDCAMP_FRONTPAGE)

    # Состояние списка треков.
    self._current_track_number = 1
    self.track_list = []
    self.tracks()

    # Состояние базы данных.
    self.database = []
    self._current_track_record = None

    # Поток поддержки базы данных.
    self.thread = Thread(target=self._maintain)
    self.thread.daemon = True # Закрывает поток вместе с основным процессом.
    self.thread.start()

    self.tracks()


def _maintain(self):
    while True:
        self._update_db()
        sleep(20) # Проверка каждые 20 секунд.


def _update_db(self):
    try:
        check = (self._current_track_record is not None
                 and (len(self.database) == 0
                      or self.database[-1] != self._current_track_record)
                 and self.is_playing())
        if check:
            self.database.append(self._current_track_record)

    except Exception as e:
        print('error while updating the db: {}'.format(e)

Если вы никогда не работали с мультипоточным программированием в Python, вы должны с этим ознакомиться! Для нашей нынешней цели, вы можете представить потоки как цикл, который возвращает фоновый режим основного процесса Python (тот, с которым вы напрямую взаимодействуете). Каждые двадцать секунд цикл выполняет небольшую проверку, чтобы увидеть, нужно ли обновить базу данных. Если нужно, он добавляет новую запись. Вполне себе круто.

Самым последним будет сохранение базы данных и восстановление из сохраненных состояний. Используя модуль csv, вы гарантированно будете хранить свою базу данных в очень портативном формате, кроме этого, базу данных можно будет использовать, даже если вы покинете ваш чудесный класс BandLeader

Метод __init__ снова нужно изменить, на этот раз, для принятия пути файла, в котором вы хотите сохранить базу данных. Вам может понадобиться загрузить эту базу данных, если она доступна, и периодически её сохранять, когда она будет обновлена. Обновления выглядят следующим образом: