Недавно меня попросили помочь с проектом, который включал в себя загрузку массово текстов песен определенного исполнителя. Для этого мы использовали пакет Python LyricsGenius, который является оболочкой для API Genius.com. Сама библиотека была довольно проста в использовании, но в конце нам пришлось проделать дополнительную работу, чтобы сделать ее пригодной для использования в этом проекте.

Вот ссылка на официальные документы библиотеки Lyricsgenius, если вы хотите их увидеть:
https://pypi.org/project/lyricsgenius/

Естественно, вам понадобится уже установленный на вашем компьютере Python3.

С этим не по пути, давайте перейдем к этому.

Учетные данные API Genius.com

Вам понадобится ключ API, который вы можете получить здесь:



Вы можете перейти по ссылке выше, чтобы создать некоторые учетные данные API для Genius.com. Нажмите Новый клиент API на левой боковой панели. В поле URL-адрес веб-сайта приложения вы можете ввести URL-адрес любого сайта, размещенного на страницах GitHub, или вы можете использовать https://es.python.org/ в качестве заполнителя. На самом деле это не имеет значения. Вот как выглядит мой.

Нажмите «Сохранить», и вы должны увидеть свой API-клиент на новой странице. Это будет выглядеть так:

Если вы нажмете «Создать токен доступа», вы увидите свой новый токен доступа, и вам нужно будет использовать его в своем коде всего за несколько минут.

Установка

Создайте виртуальную среду (или не делайте этого, если вы ненавидите организованность и эффективность), а затем установите пакет с помощью приведенной ниже команды.

pip install lyricsgenius

Основное использование

from lyricsgenius import Genius
genius = Genius(YOUR_API_KEY)

Сначала вам нужно будет импортировать класс Genius из только что установленного пакета Lyricgenius. Затем вам нужно будет создать экземпляр класса Genius и передать свой токен клиентского доступа, который вы только что создали выше. Теперь вы можете использовать это, чтобы получить песни для исполнителя.

artist = genius.search_artist("Drake", sort="title")

Вызывая search_artist, мы делаем вызов API для получения информации об указанном исполнителе. Вы можете заменить «Drake» на любого исполнителя, доступного через Genius API. При желании вы можете указать аргумент max_songs, который ограничит количество извлекаемых песен. Аргумент sort, очевидно, сортирует все извлеченные песни по названию.

for x in artist.songs:
    title = x.title
    with open(f'{title}.txt', 'w') as f:
        f.write(x.lyrics)

Под капотом пакет Lyricgenius на самом деле представляет собой веб-скрейпер, который будет просматривать каждую песню, полученную вашим исполнителем, очищать тексты песен и сохранять их. Итак, теперь мы можем перебрать песни и для каждой песни мы запишем на наш жесткий диск новый текстовый файл, содержащий текст этой песни и названный в соответствии с названием песни.

Легкий! Верно?

Хорошо. Мы довольно близки, но еще не совсем там.

for x in artist.songs:
    title = x.title.replace(" ", '_').replace('/', '')
    if x.title != '-':
        with open(f'{title}.txt', 'w') as f:
            f.write(x.lyrics.replace("See Drake LiveGet tickets as low as $153You might also like13Embed", "").replace(f'{x.title} Lyrics', ""))

Это будет немного ближе к тому, что нам нужно, и, вероятно, будет работать нормально, если бы мы получали только несколько песен. Многие названия песен могут содержать символы, которые мы не хотим использовать в именах файлов. Эти символы вызовут ошибки при записи их на диск, поэтому мы можем использовать replace() для обработки этих исключений. Также могут быть артефакты процесса веб-скрейпинга, которые могут возвращать нежелательные результаты. В этом примере у нас было название «песни» с именем «-», но на самом деле это был просто дополнительный символ, извлеченный во время веб-скрейпинга.

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

Итак, мы закончили? Ну, может быть.

Проблемы

