Качаем музыку гигами

КДПВ - патефон

Вспомнил, что в начале 2000-х качал музыку с http://music.lib.ru. Сайт всегда радовал редкими и интересными композициями. Недавно совершенно случайно вспомнил о сайте, зашел, и о чудо! Он до сих пор работает и с тех пор ничуть не изменился! Ко мне пришла мысль, почему бы не скачать немного музыки и послушать. Чтобы не качать песни по одной, я решил автоматизировать эту задачу и заодно написать для вас этот туториал.

Нам понадобятся библиотеки.

  • requests для HTTP запросов, включая скачивание файлов
  • BeautifulSoup (bs4) для парсинга HTML на предмет ссылок на музыку.
pip install requests bs4

Встроенные модули:

  • multiprocessing — для распараллеливания загрузок
  • urllib.parse — для парcинга URL (достать имя файла)
  • functools — для декоратора retry
  • time — чтобы поспать
  • os — для работы с путями ОС
import requests
from urllib.parse import urlparse
from bs4 import BeautifulSoup

from functools import wraps
import time
import os

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

# страницы 1 по 15
def page_generator():
    for i in range(1, 16):
        yield f'http://music.lib.ru/janr/index_janr_23-{i}.shtml'
list(page_generator())

Out[5]:
['http://music.lib.ru/janr/index_janr_23-1.shtml',  'http://music.lib.ru/janr/index_janr_23-2.shtml',  'http://music.lib.ru/janr/index_janr_23-3.shtml',  'http://music.lib.ru/janr/index_janr_23-4.shtml',  'http://music.lib.ru/janr/index_janr_23-5.shtml',  'http://music.lib.ru/janr/index_janr_23-6.shtml',  'http://music.lib.ru/janr/index_janr_23-7.shtml',  'http://music.lib.ru/janr/index_janr_23-8.shtml',  'http://music.lib.ru/janr/index_janr_23-9.shtml',  'http://music.lib.ru/janr/index_janr_23-10.shtml',  'http://music.lib.ru/janr/index_janr_23-11.shtml',  'http://music.lib.ru/janr/index_janr_23-12.shtml',  'http://music.lib.ru/janr/index_janr_23-13.shtml',  'http://music.lib.ru/janr/index_janr_23-14.shtml',  'http://music.lib.ru/janr/index_janr_23-15.shtml']

Куда будем качать:

DOWNLOAD_TO = os.path.expanduser('~/Downloads/music_lib_ru_test')

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

# декоратор, который делает несколько попыток выполнить функцию
def retry(tries=4, delay=3):
    def deco_retry(f):
        @wraps(f)
        def f_retry(*args, **kwargs):
            for _ in range(tries):
                try:
                    return f(*args, **kwargs)
                except:
                    time.sleep(delay)
            return f(*args, **kwargs)
        return f_retry
    return deco_retry

Эта функция определяет, является ли URL ссылкой на музыку (по расширению):

def is_music(href: str):
    href = href.lower()
    for ext in ['.mp3', '.wma', '.ogg']:
        if href.endswith(ext):
            return True
    return False

BeautifulSoup помогает нам парсить HTML страниц каталога.

def all_links(html):
    # создаем парсер
    soup = BeautifulSoup(html, 'html.parser')
    # находим все тэги <a>
    all_a = soup.find_all('a')
    # у каждого тэга получаем атрибут href (адрес ссылки) и выплевываем их из генератора
    yield from map(lambda el: el.get('href'), all_a)

Эта функция качает одну страницу из каталога, парсит, достает все ссылки (all_links), фильтрует из них музыкальные (is_music):

@retry(5)
def do_page(page_url):
    print(f'Downloading list: {page_url}')
    html = requests.get(page_url).text 
    links = all_links(html)
    music_links = filter(is_music, links)
    return list(music_links)

Также нужна функция flatten, чтобы из списка списков ссылок составить общий одно-уровневый список.

def flatten(l):
    return [item for sublist in l for item in sublist]

Скачаем каталог ссылок:

from multiprocessing import Pool

# пул из 4-х процессов для параллельного скачивания
pool = Pool(4)

# к каждой URL из page_generator мы применим ф-цию do_page
mp3_urls = pool.map(do_page, page_generator())

# расплющим список
mp3_urls = flatten(mp3_urls)

