Скачивание музыкальной коллекции vk.com

Привет, хабрахабр!

Решил я как-то скачать свою музыкальную коллекцию из vkontakte (а это без малого 1000 песен). Связываться с vk.api не хотелось, поэтому решил использовать python + библиотека request. Что из этого получилось — под катом!

Сначала посмотрим, что делает наш браузер, когда мы обращаемся к странице своих аудиозаписей вконтакте. Открываем инструменты разработчика (я использовал Chrome, F12) и заходим на vk.com/audio. Мы можем видеть все запросы, которые совершает браузер:

image

Смысл действий браузера таков:

Первая строчка — GET запрос, который мы посылаем к серверу при первом заходе на страницу. В ответе сервер отдает нам html код страницы.
Затем браузер начинает подгружать все необходимые ресурсы: css, js и изображения.
Ближе к концу списка видим нестандартную строчку: это запрос типа POST с именем audio. Скорее всего, этот запрос посылает javascript для получения списка аудиозаписей.
В ответе сервер нам возвращает строчку типа:

11055<!>audio.css,audio.js<!>0<!>6362<!>0<!>{"all":[
  ['17738938','173762121',
    'http://cs1276.userapi.com/u1040081/audio/c0e97293c5e2.mp3','300','5:00',
    'Louis Prima','Sing, Sing, Sing (With A Swing)','369754','0','0','','0','1'],
  ['17738938','173368012',
    'http://cs4372.userapi.com/u9237008/audio/5f51ceac6ca1.mp3','326','5:26',
    'Look at my horse','My horse is amazing','10324035','0','0','','0','1'], ...


Бинго! Это именно то что нам и надо. В ответе сервер возвращает нам JSON-список всех наших композиций и для каждой передает следующие параметры:
  • 0 — мой id
  • 1 — id композиции
  • 2 — ссылку на композицию
  • 3 — битрейт?
  • 4 — длительность
  • 5 — автор
  • 6 — название композиции
  • 7 — размер в байтах?
  • Остальные параметры непонятны.


Получаем список аудиозаписей



Как же нам получить желанный список? Посмотрим, какие headers отправляет браузер в нашем запросе:

Request Headers:
  Accept:*/*
  Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3
  Accept-Encoding:gzip,deflate,sdch
  Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4
  Connection:keep-alive
  Content-Length:45
  Content-Type:application/x-www-form-urlencoded
  Cookie:remixlang=0; remixseenads=2; audio_vol=100; remixdt=0;remixsid=************; remixflash=11.4.31
  Host:vk.com
  Origin:http://vk.com
  Referer:http://vk.com/audio
  User-Agent:Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.94 Safari/537.4
  X-Requested-With:XMLHttpRequest

Form:
  Dataview URL encoded
  act:load_audios_silent
  al:1
  gid:0
  id:17738938



Попробуем сымитировать наш запрос:

import requests as r

def getAudio():
    response = r.post(url = "http://vk.com/audio",
           data = {
               "act":"load_audios_silent",
                "al":"1",
                "gid":"0",
                "id":"17738938"
                }
           )
    print response.content

getAudio()



Функция request.post создает POST запрос к url. Ей можно передать несколько параметров. Вот главные из них:
  • headers — словарь хидеров, которые мы хотим отправиь серверу
  • data — словарь данных, которые надо передать в запросе


Функция нам выведет
<!--11055<!>audio.css,audio.js<!>0<!>6362<!>3<!>230b860567731c4875


Результат предсказуем — ведь мы никак не указали что мы авторизованный пользователь. Для этого надо передать серверу cookies. Исправим немного наш запрос:


import requests as r

def getAudio():
    response = r.post(
                    "http://vk.com/audio",
                    data = {
                        'act':"load_audios_silent",
                        "al":"1",
                        "gid":"0",
                        "id":"17738938"
                            },
                    headers = {
                        "Cookie":"remixlang=0; remixseenads=2; remixdt=0; remixsid=**************; audio_vol=96; remixflash=11.4.31"
                               }
                      )
    print response.content[0:1000]

getAudio()



Теперь получаем то что нужно.

Хорошо. Список мы получили. Теперь надо его отпарсить и скачать каждую песню по отдельности. Я решил не заморачиваться, и просто использовал регулярные выражения:

#-*-coding:cp1251-*-
import requests as r
import re
import random as ran
import os
import urllib as ur

#Разрешенные символы в названиях песен:
ALLOW_SYMBOLS = " qwertyuiopasdfghjklzxcvbnmйцукенгшщзхъэждлорпавыфячсмитьбюЙЦУКЕНГШЩЗХЪЭЖДЛОРПАВЫФЯЧСМИТЬБЮ.,-()"
COOKIE = ""   #Здесь надо указать cookies, которые вы передаете контакту.

def getAllowName(string):
    """Возвращает для каждой строки подстроку, состоящую только из разрешенных символов
    ALLOW_SYMBOLS"""
    s=''
    for x in string.lower():
        if x in ALLOW_SYMBOLS:
            s += x
    return s

def getRandomElement(arr, delete = False):
    """Возвращает рандомный элемент массива arr. Если delete = True, то этот элемент удаляется из массива."""
    index = ran.randrange(0, len(arr), 1)
    value = arr[index]
    if delete:
        arr.remove(value)
    return value

def getAudio():
    """С этой функцией мы уже сталкивались. Только тут она полученную строку
    разбивает на массив элементов"""
    response = r.post(
                    "http://vk.com/audio",
                    data = {
                        'act':"load_audios_silent",
                        "al":"1",
                        "gid":"0",
                        "id":"17738938"
                            },
                    headers = {
                        "Cookie":COOKIE
                               }
                      )
    i=0
    pat = re.compile(r"\[.+?\]")  #соответствует всем подстрокам типа [.*]
    return pat.findall(response.content) #тут и происходит разбиение

already_added = []  #тут будем хранить id композиций, которые уже скачаны.
pat = re.compile(r"\'(.+?)\'")  #паттерн соответствует всем подстрокам вида '.*'

def OneDownload(x):
    """Делает ОДНУ закачку песни, описание которой (типа ['...', '...', '...', ...]) передается аргументом
    """
    global already_added
    try:
        elements = pat.findall(x) #получаем свойства композиции
        id, url, author, name = (elements[1], elements[2], elements[5], elements[6]) 
        #нужные свойства - id, url, author, name
    except:
        return
    if id not in already_added: #если мы не скачивали композицию
        already_added.append(id)    #добавляем ее в скачанные
    file_path = "audio/"+getAllowName(author+" - "+name)+".mp3" #создаем путь, по которому будет храниться эта композиция 
    with open(file_path, "w"):  #создаем пустой файл с указанным путем
        pass
    ur.urlretrieve(url, file_path)  #и производим закачку
    print name, "downloaded"
    
def getFirstNSongs(first=0, last = None):
    """Функция, получает номер первой песни, которую надо скачать и номер последней
    и производит закачку"""
    if not os.path.exists(os.path.join(os.getcwd(), 'audio')):
        #если нет папки audio создаем ее
        os.mkdir('audio')
    songs = getAudio()  #получаем описания песен
    
    #обрезаем массив песен, в соответствии с указанными first и last:
    if last!=None:  
        songs = songs[first:last+1]
    else:
        songs = songs[first:]
        
    for x in songs: #для каждой нужной песни
        OneDownload(x)  #скачиваем ее

getFirstNSongs(last = 10)



Основная функя здесь — OneDownload(). По сути, именно она скачивает песни. Делается это с помощью стандартной функции urllib.urlretrieve(url, file_path, ...). Эта функция скачивает данные, которые возвращает сервер при обращении к url и пишет в файл, который находится на пути file_path.

Все хорошо, все скачивается, но медленно!

Можем попробовать распараллелить наш алгоритм. Функции которые хотелось бы выполнять параллельно — это OneDownload. Создаем декоратор распараллеливания:


def Thread(f):
    def _inside(*a, **k):
        thr = threading.Thread(target = f, args = a, kwargs = k)
        thr.start()
    return _inside



Декоратор в Python — это функция, которая принимает функцию в качестве аргумента и выполняет какие-то действия.
Данный декоратор просто запускает принятую функцию в отдельном потоке.

Добавляем глобальню переменную — число потоков. Напрямую из Thread-ов изменять эту переменную будет нельзя, поэтому добавляем функции

инкремента, и получения:


alive_threads = 0
def inc(x):
    #изменяет переменую
    global alive_threads
    alive_threads+=x
    return alive_threads
def get():
    #возвращает значение
    global alive_threads
    return alive_threads



Теперь вносим изменения в код. Вот конечная версия программы:


#-*-coding:cp1251-*-
import requests as r
import re
import threading
import time
import random as ran
import os
import urllib as ur

THREADS_COUNT = 10
ALLOW_SYMBOLS = " qwertyuiopasdfghjklzxcvbnmйцукенгшщзхъэждлорпавыфячсмитьбюЙЦУКЕНГШЩЗХЪЭЖДЛОРПАВЫФЯЧСМИТЬБЮ.,-()"
COOKIE = "" #Здесь ваш cookies

def getAllowName(string):
    s=''
    print string.lower()
    for x in string.lower():
        if x in ALLOW_SYMBOLS:
            s += x
    return s
        
def getRandomElement(arr, delete = False):
    index = ran.randrange(0, len(arr), 1)
    value = arr[index]
    if delete:
        arr.remove(value)
    return value


alive_threads = 0
def inc(x):
    global alive_threads
    alive_threads+=x
    return alive_threads
def get():
    global alive_threads
    return alive_threads

def Thread(f):
    def _inside(*a, **k):
        thr = threading.Thread(target = f, args = a, kwargs = k)
        thr.start()
    return _inside


def getAudio():
    response = r.post(
                    "http://vk.com/audio",
                    data = {
                        'act':"load_audios_silent",
                        "al":"1",
                        "gid":"0",
                        "id":"17738938"
                            },
                    headers = {
                        "Cookie":COOKIE
                               }
                      )
    i=0
    pat = re.compile(r"\[.+?\]")
    return pat.findall(response.content)


already_added = []  #тут будем хранить id композиций, которые уже скачаны.
pat = re.compile(r"\'(.+?)\'")  #паттерн соответствует всем подстрокам вида '.*'
count = 0

@Thread
def OneDownload(x):
    global already_added
    inc(1)    #когда запустился новый тред - инкрементируем число тредов
    try:
        elements = pat.findall(x)
        id, url, author, name = (elements[1], elements[2], elements[5], elements[6]) 
    except:
        return
    if id not in already_added:
        already_added.append(id)
    file_path = "audio/"+getAllowName(author+" - "+name)+".mp3"
    with open(file_path, "w"):
        pass
    ur.urlretrieve(url, file_path)
    inc(-1)     #тред закончился - декрементируем
    
    

def getFirstNSongs(a=0, N = None):
    if not os.path.exists(os.path.join(os.getcwd(), 'audio')):
        os.mkdir('audio')
    songs = getAudio()
    if N!=None:
        songs = songs[a:N]
    else:
        songs = songs[a:]
    previous = 0    #тут будем хранить число еще не скачанных песен
    cc=10
    while (len(songs)>0 and len(songs)!=previous) or (len(songs) == previous and cc>0):
        #пока число песен непусто, или количество оставшихся песен не изменялось в менее чем 10 циклах
        if previous != len(songs):
            previous = len(songs)   #смотрим, изменилось ли число песен. Если да - присваиваем
            cc=10   #число шагов - 10
        else:
            cc-=1   #если не изменилось, уменьшаем число шагов на 1. Если кол-во песен не изменится за 10 шагов мы выйдем из цикла
        print "Осталось скачать", len(songs), "Число нитей", alive_threads
        while alive_threads<THREADS_COUNT:  #пока можем создавать новые треды
            x = getRandomElement(songs, delete = True)  #получаем рандомную песню, которую надо скачать
            try:
                OneDownload(x)  #пытаемся скачать
            except:
                songs.append(x) #если не получилось - возвращаем назад.
        while alive_threads>=THREADS_COUNT:
            time.sleep(10)  #если не можем добавлять новые треды - спим 10 секунд.
    
         
getFirstNSongs(N=3) #скачиваем, например, первые 3 песни



Теперь все работает.

Исходники и компилированную версию можно скачать по этой ссылке:

VKmusic

#UPD
В компилированной версии был баг, качалась музыка только с моей страницы. Исправленная версия:
VKMusic
Метки:
Поделиться публикацией
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама
Комментарии 44
  • +1
    >3 — битрейт?
    Длительность в секундах.
    • +1
      точно! не заметил, спасибо.
      • +3
        >> Хорошо. Список мы получили. Теперь надо его отпарсить и скачать каждую песню по отдельности. Я решил не заморачиваться, и просто использовал регулярные выражения

        www.youtube.com/watch?v=zFYAR42_DFc#t=0m34s

        А simplejson.loads для кого был сделан? :)

        Сам лично пользуюсь плагином VkOpt для Chrome и DownloadMaster'ом (эта связка еще и с нормальными именами файлов сохраняет).
        • +8
          Не спортивно.
          • 0
            VkOpt и без DM сохраняет с нормальными именами ^^
            • +1
              Просто DM помогает весь плейлист из 100500 песен по 10 файлов одновременно с 8 потоками на файл качать. Разгоняется по-максимуму и от ручной рутины избавляет.

              А VkOpt да, по-уму ребята написали.
      • +1
        Помнится мне я 2 года назад задался той же целью, но сделал все на php, в один поток, однако скрипт умел добавлять в очередь песни, которые не удалось выкачать (недоступен сервер с этой песней или еще чего), затем эта очередь еще пару раз обходилась (за эти проходы еще 60-70% из этих песен выкачивалось), затем скрипт пытался выкачать подобные песни (подобные оставшимся, определялось по продолжительности песни и расстоянию Левенштейна в названии), таким образом выкачивалось абсолютно все :) На тот момент было ~1200 песен в коллекции, качалось около 9 часов :)
        • +1
          Связываться с vk.api не хотелось

          На сколько помню, даже если бы хотелось, не получилось бы.
          Точнее, с помощью VK API можно только получать информацию и управлять аудиозаписями.
          А за статью спасибо, давно хотел скачать все свои скопом.
          • +2
            Можно получать вполне себе конкретную ссылку на аудиозапись, а значит использовать API не просто можно, а нужно. Пруфлинк :)
            • 0
              Сорри за невнимательность.
              • +1
                API линки на аудио отдаёт, но по правилам, качать с помощью АПИ нельзя.
                Там как-то красиво это сказали, мол блокировать будут все приложения, которые предоставляют возможность скачать.
              • +2
                Апи прекрасно отдает линки на аудио.

                — upd:

                тред не обновляй
                @
                комментарий оставляй
                • 0
                  Можно, для личных целей писал «выкачивалку».
                • +2
                  api не в моде?
                  точнее, почему не в моде
                  • +3
                    Потому что изучать инструменты сложнее, чем что-то долбить при помощи молотка и отвертки.
                    • 0
                      И что же тут инструменты? По-моему, нелепо городить огород, когда есть интерфейс, предоставляющий всё в структурированном виде.
                      Автор просто латентный извращенец.
                    • +4
                      Для api надо ведь получать id приложения, а это значит привязывать свой номер и делать дополнительные действия, разве нет? Да и как заметили, не спортивно это)
                    • 0
                      а видео?
                      • 0
                        Абсолютно также, только придется изменить POST запрос, и выбираемые из ответа параметры.
                      • 0
                        Жаль, в скомпилированном варианте нет возможности задать урл самому, чтобы выкачивать коллекции других пользователей.
                        • 0
                          черт, не подумал. Ну это очень просто сделать в исходнике)
                          • 0
                            в getAudio когда совершаем POST запрос, надо вместо id передавать id того пользователя, чью коллекцию Вы хотите скачать.
                          • 0
                            А я обычно через «Тестовое приложение» (https://vk.com/app35569) в execute использую этот код:

                            var audio = API.audio.get();
                            return audio@.url;

                            А дальше поиском/заменой превращаю JSON в обычный список ссылок, который загоняю в какой-нибудь Download Master (либо через wget). Ну а проименовать песни на основе ID3 тегов в любимой форме (некоторые любят в квадратных скобках имя альбома указывать) можно например программой Mp3tag (http://www.mp3tag.de/en/)
                            • +10
                              Велосипеды. Они такие велосипеды.
                              Расскажу свой. Получаем список песен через
                              saveform chrome extension и этот список выкачиваем wget'ом.
                              На все про все — 5 минут.
                              • +1
                                О, почти то же самое делал, тоже через SaveFrom, но не список файлов, а плейлист, т.к. файлы имеют бессмысленные имена. Потом маленьким скриптом в linqPad выкачивал и сохранял с правильным именем.
                              • –3
                                vkontakte.dj/ скачает всю вашу коллекцию, присвоит правильные имена и раскидает по папочкам. Спасибо пожалуйста…
                                • +1
                                  И соберет и заботливо сохранит пароли пользователей :) Отличная программа! (для спамеров)
                                  • –1
                                    Заводишь фейк и используешь его, делов то…
                                    • 0
                                      Регистрируешь фейк на фейковый телефон, зерегистрированный на фейковый паспорт.
                                • –4
                                  VKMusiс выкачивает в несколько потоков с нормальными именами и с любой заданной страницы.
                                  Правда на этот софт иногда реагируют антивирусы.
                                  • 0
                                    Однажды попросили скачать аудио-коллекцию с Одноклассников для прослушивания в авто. На компьютере уже стоял перехватчик медиа трафика, работающий по принципу «Все что попадается — все качаю», но тыкать кнопку Далее около тысячи раз не хотелось, поэтому в консоли отладчика браузера задал нажимать требуемую кнопку с определенным интервалом, далее уже вопрос времени, при этом закачка шла многопоточно. Основной недостаток такого подхода это лишенные смысла имена получившихся файлов.
                                    • +2
                                      Есть еще реализация на node.js+coffeescript. Но с небольшими отличиями:
                                      — воспользоваться может кто угодно, кто может поставить node.js, то есть не нужно копаться в коде
                                      — сохраняет всю инфо в локально в sqlite, и прогресс скачивания тоже, если вдруг остановилась загрузка
                                      — показывает прогресс загрузки и гипотизу для времени конца загрузки, в минутах
                                      — в разы меньше по объему кода ,)
                                      • 0
                                        И еще, раскладывает всю музыку по исполнителям в папки и генерит файл плейлиста m3u, в том порядке, который был на vk.com.
                                        Теперь все.
                                      • 0
                                        Поделюсь еще раз ссылкой (уже отвечал в QA) на более дружественное к пользователю приложение на С#.

                                        На английской Windows 7 приложение из топика, если запустить run.bat выдает сообщения в неверной кодировке «Осталось скачать 0 Число нитей 0». Еще мне показалось, что качать в несколько потоков не очень получается, так как получается слишком много пропусков, похоже, что есть ограничения на скачивания, поэтому в своем приложении не стал делать многопоточность.

                                        Не очень понятно назначение приложения в таком виде, если квалификации пользователя хватает, на то, чтобы задать remixsid, то гораздо быстрее расширением браузера он может получить список файлов и скормить его менеджеру закачек.
                                        • 0
                                          Это конечно интересно, но может я чего не понимаю: зачем скачивать с вк, если можно скачать с торрентов в lossless?
                                          • 0
                                            Затем, что это способ быстро слить вашу личную подборку в авто, например.
                                            Либо коллекцию заинтересовавшего пользователя — в плеер, для поиска новых имён и стилей.
                                            • 0
                                              Далеко не всё и не всегда можно скачать с торрентов в lossless.
                                            • 0
                                              У меня, к сожалению, не было вашего упорства.
                                              Поэтому, для личного использования, писал простой скрипт на JS, который получает все ссылки из HTML. На странице /audio, с недавних пор, имеется следующая структура: pastebin.com/MK184cCy

                                              Как можно увидеть, все ссылки легко добываются из скрытых input'ов, а информация о треке достаётся из div'ов под ними. Минусы данного подхода очевидны, но лично для своего пользования этого хватило.
                                              • 0
                                                А как бы отбирать самые лучшие новости и вытаскивать с них хотя бы названия. Вот тут интересны те, которые за 450+
                                                :)
                                                • +1
                                                  Я использую vkopt + download master.
                                                  • 0
                                                    Зря не используете API.
                                                    У меня пару лет назад получилось сделать то же самое несколько десятков строк.
                                                    • +1
                                                      мой велосипед на Haskelle на эту тему, только я парсю html, потому что основная цель скрипта была — научиться работать с html в haskell'e
                                                      • +1
                                                        Мой велосипед на Perl

                                                        sudo cpanm VK::MP3
                                                        export VKMP3_LOGIN=…
                                                        export VKMP3_PASSWORD=…
                                                        export VKMP3_SAVE_DIR=…
                                                        vkmp3 --sync
                                                        • 0
                                                          Подскажите, как обработать в коде Питона ошибку 201. Access denied: Access to users audio is denied
                                                          Она возникает, когда доступ к списку аудио юзера ограничен.

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