Проблема, с которой мы столкнулись сейчас, заключается в том, что библиотека Lyricgenius будет вызывать API для каждой найденной песни и будет посещать страницу для каждой песни, чтобы очистить текст. Если какой-либо из этих запросов завершится ошибкой, наша программа выдаст ошибку и перестанет работать. Это проблематично, потому что текст каждой песни не будет записан в файлы на нашем жестком диске, пока не будут найдены данные для каждой песни.

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

Изменение пакета LyricsGenius

Мы хотим найти файл с именем genius.py где-нибудь на нашем компьютере, возможно, в папке с именем «site-packages». Я использую Debian Linux и использую Anaconda для управления своими виртуальными средами, поэтому моя находится здесь:

/home/tony/anaconda3/envs/lyrics/lib/python3.10/site-packages

Внутри папки site-packages должна быть папка lyricsgenius, в которой будет наш файл genius.py. В зависимости от вашей операционной системы и виртуальных сред ваши файлы могут находиться в другом месте.

Изменение файлаgenius.py

У вас должен быть файл genius.py в аналогичном каталоге где-то на вашем компьютере. Нам нужно будет внести некоторые изменения, чтобы получить желаемое поведение.

Нам нужно прокрутить вниз до строки 577, где должен быть блок кода, который выглядит следующим образом:

# Attempt to add the Song to the Artist
result = artist.add_song(song, verbose=False,
                          include_features=include_features)
if result is not None and self.verbose:
    print('Song {n}: "{t}"'.format(n=artist.num_songs,
                                   t=safe_unicode(song.title)))

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

# Attempt to add the Song to the Artist
result = artist.add_song(song, verbose=False,
                         include_features=include_features)
if result is not None and self.verbose:
    print('Song {n}: "{t}"'.format(n=artist.num_songs,
                                   t=safe_unicode(song.title)))
                
title = song.title.replace(" ", '_').replace('/', '')
with open(f'{title}.txt', 'w') as f:
    f.write(song.lyrics.replace("See Drake LiveGet tickets as low as $153You might also like13Embed", "")
                                .replace(f'{song.title} Lyrics', ""))

В идеальном мире это сработало бы, но пока не работает. Мы столкнемся с ошибками, потому что делаем массу вызовов API и собираем данные с загружаемых страниц. Пакет Lyricgenius обрабатывает эти ошибки, но делает это «питоновским» способом, в результате чего ваша программа останавливается при обнаружении ошибки.

У каждого музыкального исполнителя могут быть сотни песен, поэтому нам нужно убедиться, что если мы столкнемся с ошибкой, вся наша программа не остановится.

Изменение файла base.py

В вашей папке site-packages у вас должна быть еще одна папка с именем api. В этой папке у вас должен быть файл base.py, и нам нужно будет его изменить. Откройте его и прокрутите вниз до строки 69, хорошо, и вы должны увидеть блок кода, который выглядит следующим образом:

# Make the request
response = None
tries = 0
while response is None and tries <= self.retries:
    tries += 1
    try:
        response = self._session.request(method, uri,
                                         timeout=self.timeout,
                                         params=params_,
                                         headers=header,
                                         **kwargs)
        response.raise_for_status()
    except Timeout as e:
        error = "Request timed out:\n{e}".format(e=e)
        if tries > self.retries:
            raise Timeout(error)
    except HTTPError as e:
        error = get_description(e)
        if response.status_code < 500 or tries > self.retries:
            raise HTTPError(response.status_code, error)

    # Enforce rate limiting
    time.sleep(self.sleep_time)

if web:
    return response.text
elif response.status_code == 200:
    res = response.json()
    return res.get("response", res)
elif response.status_code == 204:
    return 204
else:
    raise AssertionError("Response status code was neither 200, nor 204! "
                         "It was {}".format(response.status_code))

Это блок кода, в котором происходит вся обработка ошибок кода, который мы выполняем. Поэтому нам придется изменить его, чтобы мы не «вызывали» исключения, потому что они заставят наш код перестать работать.

