Я уже давно заметил, что данные в социальных сетях хранятся плохо. Например, сделанный вами репост окажется пустым, если автор оригинальной записи ее удалит. Недавние проблемы с аудиозаписями в 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