# выведем немного:
print(mp3_urls[:10])
total = len(mp3_urls)
print('total:', total)
Downloading list: http://music.lib.ru/janr/index_janr_23-2.shtml
Downloading list: http://music.lib.ru/janr/index_janr_23-3.shtml
Downloading list: http://music.lib.ru/janr/index_janr_23-1.shtml
Downloading list: http://music.lib.ru/janr/index_janr_23-4.shtml
....
['http://mp3.music.lib.ru/mp3/s/shergin_b_w/shergin_b_w-diwnyj_gudochek-1.mp3', 'http://mp3.music.lib.ru/mp3/s/shergin_b_w/shergin_b_w-diwnyj_gudochek-2.mp3', 'http://mp3.music.lib.ru/mp3/k/kalinin_a/kalinin_a-....]
total: 3704

Каждую песню нужно теперь скачать. Для одной скачивания одной песни напишем функцию:

@retry(5)
def download_music_piece(url, i, total, download_to):
    """
    url - ссылка
    i - номер песни (для отображения прогресса)
    total - всего песен
    download_to - каталог, куда сохранить
    """
    print(f'[{i:6} / {total:6}] {url}')

    # парсим URL, чтобы достать оригинальное имя файла
    original_path = urlparse(url).path
    original_path = original_path[1:]  # убираем передний слэш / (это важно!)

    # чисто имя файла
    filename = os.path.basename(original_path)
    
    # будущий локальный каталог файла
    dirname = os.path.join(download_to, os.path.dirname(original_path))
    
    # полное локальное имя файла
    full_local_path = os.path.join(dirname, filename)

    # проверка, не скачали ли мы уже этот файл ранее (если да, пропускаем)
    if not os.path.isfile(full_local_path):
        # создадим каталог для него, если еще нет
        os.makedirs(dirname, exist_ok=True)

        # открываем локлаьный файл для записи бинарно
        with open(full_local_path, 'wb') as f:
            # делаем запрос на скачивание
            r = requests.get(url)
            if r.status_code == 200:
                # если ответ ОК (200), то все кусочки пишем в файл
                for c in r:
                    f.write(c)

Теперь для каждого файла мы его скачаем также в мульти-процессном пуле для ускорения. Но для начала мы снабдим каждую запись в mp3_urls номером песни, общим числом песен и каталогом для скачивания, потому что download_music_piece принимает как раз эти 4 аргумента, а не лишь одну ссылку.

mp3_urls = [(url, i, total, DOWNLOAD_TO) for i, url in enumerate(mp3_urls, start=1)]
mp3_urls[:5]
[('http://mp3.music.lib.ru/mp3/s/shergin_b_w/shergin_b_w-diwnyj_gudochek-1.mp3',
  1,
  3704,
  '/Users/tirinox/Downloads/music_lib_ru_test'),
 ('http://mp3.music.lib.ru/mp3/s/shergin_b_w/shergin_b_w-diwnyj_gudochek-2.mp3',
  2,
  3704,
  '/Users/tirinox/Downloads/music_lib_ru_test'),
 ('http://mp3.music.lib.ru/mp3/k/kalinin_a/kalinin_a-matushke_moej-2.mp3',
  3,
  3704,
  '/Users/tirinox/Downloads/music_lib_ru_test'),
 ('http://mp3.music.lib.ru/mp3/p/papisowa_a/papisowa_a-planxty_capitan_okane-2.mp3',
  4,
  3704,
  '/Users/tirinox/Downloads/music_lib_ru_test'),
 ('http://mp3.music.lib.ru/mp3/a/anonymous/anonymous-radio_radonezh-2.mp3',
  5,
  3704,
  '/Users/tirinox/Downloads/music_lib_ru_test')]

pool.starmap — передаст содержимое кортежа в аргументы ф-ции download_music_piece(url, i, total, download_to):

# после запуска - файлы начнут качаться:
pool.starmap(download_music_piece, mp3_urls)

Музыка начала качаться, нужно немного подождать.

Полный код ноутбука здесь. Думаю, вы легко сможете адаптировать скрипт к скачиванию любых других материалов с разных сайтов.

P.S. Спасибо авторам и редакторам сайта, и, конечно же, певцам и музыкантам.

🧙 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Добавить комментарий