Примечание*

Как правило, это не очень хорошая идея. Вызов исключений может предотвратить серьезные ошибки, вызывающие нежелательное поведение нашей программы, например бесконечные циклы и другие вещи. Мы собираемся игнорировать это, потому что кодирование — это весело, и нам не нужно следовать правилам.

Мы собираемся удалить операторы возврата ошибок и убедиться, что наша программа продолжает работать, поэтому наш код теперь должен выглядеть так:

# Make the request
response = None
tries = 0
while response is None and tries <= self.retries:
    tries += 1
    try:
        response = self._session.request(method, uri,
                                         timeout=self.timeout,
                                         params=params_,
                                         headers=header,
                                         **kwargs)
        response.raise_for_status()
    except Timeout as e:
        error = "Request timed out:\n{e}".format(e=e)
        if tries > self.retries:
#Changed code here
            continue
    except HTTPError as e:
        error = get_description(e)
        if response.status_code < 500 or tries > self.retries:
#Changed code here
            continue

    # Enforce rate limiting
    time.sleep(self.sleep_time)

#Added an extra if statement here
if response is not None:
  if web:
      return response.text
  elif response.status_code == 200:
      res = response.json()
      return res.get("response", res)
  elif response.status_code == 204:
  #Changed code below
      pass
  else:
      pass
else:
  pass

Мы можем использовать continue внутри цикла while, чтобы пропустить любые возникающие ошибки. Если бы мы захотели, мы могли бы также включить операторы печати, чтобы мы могли отслеживать ошибки, но я не сделал этого здесь.

Вне цикла while мы можем использовать pass в качестве замены, чтобы пропустить любые ошибки, которые помешали бы нашему коду продолжить работу должным образом.

Первоначально это было все, что я изменил, но я все еще получал ошибки, потому что иногда появлялся объект NoneType, который пытался использовать остальной код. Чтобы помочь с этим, мы добавляем дополнительную проверку, чтобы увидеть, является ли response None. Однако это по-прежнему не работает, поэтому нам придется внести еще одно изменение.

Назад к файлуgenius.py

На данный момент мы все еще сталкиваемся с проблемами NoneType, поэтому нам нужно использовать некоторые передовые методы, чтобы этот код работал правильно.

Первое, что мы сделаем, — поднимем руки вверх и скажем: «Окей, очевидно, Вселенная не хочет, чтобы я заставлял этот код работать».

Затем мы соберёмся с духом, осушим соленые слёзы и начнем заставлять код подчиняться нашей воле.

Мы собираемся изменить вещи очень простым и нерекомендуемым способом, чтобы этот проект мог просто работать и с ним было покончено. Где-то в строке 537 вашего файла genius.py вы должны увидеть такой код:

# Create the Artist object
artist = Artist(self, artist_info)
# Download each song by artist, stored as Song objects in Artist object
page = 1
reached_max_songs = True if max_songs == 0 else False

Сразу же после этого кода мы начнем новую попытку/за исключением того, что обертывает всю остальную часть этой функции. У вас должно получиться что-то вроде этого:

