Skip to content

Instantly share code, notes, and snippets.

@tirinox
Last active December 16, 2020 00:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tirinox/e53943764f4e445e31b0a940c47560a9 to your computer and use it in GitHub Desktop.
Save tirinox/e53943764f4e445e31b0a940c47560a9 to your computer and use it in GitHub Desktop.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Качаем музыку с music.lib.ru\n",
"Вспомнил, что в начале 2000-х качал музыку с http://music.lib.ru. Сайт всегда радовал редкими и интересными композициями. Недавно совершенно случайно вспомнил вновь о сайте, зашел, и о чудо! Он до сих пор работает и с тех пор ничуть не изменился!\n",
"Ко мне пришла мысль, почему бы не скачать немного музыки и послушать. Чтобы не качать все песни по одной, я решил автоматизировать эту задачу и заодно написать для вас этот туториал."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Нам понадобоятся библиотеки.\n",
"- requests для HTTP запросов, включая скачивание файлов\n",
"- BeautifulSoup (bs4) для парсинга HTML на предмет ссылок на музыку.\n",
"\n",
"```pip install requests bs4```\n",
"\n",
"Встроенные модули:\n",
"- multiprocessing - для распараллеливания загрузок\n",
"- urllib.parse - для парзинга URL (достать имя файла)\n",
"- functools - для декоратора retry\n",
"- time - чтобы поспать\n",
"- os - для работы с путями ОС"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import requests\n",
"from urllib.parse import urlparse\n",
"from bs4 import BeautifulSoup\n",
"\n",
"from functools import wraps\n",
"import time\n",
"import os"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Я люблю фолк, поэтому в примере качать будем его. Вы, ествественно, можете выбрать любой жанр или исполнителя на ваш вкус. \n",
"Нужно просто сделать генератор, который будет выдавать URL страниц, где будут искаться ссылки на музыку."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"# страницы 1 по 15\n",
"def page_generator():\n",
" for i in range(1, 16):\n",
" yield f'http://music.lib.ru/janr/index_janr_23-{i}.shtml'"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['http://music.lib.ru/janr/index_janr_23-1.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-2.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-3.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-4.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-5.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-6.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-7.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-8.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-9.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-10.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-11.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-12.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-13.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-14.shtml',\n",
" 'http://music.lib.ru/janr/index_janr_23-15.shtml']"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"list(page_generator())"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Куда будем качать:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"DOWNLOAD_TO = os.path.expanduser('~/Downloads/music_lib_ru_test')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"При скачивании могут быть ошибки, поэтому этот декоратор будет полезен. Если декорируемая ф-ция бросила исключение, что он совершит еще попытки через несколько секунд:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [],
"source": [
"# декоратор, который делает несколько попыток выполнить функцию\n",
"def retry(tries=4, delay=3):\n",
" def deco_retry(f):\n",
" @wraps(f)\n",
" def f_retry(*args, **kwargs):\n",
" for _ in range(tries):\n",
" try:\n",
" return f(*args, **kwargs)\n",
" except:\n",
" time.sleep(delay)\n",
" return f(*args, **kwargs)\n",
" return f_retry\n",
" return deco_retry"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Эта функция определяет, является ли URL ссылкой на музыку (по расширению):"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [],
"source": [
"def is_music(href: str):\n",
" href = href.lower()\n",
" for ext in ['.mp3', '.wma', '.ogg']:\n",
" if href.endswith(ext):\n",
" return True\n",
" return False"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`BeautifulSoup` помогает нам парсить HTML страниц каталога. "
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"def all_links(html):\n",
" # создаем парсер\n",
" soup = BeautifulSoup(html, 'html.parser')\n",
" # находим все тэги <a>\n",
" all_a = soup.find_all('a')\n",
" # у каждого тэга получаем атрибут href (адрес ссылки) и выплевываем их из генертора\n",
" yield from map(lambda el: el.get('href'), all_a)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Качает одну страницу из каталога, парсит, достатет все ссылки (`all_links`), фильтрует из них музыкальные (`is_music`):"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"@retry(5)\n",
"def do_page(page_url):\n",
" print(f'Downloading list: {page_url}')\n",
" \n",
" html = requests.get(page_url).text\n",
" \n",
" links = all_links(html)\n",
" \n",
" music_links = filter(is_music, links)\n",
" \n",
" return list(music_links)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Также нужна ф-ция `flatten`, чтобы из списка списков ссылок составить общий одноуровненый список."
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [],
"source": [
"def flatten(l):\n",
" return [item for sublist in l for item in sublist]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Скачаем каталог ссылок:"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Downloading list: http://music.lib.ru/janr/index_janr_23-2.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-3.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-1.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-4.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-5.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-6.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-7.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-8.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-9.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-10.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-11.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-12.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-13.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-14.shtml\n",
"Downloading list: http://music.lib.ru/janr/index_janr_23-15.shtml\n",
"['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-matushke_moej-2.mp3', 'http://mp3.music.lib.ru/mp3/p/papisowa_a/papisowa_a-planxty_capitan_okane-2.mp3', 'http://mp3.music.lib.ru/mp3/a/anonymous/anonymous-radio_radonezh-2.mp3', 'http://mp3.music.lib.ru/mp3/m/marina_o/marina_o-garmoshka_i_chuzhaya_zhena-2.mp3', 'http://mp3.music.lib.ru/mp3/m/marina_o/marina_o-kukushka-2.mp3', 'http://mp3.music.lib.ru/mp3/m/mater_sva/mater_sva-pesnja_2-2.mp3', 'http://mp3.music.lib.ru/mp3/m/musicway/musicway-pot_zima-2.mp3', 'http://mp3.music.lib.ru/mp3/s/svnstudio/svnstudio-dlja_tebja_ljubowx_-_nektar-2.mp3']\n"
]
}
],
"source": [
"from multiprocessing import Pool\n",
"\n",
"# пул из 4-х процессов для параллельного скачивания\n",
"pool = Pool(4)\n",
"\n",
"# к каждой URL из page_generator мы применим ф-цию do_page\n",
"mp3_urls = pool.map(do_page, page_generator())\n",
"\n",
"# расплющим список\n",
"mp3_urls = flatten(mp3_urls)\n",
"\n",
"# выведем немного:\n",
"print(mp3_urls[:10])"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3704"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"total = len(mp3_urls)\n",
"total"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Каждую песню нужно теперь скачать. Для одной скачивания одной песни напишем ф-цию:"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [],
"source": [
"@retry(5)\n",
"def download_music_piece(url, i, total, download_to):\n",
" \"\"\"\n",
" url - ссылка\n",
" i - номер песни (для отображения прогресса)\n",
" total - всего песен\n",
" download_to - каталог, куда сохранить\n",
" \"\"\"\n",
" print(f'[{i:6} / {total:6}] {url}')\n",
"\n",
" # парсим URL, чтобы достать оригинальное имя файла\n",
" original_path = urlparse(url).path\n",
" original_path = original_path[1:] # убираем передний слэш / (это важно!)\n",
"\n",
" # чисто имя файла\n",
" filename = os.path.basename(original_path)\n",
" \n",
" # будущий локальный каталог файла\n",
" dirname = os.path.join(download_to, os.path.dirname(original_path))\n",
" \n",
" # полное локальное имя файла\n",
" full_local_path = os.path.join(dirname, filename)\n",
"\n",
" # проверка, не скачали ли мы уже этот файл ранее (если да, пропускаем)\n",
" if not os.path.isfile(full_local_path):\n",
" # создадим каталог для него, если еще нет\n",
" os.makedirs(dirname, exist_ok=True)\n",
"\n",
" # открываем локлаьный файл для записи бинарно\n",
" with open(full_local_path, 'wb') as f:\n",
" # делаем запрос на скачивание\n",
" r = requests.get(url)\n",
" if r.status_code == 200:\n",
" # если ответ ОК (200), то все кусочки пишем в файл\n",
" for c in r:\n",
" f.write(c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Теперь для каждого файла мы его скачаем также в мультепроцессном пуле для ускорения. Но для начала мы снабдим каждую запись в `mp3_urls ` номером песни, общим числом песен и каталогом для скачивания, потому что `download_music_piece` принимает как раз эти 4 аргумента, а не лишь одну ссылку."
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[('http://mp3.music.lib.ru/mp3/s/shergin_b_w/shergin_b_w-diwnyj_gudochek-1.mp3',\n",
" 1,\n",
" 3704,\n",
" '/Users/tirinox/Downloads/music_lib_ru_test'),\n",
" ('http://mp3.music.lib.ru/mp3/s/shergin_b_w/shergin_b_w-diwnyj_gudochek-2.mp3',\n",
" 2,\n",
" 3704,\n",
" '/Users/tirinox/Downloads/music_lib_ru_test'),\n",
" ('http://mp3.music.lib.ru/mp3/k/kalinin_a/kalinin_a-matushke_moej-2.mp3',\n",
" 3,\n",
" 3704,\n",
" '/Users/tirinox/Downloads/music_lib_ru_test'),\n",
" ('http://mp3.music.lib.ru/mp3/p/papisowa_a/papisowa_a-planxty_capitan_okane-2.mp3',\n",
" 4,\n",
" 3704,\n",
" '/Users/tirinox/Downloads/music_lib_ru_test'),\n",
" ('http://mp3.music.lib.ru/mp3/a/anonymous/anonymous-radio_radonezh-2.mp3',\n",
" 5,\n",
" 3704,\n",
" '/Users/tirinox/Downloads/music_lib_ru_test')]"
]
},
"execution_count": 23,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"mp3_urls = [(url, i, total, DOWNLOAD_TO) for i, url in enumerate(mp3_urls, start=1)]\n",
"mp3_urls[:5]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"`pool.starmap` - передаст содержимое кортежа в аргументы ф-ции `download_music_piece(url, i, total, download_to)`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# полсе запуска - файлы начнут качаться:\n",
"pool.starmap(download_music_piece, mp3_urls)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.1"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
# полный код
from multiprocessing import Pool
import requests
from urllib.parse import urlparse
from bs4 import BeautifulSoup
from functools import wraps
import time
import os
# генератор страниц, с которых мы будем качать музыку
def page_generator():
for i in range(1, 16):
yield f'http://music.lib.ru/janr/index_janr_23-{i}.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
def is_music(href: str):
href = href.lower()
for ext in ['.mp3', '.wma', '.ogg']:
if href.endswith(ext):
return True
return False
def all_links(html):
soup = BeautifulSoup(html, 'html.parser')
all_a = soup.find_all('a')
yield from map(lambda el: el.get('href'), all_a)
@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)
def flatten(l):
return [item for sublist in l for item in sublist]
@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)
if __name__ == '__main__':
os.makedirs(DOWNLOAD_TO, exist_ok=True)
pool = Pool(4)
mp3_urls = flatten(pool.map(do_page, page_generator()))
total = len(mp3_urls)
mp3_urls = [(url, i, total, DOWNLOAD_TO) for i, url in enumerate(mp3_urls, start=1)]
pool.starmap(download_music_piece, mp3_urls)
print('All done!')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment