
Вспомнил, что в начале 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 👈