# Create the Artist object
artist = Artist(self, artist_info)
# Download each song by artist, stored as Song objects in Artist object
page = 1
reached_max_songs = True if max_songs == 0 else False
try:
    while not reached_max_songs:
        songs_on_page = self.artist_songs(artist_id=artist_id,
                                        per_page=per_page,
                                        page=page,
                                        sort=sort,
                                        )

        # Loop through each song on page of search results
        for song_info in songs_on_page['songs']:
            # Check if song is valid (e.g. contains lyrics)
            if self.skip_non_songs and not self._result_is_lyrics(song_info):
                valid = False
            else:
                valid = True

            # Reject non-song results (e.g. Linear Notes, Tracklists, etc.)
            if not valid:
                if self.verbose:
                    s = song_info['title']
                    print('"{s}" is not valid. Skipping.'.format(
                        s=safe_unicode(s)))
                continue

            # Create the Song object from lyrics and metadata
            if song_info['lyrics_state'] == 'complete':
                lyrics = self.lyrics(song_url=song_info['url'])
            else:
                lyrics = ""
            if get_full_info:
                new_info = self.song(song_info['id'])['song']
                song_info.update(new_info)
            song = Song(self, song_info, lyrics)

            # Attempt to add the Song to the Artist
            result = artist.add_song(song, verbose=False,
                                    include_features=include_features)
            if result is not None and self.verbose:
                print('Song {n}: "{t}"'.format(n=artist.num_songs,
                                            t=safe_unicode(song.title)))

            # Exit search if the max number of songs has been met
            reached_max_songs = max_songs and artist.num_songs >= max_songs
            if reached_max_songs:
                if self.verbose:
                    print(('\nReached user-specified song limit ({m}).'
                        .format(m=max_songs)))
                break

            title = song.title.replace(" ", '_').replace('/', '')
            if title != '-':
                with open(f'lyrics/{title}.txt', 'w') as f:
                    f.write(song.lyrics.replace("See Drake LiveGet tickets as low as $153You might also like13Embed", "")
                    .replace(f'{song.title} Lyrics', ""))

        # Move on to next page of search results
        page = songs_on_page['next_page']
        if page is None:
            break  # Exit search when last page is reached
except Exception as e:
    pass

Мы просто оборачиваем огромный кусок кода в блок try/except, так что если там произойдет что-то, что не понравится нашей программе, мы просто проигнорируем это.

Все в порядке. Только не говорите людям, что именно так вы программируете в свободное время. Также не стоит писать об этом посты в блоге.

Итак, теперь мы закончили изменять пакет Lyricgenius и можем вернуться и убедиться, что наш код, который мы написали, верен.

Вернуться к нашему файлу лирики.py

Нам нужно будет внести некоторые окончательные изменения в наш код, который мы написали, когда начинали. Мы удалим большую часть кода, потому что мы переместили его в файлы в пакете Lyricgenius. Теперь наш код будет выглядеть просто так:

from lyricsgenius import Genius
genius = Genius('YOUR_API_KEY')

artist = genius.search_artist("Drake", sort="title")

Теперь это будет проходить через каждую песню, найденную для исполнителя, и сохранять текст каждой песни в отдельный файл.

Все, что осталось сделать, это сидеть сложа руки и наслаждаться нашими горячими данными, только что из печи Python.

Последние мысли

  • Вероятно, хорошей идеей будет создать каталог для вашей программы, так как в итоге вы получите сотни текстовых файлов.
  • Что-то вроде этого в идеале было бы асинхронным, потому что требуется много времени, чтобы искать/вычищать слова для каждой песни отдельно. Сделать всю библиотеку асинхронной задним числом было бы очень, очень далеко за рамками этого проекта.
  • Вы захотите проверить выходные текстовые файлы на наличие странного текста, включенного в начало и/или конец файлов. Веб-скрапинг может собирать рекламу и другой нежелательный текст на странице. Использование регулярного выражения, вероятно, будет лучшим выбором для удаления нежелательного текста.
  • Некоторые из вещей, которые мы сделали в этом посте, считаются «плохой практикой», но знаете что? Код работает, и он выполняет свою работу. Иногда для таких неважных вещей, как этот, не имеет значения, суперчистый у вас код или гипероптимизированный. Просто заставь это работать.
  • Если вы не можете заставить свой код работать после того, как прочитали этот пост в блоге, дайте мне знать, и я посоветую вам просто обернуть больше вещей в блок try/except.

Надеюсь, вам понравилось и вы весело провели время. Если да, пожалуйста, не забудьте подписаться на меня и аплодировать этой статье. Следите за новыми проектами по различным темам в будущем.

Спасибо за чтение, и я надеюсь, что у вас есть отличный день :)