vk.com — Сохранение аудиозаписей, документов, содержимого стены

от автора

Я уже давно заметил, что данные в социальных сетях хранятся плохо. Например, сделанный вами репост окажется пустым, если автор оригинальной записи ее удалит. Недавние проблемы с аудиозаписями в vk стали последней каплей, и я решил сохранить локально все данные, которые могут представлять интерес на случай ядерной войны. Поискав готовые решения, я не нашел ничего, что бы устроило меня, поэтому за несколько дней был написан скрипт на Python.

Цели

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

За дело!

Процесс создания подобного приложения уже не раз описан на хабре, поэтому повторять все подробности не стану, опишу шаги работы вкратце, а еще скажу пару слов о пролемах. Чтобы статья не была перегружена исходниками, в конце будет ссылка на github.

Соображения по ходу разработки

  • Прежде всего, потребуется завести себе id приложения. Важно, чтобы тип был standalone, иначе некоторые методы vk api будут недоступны.
  • Еще нужен id пользователя, данные которого будем сохранять. Свой найти можно на странице настроек
  • Чтобы приложение работало, нужно разрешение пользователя, а точнее, access token. Прямого неинтерактивного способа получить токен нет, можно парсить страницу авторизации, но проще — попросить пользователя нажать на кнопку в браузере и скопировать url. За это отвечает функция auth():
        url = "https://oauth.vk.com/oauth/authorize?" + \           "redirect_uri=https://oauth.vk.com/blank.html&response_type=token&" + \           "client_id=%s&scope=%s&display=wap" % (args.app_id, ",".join(args.access_rights))      print("Please open this url:\n\n\t{}\n".format(url))     raw_url = raw_input("Grant access to your acc and copy resulting URL here: ")     res = re.search('access_token=([0-9A-Fa-f]+)', raw_url, re.I) 

  • У запросов vk api есть ограничение: не более пяти в секунду. Если обращаться к серверу слишком часто, он ответит ошибкой. Это достаточно удобно: по коду ошибки можно понять, что скрипт работает слишком быстро, подождать какое-то время и повторить запрос.
            if result[u'error'][u'error_code'] == 6:  # too many requests                 logging.debug("Too many requests per second, sleeping..")                 sleep(1)                 continue 

  • Периодически сервер vk требует решить каптчу, подозревая, что клиент — бот. В общем-то, правильно подозревает. Чтобы процесс сохранения не прерывался, приходится просить пользователя перейти по ссылке на картинку, разгадать каптчу и вбить ответ. Это вынесено в функцию с незамысловатым именем captcha():
        print("They want you to solve CAPTCHA. Please open this URL, and type here a captcha solution:")     print("\n\t{}\n".format(data[u'error'][u'captcha_img']))     solution = raw_input("Solution = ").strip()     return data[u'error'][u'captcha_sid'], solution 

  • Ссылки, дополнительную информацию вроде количества лайков и ответы сервера в JSON будем писать в файлы, на всякий случай.
  • К некоторым аудиозаписям приложен текст песни, что тоже имеет смысл сохранять.
  • Имена файлов могут быть некорректны для файловой системы, поэтому приходится избавляться от некоторых символов. Готового «правильного» решения я не нашел, поэтому пришлось изобрести мини-велосипед:
    result =  unicode(re.sub('[^+=\-()$!#%&,.\w\s]', '_', name, flags=re.UNICODE).strip()) 

  • Еще одна проблема с именами файлов: могут совпадать, например в случае с документами. Для этого к имени файла добавим (n), где n — первое число, дающее уникальное имя файла.
            #file might exist, so add (1) or (2) etc         counter = 1         if exists(fname) and isfile(fname):             name, ext = splitext(fname)             fname = name + " ({})".format(counter) + ext         while exists(fname) and isfile(fname):             counter += 1             name, ext = splitext(fname)             fname = name[:-4] + " ({})".format(counter) + ext 

Продолжим

Код обращения к api взят из статьи хабраюзера dzhioev, и добавлена обработка ситуаций, описанных выше. Чтобы было, что сохранять (в случае с обработкой стены), надо сначала узнать количество постов:

        #determine posts count         (response, json_stuff) = call_api("wall.get", [("owner_id", args.id), ("count", 1), ("offset", 0)], args)         count = response[0] 

Дальше запрашиваем каждый пост по отдельности и разбираем его

        for x in xrange(args.wall_start, args.wall_end):             (post, json_stuff) = call_api("wall.get", [("owner_id", args.id), ("count", 1), ("offset", x)], args)             process_post(("wall post", x), post, post_parser, json_stuff) 

Результат запроса — это набор данных в JSON, которые разбираются в стандартные для python’а структуры с помощью json.loads() из стандартной библиотеки. В итоге, имеем хэш-массив, в котором некоторые поля (ключ-значение) несут полезную нагрузку, а остальные нас не интересуют. Чтобы руками не писать, какое поле каким методом обрабатывать, воспользуемся мощью рефлексии: будем искать метод, имя которого совпадает с интересующим ключом.

        for k in raw_data.keys():             try:                 f = getattr(self, k)                 keys.append(k)                 funcs.append(f)             except AttributeError:                 logging.warning("Not implemented: {}".format(k))         logging.info("Saving: {} for {}".format(', '.join(keys), raw_data['id']))          for (f, k) in zip(funcs, keys):             f(k, raw_data) 
Парсим

Теперь нужно разбираться с полями ответа. Интересные — это attachments, text, comments. Attachments — это список приложений к посту (аудио, картинки, документы, заметки), надо уметь скачивать каждый тип. Определяемся, каким методом обрабатывать каждый attachment, аналогичным способом: по типу аттача ищем метод с подходящим именем. Вот пример «качалки» для аудио:

    def dl_audio(self, data):         aid = data["aid"]         owner = data["owner_id"]         request = "{}_{}".format(owner, aid)         (audio_data, json_stuff) = call_api("audio.getById", [("audios", request), ], self.args)         try:             data = audio_data[0]             name = u"{artist} - {title}.mp3".format(**data)             self.save_url(data["url"], name)         except IndexError: # deleted :(             logging.warning("Deleted track: {}".format(str(data)))             return          # store lyrics if any         try:             lid = data["lyrics_id"]         except KeyError:             return         (lyrics_data, json_stuff) = call_api("audio.getLyrics", [("lyrics_id", lid), ], self.args)         text = lyrics_data["text"].encode('utf-8')         ... 

К сожалению, изъятые по просьбе правообладателей аудиозаписи больше не доступны, для них возвращается пустой ответ.

А остальное?

Методы обработки картинок, текста, заметок, закачки документов и остальное — в github. Скажу только, что все аналогично приведенным примерам. Еще скрипт имеет аргументы командной строки, их описывать в статье смысла нет. Примеры и прочие подробности — в readme.

TODO

Я не стал делать сохранение фотоальбомов, потому что у меня там ничего важного не хранится, да и код kilonet из его статьи неплохо работает. Еще не сохраняются видеозаписи и заметки, мне это показалось не сильно нужным.

На последок

Код далек от идеала и не отличается отсутствием костылей, но выполняет поставленную задачу. Надеюсь, кому-то пригодится моя поделка, для сохранения своих записей/документов/музыки, или для обучения.

ссылка на оригинал статьи http://habrahabr.ru/post/184224/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *