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

в 19:41, , рубрики: python, vk.com, vk.com api, велосипед, Вконтакте, Вконтакте API, метки: , , ,

Я уже давно заметил, что данные в социальных сетях хранятся плохо. Например, сделанный вами репост окажется пустым, если автор оригинальной записи ее удалит. Недавние проблемы с аудиозаписями в 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:nnt{}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("nt{}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('[^+=-()$!#%&,.ws]', '_', 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 из его статьи неплохо работает. Еще не сохраняются видеозаписи и заметки, мне это показалось не сильно нужным.

На последок

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

Автор: Rast1234

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js