Конкурс Python - Dump VK (фото, документы, диалоги+вложения)

Dump VK
Статья для участия в конкурсе Конкурс 2018 года - авторская статья по любой тематике нашего форума!

Всем привет, в этой статье я вас научу делать дамп данных из вк, а вернее напишем инструмент на python для автоматизации данного процесса.
dump_vk.jpg


"Из коробки" доступен дамп:
  • Фото (из всех альбомов в том числе личных)
  • Документы
  • Диалоги
    • сообщения {'from', 'text', 'date'}
    • вложения (photo, docs, audio_message)
Такой набор требований к дампу сформулирован из моих личных желаний(интерес к этим данным), однако, финальная структура проекта, позволяет легко добавить и другие режимы (под ваши требования).

Приступим к созданию проекта. Этапы:
Часть 1 - Разберем на пальцах
  • Подготовка
    • Создание приложения Вконтакте
    • Настройка виртуального окружения
  • Авторизация в приложении
  • Получение данных
  • Выкачивание данных
Часть 2 - Наведение красоты
  • Упаковка наработок в классы
    • + Читаемый размер документов(КБ, МБ, ГБ)
    • + json-коллекции
  • Добавление дамп-менеджера
    • Сбор данных
    • Скачивание данных
    • Обработка ошибок

Часть 1. Разберем на пальцах
Для работы vk_api требуется APP_ID. Вы можете использовать мое приложение, доступ к вашим данным я не получу. Но кратко расскажу, как создать свое приложение вконтакте:
Перейдите по ссылке https://vk.com/apps?act=manage и нажмите на "Создать приложение". Укажите название (любое) и тип standalone. Подтвердите создание с помощью sms. Подтвердив, откроется страница с приложением, нам интересен его id. Копируем id и вставляем в ссылку ниже (на место client_id). Сейчас там ссылка на оф. приложение android.

Перейдите по ссылке:
https://oauth.vk.com/authorize?client_id=2890984&scope=notify,photos,friends,audio,video,notes,pages,docs,status,questions,offers,wall,groups,messages,notifications,stats,ads,offline&redirect_uri=http://api.vk.com/blank.html&display=page&response_type=token
Разрешив доступ к аккаунту, откроется страница OAuth Blank, в адресе которой будет ваш token (access_token=*** до &expires).

Для удобной организации модулей, создадим виртуальное окружение python:
В директории с проектом(пока пустым) в терминале/командной строке введите:
python -m venv env
call env\scripts\activate (windows)
source env\bin\activate (linux)
pip install vk-api requests

Можем приступать к коддингу. Создайте файл run.py и откройте его. Для начала задайте структуру модуля:
Python:
# импорт модуля вк
import vk_api

def main():
    pass

# точка входа в приложение
if __name__ == '__main__':
    main()

Теперь заполняем функцию main - в ней разберемся с основным механизмом нашего будущего инструмента.

Авторизация в приложении.
Python:
def main():
    # версия api и id приложения
    API_VERSION = '5.92'
    APP_ID = 6781880
    # вводим токен от аккаунта вк
    token = ''
    # создаем сессию по токену
    vk_session = vk_api.VkApi(token=token, app_id=APP_ID, api_version=API_VERSION)
    # получаем доступ к апи сессии
    vk = vk_session.get_api()
    # для проверки подключения запоминаем данные профиля
    account = vk.account.getProfileInfo()
    # и выводим данные на экран
    print(account)

JSON:
{'first_name': '', 'last_name': '', 'screen_name': '', 'sex': , 'relation': 0, 'bdate': '', 'bdate_visibility': 1,
'home_town': '', 'country': {'id': 1, 'title': 'Россия'}, 'city': {'id': 1, 'title': ''}, 'status': '', 'phone': ''}
В структуре указаны: имя, фамилия, адрес страницы (короткая ссылка, id), пол, дата рождения, страна, город, статус, телефон.

Для входа по логину-паролю замените строчку "vk_session = ..." на:
Python:
vk_session = vk_api.VkApi(login, password, app_id=APP_ID, api_version=API_VERSION)
vk_session.auth(token_only=True, reauth=True)

Соберем авторизацию вк в одну функцию и добавим обработку 2FA и CAPTCHA:
Python:
import vk_api

API_VERSION = '5.92'
APP_ID = 6781880

# 2FA обработчик
def auth_handler():
    key = input('Введите код двухфакторной аутентификации: ')
    remember_device = True
    return key, remember_device

# captcha обработчик
def captcha_handler(captcha):
    key = input(f"Введите капчу ({captcha.get_url()}): ").strip()
    return captcha.try_again(key)

# авторизация по токену или логин-паролю
def login_vk(token=None, login=None, password=None):
    try:
        # если токен введен
        if token:
            # то создаем сессию с ним
            vk_session = vk_api.VkApi(token=token, app_id=APP_ID, auth_handler=auth_handler, api_version=API_VERSION)
        # иначе если введены login password
        elif login and password:
            # то создаем сессию с этими данными
            vk_session = vk_api.VkApi(login, password, captcha_handler=captcha_handler, app_id=APP_ID,
                                      api_version=API_VERSION, auth_handler=auth_handler)
            # авторизуемся (использовать для пароля-логина)
            vk_session.auth(token_only=True, reauth=True)
        # иначе, если данных недостаточно, то бросаем исключение
        else:
            raise KeyError('Не введен токен или пара логин-пароль')
        # возвращаем апи к сессии
        return vk_session.get_api()
    # в случае ошибки, выводим сообщение на экран
    except Exception as e:
        print(e)

def main():
    # вводим токен
    token = ''
    # логинимся по токену
    vk = login_vk(token)
    # получаем инфо профиля
    account = vk.account.getProfileInfo()
    # выводим на экран профиль
    print(account)

if __name__ == '__main__':
    main()

Получение данных
Для понимания механизмов получения альбомов и документов введем две функции:
Python:
def get_albums(vk):
    albums = vk.photos.getAlbums(need_system=1)['items']
    print(albums)

def get_docs(vk):
    docs = vk.docs.get()
    print(docs)

JSON:
[
    {'id': -6,'thumb_id': num,'owner_id': num,'title': 'Фотографии с моей страницы','size': 3},
    {'id': -7,'thumb_id': num,'owner_id': num,'title': 'Фотографии на моей стене','size': 19},
    {'id': -15,'thumb_id': num,'owner_id': num,'title': 'Сохранённые фотографии','size': 2}
]
Как видим, у данного пользователя 3 альбома, в каждом по size фотографий (всего 24 шт.)

JSON:
{
'count': 344,
'items': [{'id': num, 'owner_id': num, 'title': 'VK.zip', 'size': 4093,'ext': 'zip',
'url': '<url>', 'date': 1540842757, 'type': 2}]
}
В поле count - количество всех документов(элементов 'items') у текущего пользователя, а в поле items - список самих документов(id, кто выложил, когда, с каким именем, размером, расширением)

Для доступа к фотографиям в альбоме, введем в main новую переменную vk_tools:
vk_tools = vk_api.VkTools(vk)

Преобразим функцию получения альбомов:
Python:
def get_albums(vk, vk_tools):
    # результирующий список альбомов
    albums = []
    # обрабатываем каждый альбом
    for album in vk.photos.getAlbums(need_system=1)['items']:
        # получаем список фотографий из альбома
        photos = vk_tools.get_all(values={'album_id': album['id'], 'photo_sizes': 1}, method='photos.get', max_count=1000)
        # добавляем название альбома и ссылки на каждую фотографию
        albums.append({'name': album['title'], 'photos': [p['sizes'][-1]['url'] for p in photos['items']]})
    print(albums)

JSON:
[
  { 'name': 'Фотографии с моей страницы',  'photos': ['https://pp.userapi.com/<path>/<filename>.jpg', '...']},
  { 'name': 'Фотографии на моей стене', 'photos': ['https://pp.userapi.com/<path>/<filename>.jpg', '...']},
  { 'name': 'Сохранённые фотографии', 'photos': ['https://pp.userapi.com/<path>/<filename>.jpg', '...']}
]

Также как и с альбомами, оставим только нужные нам данные для документов:
Python:
def get_docs(vk):
    # получаем список документов
    doc_list = vk.docs.get()
    # задаем новую структуру с количеством документов, общим размером(пока в байтах) и списком документов
    docs = {'count': doc_list['count'], 'size': 0, 'docs': []}
    # для каждого документа
    for doc in doc_list['items']:
        # добавим в список документов url, имя и расширение текущего документа
        docs['docs'].append({'url': doc['url'], 'name': f"{doc['title']}_{doc['id']}", 'ext': doc['ext']})
        # и добавим размер текущего документа к общему
        docs['size'] += doc['size']
    print(docs)

{'count': 344, 'size': 1062120373, 'docs': [{'url': "vk.com/<url>", 'name': 'VK.zip_4789', 'ext': 'zip'}]}

Теперь разберем функции получения диалогов и сообщений. Этот метод сложнее предыдущих и значительно объемнее по коду (ввиду обработки только нужных полей и аттачментов).
В простейшей реализации, все сообщения можно собрать таким способом:
Python:
def get_conversations(vk, vk_tools):
    conversation_list = vk_tools.get_all(method='messages.getConversations', max_count=100,
                                         values={'extended': 1, 'fields': 'first_name, last_name, name'})
    conversations = []
    for conversation in conversation_list['items']:
        messages = vk_tools.get_all(max_count=200, method='messages.getHistory',
                               values={'rev': 1, 'extended': 1, 'fields': 'first_name, last_name',
                                       'peer_id': conversation['conversation']['peer']['id']})
        conversations.append({'id': conversation['conversation']['peer']['id'], 'messages': messages})
    return conversations

Метод собирает все сообщения, однако, в этой структуре много лишней информации. Если выделить только id, имя диалога, сообщения и вложения нужных типов, то метод сбора разрастется до подобного кода:
Python:
def get_conversations(vk, vk_tools):
    # получаем список диалогов
    conversation_list = vk_tools.get_all(method='messages.getConversations', max_count=100,
                                         values={'extended': 1, 'fields': 'first_name, last_name, name'})
    conversations = []
    # формируем список обработанных диалогов
    for conversation in conversation_list['items']:
        conversations.append(get_dialog_data(conversation, vk, vk_tools))
    return conversations

# сборка нужных данных в одной структуре
def get_dialog_data(conversation, vk, vk_tool):
    # запоминаем id, type(chat, user, group), name
    dialog = {'id': conversation['conversation']['peer']['id'], 'type': conversation['conversation']['peer']['type'],
              'name': get_dialog_name(conversation, vk)}
    # формируем сообщения и вложения
    dialog['messages'], dialog['attachments'] = get_messages(vk, vk_tool, dialog['id'])
    return dialog

def get_dialog_name(conversation, vk):
    # если тип беседы - личный или группа,
    if conversation['conversation']['peer']['type'] in ['user', 'group']:
        # то добавляем пользователя в список {id: name}
        users_add(conversation['conversation']['peer']['id'], vk)
        # Получаем имя юзера или группы
        name = users[abs(conversation['conversation']['peer']['id'])]
    # если диалог - чат,
    elif conversation['conversation']['peer']['type'] == 'chat':
        # то получаем имя из title
        name = conversation['conversation']['chat_settings']['title']
    else:
        # иначе неожиданный результат, присвоим unknown
        name = r'{unknown}'
    # заменяем все "плохие" символы
    for c in ['\\', '/', ':', '*', '?', '<', '>', '|', '"']:
        name = name.replace(c, '_')
    return name

def users_add(uid, vk):
    try:
        # если имени нет в списке
        if uid not in users:
            # то если юзер (id>0)
            if uid > 0:
                # получаем инфо о человеке
                user = vk.users.get(user_ids=uid)[0]
                # если удален, то имя - deleted
                if ('deactivated' in user) and (user['deactivated'] == 'deleted') and (
                        user['first_name'] == 'DELETED'):
                    name = 'DELETED'
                else:
                    # если все норм - имя фамилия
                    name = f"{user['first_name']} {user['last_name']}"
            else:
                # если группа (ид <0), то получаем инфо об группе
                group = vk.messages.getConversationsById(peer_ids=uid, extended=1)['groups'][0]
                # и получаем имя
                name = group['name']
            # добавляем в список
            users[abs(uid)] = name
    except:
        # в случае ошибки присвоим неизвестное имя
        users[abs(uid)] = '{unknown user}'

# получаем сообщения и вложения
def get_messages(vk, vk_tools, cid):
    messages = []
    attachments = {}
    # читаем сообщения методом getHistory
    history = vk_tools.get_all(max_count=200, method='messages.getHistory',
                               values={'rev': 1, 'extended': 1, 'fields': 'first_name, last_name', 'peer_id': cid})
    # Для каждого сообщения
    for message in history['items']:
        # добавляем пользователя в список
        users_add(message['from_id'], vk)
        # обрабатываем сообщение
        mess = message_handler(message)
        # обрабатываем аттачменты
        attach = attach_handler(message)
        if mess: messages.append(mess)
        # добавляем в структура {type: [objects]}
        for at_type in attach:
            if at_type not in attachments: attachments[at_type] = []
            attachments[at_type] += attach[at_type]
    return messages, attachments

def message_handler(msg)
    # если сообщение не пусто
    if len(msg['text']) > 0:        text = [line for line in msg['text'].split('\n') if line]
        # то возвращаем текст, от кого, дату
        return {'text': text, 'from': users.get(msg['from_id']), 'date': msg['date']}
    return None

def attach_handler(msg):
    attachments = {}
    # для каждого вложения
    for attach in msg['attachments']:
        # определяем тип
        at_type = attach['type']
        at = attach[at_type]
        # если нужный тип не в списке вложений, то создаем список
        if at_type not in attachments and at_type in ['photo', 'doc', 'audio_message']:
            attachments[at_type] = []
        # в зависимости от типа, добавляем нужную структуру
        if at_type == 'photo':
            attachments[at_type].append(at['sizes'][-1]['url'])
        elif at_type == 'doc':
            attachments[at_type].append({'url': at['url'], 'access_key': at['access_key'], 'size': at['size'],
                                         'name': f"{at['title']}_{at['id']}.{at['ext']}", 'date': at['date']})
        elif at_type == 'audio_message':
            attachments[at_type].append({'url': at['link_ogg'], 'access_key': at['access_key'],
                                         'name': f"{at['owner_id']}_{at['id']}.oog"})
    return attachments

JSON:
{
  "id": "<id>", "type": "user", "name": "Имя Фамилия",
  "messages": [{ "text": ["Держи ссылку", "http://vk.com/<url>"], "from": "Имя Фамилия", "date": 1465503764}],
  "attachments": {
      "photo": ["https://pp.userapi.com/<someurl>"],
          "doc": [
              {
                  "url": "https://vk.com/doc<id>_437686018?hash=953f6&api=1&no_preview=1",
                  "access_key": "e9e3a1868e76702b49", "size": 868057,
                  "name": "tumalo1_400.gif_437686018.gif","date": 1469215825},
              {
                  "url": "https://vk.com/doc<id>_437653303?hash=36d440&api=1&no_preview=1",
                  "access_key": "5578816d89e9600d63", "size": 966158,
                  "name": "https://vk.com/p_437653303.gif", "date": 1468267537}
          ]
  }
}

На этом все нужные данные собраны, теперь научимся скачивать файлы.

Скачивание файлов.
В простом представлении фукнкция выглядит так:
Python:
import shutil
from os.path import exists

# функция по скачиванию файла по адресу url в файл path
def simple_download(url, path):
    try:
        # если файл еще на скачан
        if not exists(path):
            # делаем запрос по ссылке, указываем что это поток и задаем таймаут
            r = requests.get(url, stream=True, timeout=(30, 5))
            # открываем файл по пути path в режиме записи байтов
            with open(path, 'wb') as f:
                # сохраняем объект в файл
                shutil.copyfileobj(r.raw, f)
    # в случае ошибки выводим ее на экран
    except Exception as e:
        print(e)

На самом деле, если совсем минимизировать(исключительно для понимания механизма), то получится такая функция:
Python:
def simple_download(url, path):
    r = requests.get(url, stream=True, timeout=(30, 5))
    with open(path, 'wb') as f:
        hutil.copyfileobj(r.raw, f)

В первом варианте, учитывается отказ от повторного скачивания файла и обработка ошибочного запроса (не вылетает из программы при ошибке).
По поводу повторного скачивания: если файл с таким именем существует, значит не скачиваем заново. Для данного проекта актуально, так как мы формируем уникальное имя файла по id. Для других проектов, эта "проверка на повторное скачивание" не является достоверной.

Теперь адаптируем скачивание под наши цели + добавим многопоточность:
Python:
import shutil
from itertools import repeat
from multiprocessing.pool import Pool
from os import cpu_count
from os.path import join, exists
PULL_PROCESSES = 4 * cpu_count()

def download_engine(object_list, path):
    with Pool(PULL_PROCESSES) as pool:
        pool.starmap(download, zip(object_list, repeat(path)))

def download(obj, root):
    try:
        filename, url = get_filename_url(obj)
        path = join(root, filename)
        if not exists(path):
            r = requests.get(url, stream=True, timeout=(30, 5))
            with open(path, 'wb') as f:
                shutil.copyfileobj(r.raw, f)
    except Exception as e:
        print(e)
        return False

def get_filename_url(obj):
    if not obj:
        raise ValueError('Объект пустой')
    # если получена строка, то запоминаем ее как url и последнюю часть после / - имя файла
    if isinstance(obj, str):
        url = obj
        del obj
        filename = url.split('/')[-1]
    # если получен словарь, то берем url из поля url, имя собираем в update_path
    elif isinstance(obj, dict):
        url = obj.pop('url')
        options = obj
        filename, url = update_path(url, options=options)
    else:
        # иначе не обрабатываем
        raise ValueError('Неизвестный объект')
    # заменяем недопустимые символы на _
    for char in ['\\', '/', ':', '*', '?', '<', '>', '|', '"']:
        filename = filename.replace(char, '_')
    # возвращаем имя файла и url
    return filename, url

# собираем имя и урл по "опциям" словаря
def update_path(url, filename=None, options=None):
    if options is None:
        options = {}
    if 'name' in options:
        filename = '_'.join(options['name'].split(' '))
        if 'ext' in options:
            if filename.split('.')[-1] != options['ext']:
                filename += f".{options['ext']}"
    if 'prefix' in options:
        filename = str(options['prefix']) + '_' + filename
    if 'access_key' in options:
        url = f"{url}?access_key={options['access_key']}"
    return filename, url

Теперь мы можем скачивать любые файл из наших структур. Вот пример как это делать:
Python:
def dump_albums(albums, path):
    # для каждого альбома
    for album in albums:
        # создаем свой путь
        current_path = join(path, album['name'])
        makedirs(current_path, exist_ok=True)
        # скачиваем по ссылкам из photos
        download_engine(album['photos'], current_path)

def dump_docs(docs, path):
    makedirs(path, exist_ok=True)
    download_engine(docs['docs'], path)

def dump_dialogs(dialogs, path):
    for dialog in dialogs:
        dialog_name = f"{dialog['name'].replace(' ', '_')}_{dialog['id']}"
        current_path = join(path, dialog_name)
        makedirs(current_path, exist_ok=True)
        with open(f'{join(current_path,dialog_name)}.json', 'w') as dialog_json:
            json.dump(dialog, dialog_json)
            for mode in dialog['attachments']:
                attachment_path = join(current_path, mode)
                download_engine(dialog['attachments'][mode], attachment_path)

Конец первой части. Полученный код можете посмотреть в прикрепленных файлах - sandbox.py


Часть 2 - Наведение красоты
Теперь соберем все наработки в обособленные классы. Выделим следующие классы:
  • LoginVK - класс по работе с авторизацией (возвращает сессию)
  • MethodsVK - сборщик данных, используя методы ВК (используются только тут) (возвращает облегченные словари нужного вида)
  • DownloadManager - класс для скачивания списка файлов в несколько потоков. Генерация имен для скачанных файлов тоже тут
  • DumpVK - методы по связи между словарями с данными и downloadManager'ом

Провел рефакторинг и добавив некоторые функции:
  • читаемый вид размера документа и дат сообщений
  • сохранение инфо об аккаунте, списков пользователей
  • дамп-менеджер
  • сохранение переписки в txt формате
  • добавление лога ошибок
Весь код не поместился в статью (при сохранении выводит ошибку), поэтому я постараюсь прикрепить конечный код модуля к сообщению под этим постом. Также все наработки и конечный модуль прикреплен во вложения.

Python:
def dump_manager(config):
    dump_config = config['dump_config']

    if config['mode'] == 'collect':
        vk = collect(config['login_data'])
        dump_config.update({'albums': vk.albums, 'docs': vk.docs, 'conversations': vk.conversations, 'users': vk.users,
                            'account': vk.account})
    elif config['mode'] == 'dump':
        dump_config.update({'albums': load_collection(dump_config['albums']),
                            'docs': load_collection(dump_config['docs']),
                            'conversations': load_collection(dump_config['conversations'])})
    elif config['mode'] == 'redump_errors':
        try_dump_error_list(dump_config['path'], dump_config['download_errors'])
    if config['mode'] in ['collect', 'dump']:
        dump_vk = DumpVK(dump_config)
        dump_vk.dump()

def collect(config):
    login_vk = LoginVK(config)
    return MethodsVK(login_vk.vk, login_vk.vk_tools, login_vk.account)

def load_collection(filename):
    try:
        with open(filename, 'r') as collection_file:
            return json.load(collection_file)
    except:
        return []

def try_dump_error_list(path, filename):
    with open(filename, 'r') as errors_file:
        error_list = json.load(errors_file)
    dump_list = [error['obj'] for error in error_list]
    errors = DownloadManager.download_engine(dump_list, join(path, 'errors'))
    with open(filename, 'w') as download_errors_file:
        json.dump(errors, download_errors_file)

if __name__ == '__main__':
    try:
        with open('config.json', 'r') as config_file:
            vk_config = json.load(config_file)
        dump_manager(vk_config)
    except Exception as e:
        error_log.add(__name__, e)
    error_log.save_log('error.log')

Подробнее про дамп-менеджер:
  • Сбор данных - collect - произвести подключение к вк, произвести полный сбор данных в списки json.
    • если download = true, то файлы скачиваются сразу.
    • если download = false, то скачивание не происходит.
    • При любом варианте "коллекции файлов" сохраняются в отдельные json(фото+диалоги+документы)
  • Скачивание данных - dump - скачивание уже существующих коллекций (из файлов)
  • Обработка ошибок - redump_error - При большом количестве потоков скачивания выдает ошибку - много запросов к ресурсу(при ошибке скачивания файла он логгируется в отдельный список). В данном режиме происходит повторное скачивание файлов из списка ошибок. Если успешно, то файл из списка удаляется.
JSON:
[
  {
    "mode": "collect",
    "dump_config": {
      "path": "dump",
      "download": false,
      "download_errors": "download_errors.json"
    },
    "login_data": {
      "token": "",
      "login": "",
      "password": ""
    }
  },
  {
    "mode": "dump",
    "dump_config": {
      "albums": "albums.json",
      "docs": "docs.json",
      "conversations": "conversations.json",
      "path": "dump",
      "download": true,
      "download_errors": "download_errors.json"
    }
  },
  {
    "mode": "redump_errors",
    "dump_config": {
      "path": "dump",
      "download_errors": "download_errors.json"
    }
  }
]

Best-way эксплуатация:
  • Запускаем конфиг collect БЕЗ скачивания - 1й пример
  • Копируем полученные файлы (conversations.json, docs.json, albums.json) в корень скрипта(рядом с файлом config.json)
  • Запускаем конфиг dump (данные из файлов, download = true) - 2ой пример конфига.
  • В случае ошибок при скачивании, запустить конфиг redump_error - 3й пример
Уже говорил, что данная сборка для меня подходит хорошо, но расскажу вам про пути масштабирования:
  • Для скачивания видео, аудио, лайкосиков и т.д.
    • Обработайте методы вк по получению нужной инфы в классе MethodsVK
    • Введите переменные для хранения новых структур
    • В классе DumpVK добавить метод по скачиванию структуры (мост между url ваших объектов, и download_manager'ом)
    • делайте по подобию других методов, там все наглядно
  • Для скачивания новых типов вложений добавить в attach_handler подобную инструкцию:
    • Python:
      if at_type == 'your_type':
          attachments[at_type].append({'url', 'name', 'ext', etc})
Проект получился мультиплатформенным, так как зависимостей от конкретной ОС нет.

На последок создадим exe версию приложения для Windows:
Находясь в виртуальном окружении установите модуль:
pip install pyinstaller

Для автоматизации процесса рекомендую использовать bat файл следующего вида:
Bash:
call env\Scripts\activate
python -OO -m PyInstaller --noconfirm --log-level=WARN ^
--onefile --noconsole ^
--icon=vk.ico --name=vk_dump ^ vk.py

Этот батник соберет весь код vk.py в один файл vk_dump.exe, с иконкой vk.ico
При попытке собрать exe, у меня появилось множество ошибок, связанных с импортом.
Решение:
В виртуальном окружении удалите модуль:
pip uninstall enum34
и повторите запуск батника.

ATTENTION! Я не стал выкладывать exe файл, и вот почему:
Проверяя работоспособность исполняемого файла, я успешно собрал данные в json-коллекции, а на этапе дампа по файлам, exe'шник засорил все 4 ГБ ОЗУ (создал около 2000 процессов!) на виртуальной машине, и через 45 минут я отключил машину, а при перезагрузке обнаружил, что он ничего и не скачал. Сам скрипт РАЗУМЕЕТСЯ рабочий (проверял в linux и windows через pycharm).
Поэтому, дабы не прослыть дядей, который майнеры вам в компы сует, я приложил инструкцию по созданию exe в пару кликов (в сурсах будут все батники), а там вы уж сами думайте, нужно ли делать ехе для данного проекта или нет.

Приложенные архивы:
  • source.zip - исходные коды данного проекта
    • vk.py - основной модуль со всеми классами описанными в статье
    • sandbox.py - наработки из первой части статьи
    • error_log.py - класс для логгирования ошибок
    • config.json - 1ый пример конфига - коллекция данных
  • additional.zip - дополнительные необязательные файлы для проекта:
    • make_env.bat - автоустановка виртуального окружения, установка зависимостей из requirements.txt
    • requirements.txt - необходимые дополнительные модули для проекта
    • template_configs.json - примеры правильных конфигов
    • make_exe.bat - компиляция vk.py в vk_dump.exe (если будет ошибка, удалите enum34)
    • vk.ico - иконка для vk_dump.exe
На этом все, спасибо за внимание.
 

Вложения

  • additional.zip
    5,1 КБ · Просмотры: 1 201
  • source.zip
    8,2 КБ · Просмотры: 1 422

gh0st4ge

One Level
25.11.2018
6
44
BIT
15
Как обещал, прикрепляю результат моих трудов :alien:
Python:
import json
import shutil
from datetime import datetime
from itertools import repeat
from multiprocessing import Pool
from os import cpu_count, makedirs
from os.path import join, exists

import requests
import vk_api

from error_log import ErrorLog

INVALID_CHARS = ['\\', '/', ':', '*', '?', '<', '>', '|', '"']
error_log = ErrorLog()


def user_friendly_size(size):
    suffix_set = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
    level = 0
    while size > 1024:
        level += 1
        size = size / 1024
    suffix = suffix_set[level]
    if level != 0: size = f'{size:.2f}'
    return f'{size} {suffix}'


class LoginVK:
    API_VERSION = '5.92'
    APP_ID = 6781880

    def __init__(self, login_data):
        self.login_data = login_data
        self.vk = None
        self.vk_tools = None
        self.account = None
        self.login_vk()

    def login_vk(self):
        try:
            if 'token' in self.login_data and self.login_data['token']:
                token = self.login_data['token']
                vk_session = vk_api.VkApi(token=token, app_id=LoginVK.APP_ID, auth_handler=self.auth_handler,
                                          api_version=LoginVK.API_VERSION)
            elif 'login' in self.login_data and 'password' in self.login_data:
                login, password = self.login_data['login'], self.login_data['password']
                vk_session = vk_api.VkApi(login, password, captcha_handler=self.captcha_handler, app_id=LoginVK.APP_ID,
                                          api_version=LoginVK.API_VERSION, auth_handler=self.auth_handler)
                vk_session.auth(token_only=True, reauth=True)
            else:
                raise KeyError('Введите токен или пару логин-пароль')
            self.vk = vk_session.get_api()
            self.vk_tools = vk_api.VkTools(self.vk)
            self.account = self.vk.account.getProfileInfo()
        except Exception as e:
            raise ConnectionError(e)

    @staticmethod
    def auth_handler():
        key = input('Введите код двухфакторой аутентификации: ')
        remember_device = True
        return key, remember_device

    @staticmethod
    def captcha_handler(captcha):
        key = input(f"Введите капчу ({captcha.get_url()}): ").strip()
        return captcha.try_again(key)


class MethodsVK:
    def __init__(self, vk, vk_tools, account):
        self.vk = vk
        self.vk_tools = vk_tools
        self.account = account
        self.albums = []
        self.docs = []
        self.users = {}
        self.conversations = []
        self.get_albums()
        self.get_docs()
        self.get_conversations()

    def get_albums(self):
        for album in self.vk.photos.getAlbums(need_system=1)['items']:
            try:
                photos = self.vk_tools.get_all(values={'album_id': album['id'], 'photo_sizes': 1}, method='photos.get',
                                               max_count=1000)
                self.albums.append({'name': '_'.join(album['title'].split(' ')),
                                    'photos': [p['sizes'][-1]['url'] for p in photos['items']]})
            except Exception as e:
                error_log.add('get_albums', e)

    def get_docs(self):
        docs = self.vk.docs.get()
        self.docs = {'count': docs['count'], 'size': 0, 'docs': []}
        for doc in docs['items']:
            try:
                self.docs['docs'].append({'url': doc['url'], 'name': f"{doc['title']}_{doc['id']}.{doc['ext']}"})
                self.docs['size'] += doc['size']
            except Exception as e:
                error_log.add('get_docs', e)
        self.docs['size'] = user_friendly_size(self.docs['size'])

    def get_conversations(self):
        conversations = self.vk_tools.get_all(method='messages.getConversations', max_count=100,
                                              values={'extended': 1, 'fields': 'first_name, last_name, name'})
        for conversation in conversations['items']:
            self.conversations.append(self.get_dialog_data(conversation))

    def get_dialog_data(self, conversation):
        dialog = {'id': conversation['conversation']['peer']['id'],
                  'type': conversation['conversation']['peer']['type']}
        if dialog['type'] in ['user', 'group']:
            self.users_add(dialog['id'])
            dialog['name'] = self.users[abs(dialog['id'])]
        elif dialog['type'] == 'chat':
            dialog['name'] = conversation['conversation']['chat_settings']['title']
        else:
            dialog['name'] = r'{unknown}'
        for c in INVALID_CHARS:
            dialog['name'] = dialog['name'].replace(c, '_')
        dialog['messages'], dialog['attachments'] = self.get_messages(dialog['id'])
        return dialog

    def users_add(self, uid):
        try:
            if uid not in self.users:
                if uid > 0:
                    user = self.vk.users.get(user_ids=uid)[0]
                    if ('deactivated' in user) and (user['deactivated'] == 'deleted') and (
                            user['first_name'] == 'DELETED'):
                        name = 'DELETED'
                    else:
                        name = f"{user['first_name']} {user['last_name']}"
                else:
                    group = self.vk.messages.getConversationsById(peer_ids=uid, extended=1)['groups'][0]
                    name = group['name']
                self.users[abs(uid)] = name
        except:
            self.users[abs(uid)] = '{unknown user}'

    def get_messages(self, cid):
        messages = []
        attachments = {}
        history = self.vk_tools.get_all(max_count=200, method='messages.getHistory',
                                        values={'rev': 1, 'extended': 1, 'fields': 'first_name, last_name',
                                                'peer_id': cid})
        for message in history['items']:
            try:
                if message['from_id'] not in self.users:
                    self.users_add(message['from_id'])
                mess = self.message_handler(message)
                if mess:
                    messages.append(mess)
                attach = self.attach_handler(message)
                for at_type in attach:
                    if at_type not in attachments:
                        attachments[at_type] = []
                    attachments[at_type] += attach[at_type]
            except Exception as e:
                error_log.add('get_messages', e)
        return messages, attachments

    def message_handler(self, msg):
        if len(msg['text']) > 0:
            text = [line for line in msg['text'].split('\n') if line]
            return {'text': text, 'from': self.users.get(msg['from_id']),
                    'date': str(datetime.fromtimestamp(msg['date']))}
        return None

    @staticmethod
    def attach_handler(msg):
        attachments = {}
        for attach in msg['attachments']:
            try:
                at_type = attach['type']
                at = attach[at_type]
                if at_type not in attachments and at_type in ['photo', 'doc', 'audio_message']:
                    attachments[at_type] = []
                if at_type == 'photo':
                    attachments[at_type].append(at['sizes'][-1]['url'])
                elif at_type == 'doc':
                    attachments[at_type].append({'url': at['url'], 'access_key': at['access_key'],
                                                 'size': user_friendly_size(at['size']),
                                                 'name': f"{at['title']}_{at['id']}.{at['ext']}",
                                                 'date': str(datetime.fromtimestamp(at['date']))})
                elif at_type == 'audio_message':
                    attachments[at_type].append({'url': at['link_ogg'], 'access_key': at['access_key'],
                                                 'name': f"{at['owner_id']}_{at['id']}.oog"})
            except Exception as e:
                error_log.add('attach_handler', e)
        return attachments


class DownloadManager:
    PULL_PROCESSES = 4 * cpu_count()

    @staticmethod
    def download_engine(object_list, path):
        makedirs(path, exist_ok=True)
        with Pool(DownloadManager.PULL_PROCESSES) as pool:
            results = pool.starmap(DownloadManager.download, zip(object_list, repeat(path)))
            # возвращаем список с ошибками
        return [file for file in results if file is not True]

    @staticmethod
    def download(obj, root):
        reserve_obj = obj
        try:
            filename, url = DownloadManager.get_filename_url(obj)
            path = join(root, filename)
            if not exists(path):
                r = requests.get(url, stream=True, timeout=(30, 5))
                with open(path, 'wb') as f:
                    shutil.copyfileobj(r.raw, f)
            return True
        except Exception as e:
            error_log.add('download', e)
            return {'message': str(e), 'obj': reserve_obj}

    @staticmethod
    def get_filename_url(obj):
        if not obj:
            raise ValueError('Объект пустой')
        if isinstance(obj, str):
            url = obj
            filename = url.split('/')[-1]
        elif isinstance(obj, dict):
            url = obj['url']
            options = obj
            filename, url = DownloadManager.update_path(url, options=options)
        else:
            raise ValueError('Неизвестный объект')
        for char in INVALID_CHARS:
            filename = filename.replace(char, '_')
        return filename, url

    @staticmethod
    def update_path(url, filename=None, options=None):
        if options is None:
            options = {}
        if 'name' in options:
            filename = '_'.join(options['name'].split(' '))
        if 'access_key' in options:
            url = f"{url}?access_key={options['access_key']}"
        return filename, url


class DumpVK:
    def __init__(self, config):
        self.account = self.get_from_config(config, 'account')
        self.users = self.get_from_config(config, 'users')
        self.albums = self.get_from_config(config, 'albums') or []
        self.docs = self.get_from_config(config, 'docs') or []
        self.conversations = self.get_from_config(config, 'conversations') or []
        self.path = self.get_from_config(config, 'path') or 'dump'
        self.download = self.get_from_config(config, 'download')
        self.download_errors_path = config['download_errors']
        self.errors = []

    @staticmethod
    def get_from_config(config, parameter):
        if parameter in config:
            return config[parameter]
        else:
            return None

    def dump(self):
        self.dump_info(self.path)
        self.dump_albums(self.albums, join(self.path, 'photos'), self.download)
        self.dump_docs(self.docs, join(self.path, 'docs'), self.download)
        self.dump_dialogs(self.conversations, join(self.path, 'dialogs'), self.download)
        if self.errors:
            with open(self.download_errors_path, 'w') as download_errors_file:
                json.dump(self.errors, download_errors_file)

    def dump_albums(self, albums, path, download=True):
        makedirs(path, exist_ok=True)
        with open(join(path, 'albums.json'), 'w') as alb_file:
            json.dump(albums, alb_file)
        if download:
            for album in albums:
                current_path = join(path, album['name'])
                self.errors += DownloadManager.download_engine(album['photos'], current_path)

    def dump_info(self, path):
        makedirs(path, exist_ok=True)
        if self.account:
            with open(join(path, 'account.json'), 'w') as acc_file:
                json.dump(self.account, acc_file)
        if self.users:
            with open(join(path, 'users.json'), 'w') as users_file:
                json.dump(self.users, users_file)

    def dump_docs(self, docs, path, download=True):
        makedirs(path, exist_ok=True)
        with open(join(path, 'docs.json'), 'w') as doc_file:
            json.dump(docs, doc_file)
        if download and 'docs' in docs:
            self.errors += DownloadManager.download_engine(docs['docs'], path)

    def dump_dialogs(self, dialogs, path, download=True):
        makedirs(path, exist_ok=True)
        with open(f"{join(path,'conversations')}.json", 'w') as dialog_json:
            json.dump(dialogs, dialog_json)
        if download:
            for dialog in dialogs:
                dialog_name = f"{dialog['name'].replace(' ', '_')}_{dialog['id']}"
                current_path = join(path, dialog_name)
                makedirs(current_path, exist_ok=True)
                with open(f'{join(current_path,dialog_name)}.json', 'w') as dialog_json:
                    json.dump(dialog, dialog_json)
                DumpVK.save_conversation_txt(dialog['messages'], f'{join(current_path,dialog_name)}.txt')
                for mode in ['photo', 'doc', 'audio_message']:
                    try:
                        if mode in dialog['attachments']:
                            attachment_path = join(current_path, mode)
                            self.errors += DownloadManager.download_engine(dialog['attachments'][mode], attachment_path)
                    except Exception as e:
                        error_log.add('dump_dialogs', e)

    @staticmethod
    def save_conversation_txt(messages, filename):
        with open(filename, 'w', encoding='utf8') as dialog_file:
            previous = None
            for message in messages:
                try:
                    text = '\n'.join(message["text"])
                    if message["from"]:
                        name = f'{message["from"]}:'
                    else:
                        name = 'unknown_user'
                    if name == previous:
                        current = ' ' * (len(name) + 2)
                    else:
                        current = name
                    text_message = f'{current}\t{text}\t[{message["date"]}]'
                    dialog_file.write(f'{text_message}\n')
                    previous = name
                except Exception as e:
                    error_log.add('save_conversation_txt', e)


def dump_manager(config):
    dump_config = config['dump_config']

    if config['mode'] == 'collect':
        vk = collect(config['login_data'])
        dump_config.update({'albums': vk.albums, 'docs': vk.docs, 'conversations': vk.conversations, 'users': vk.users,
                            'account': vk.account})
    elif config['mode'] == 'dump':
        dump_config.update({'albums': load_collection(dump_config['albums']),
                            'docs': load_collection(dump_config['docs']),
                            'conversations': load_collection(dump_config['conversations'])})
    elif config['mode'] == 'redump_errors':
        try_dump_error_list(dump_config['path'], dump_config['download_errors'])
    if config['mode'] in ['collect', 'dump']:
        dump_vk = DumpVK(dump_config)
        dump_vk.dump()


def collect(config):
    login_vk = LoginVK(config)
    return MethodsVK(login_vk.vk, login_vk.vk_tools, login_vk.account)


def load_collection(filename):
    try:
        with open(filename, 'r') as collection_file:
            return json.load(collection_file)
    except:
        return []


def try_dump_error_list(path, filename):
    with open(filename, 'r') as errors_file:
        error_list = json.load(errors_file)
    dump_list = [error['obj'] for error in error_list]
    errors = DownloadManager.download_engine(dump_list, join(path, 'errors'))
    with open(filename, 'w') as download_errors_file:
        json.dump(errors, download_errors_file)


if __name__ == '__main__':
    try:
        with open('config.json', 'r') as config_file:
            vk_config = json.load(config_file)
        dump_manager(vk_config)
    except Exception as e:
        error_log.add(__name__, e)
    error_log.save_log('error.log')
 

hikiko4ern

One Level
08.11.2018
5
9
BIT
0
Что-то мне подсказывает, что твой код что-то уж сильно похож на этот хд
всё понимаю, но копипаст/небольшое_изменение исходников, коим является 66% твоего кода - это уж слишком ᕙ(⇀‸↼‶)ᕗ
 
Последнее редактирование:

r4z0r5

Green Team
03.04.2018
14
18
BIT
2
Хорошая статья, не придется вникать в api.vk.com и developers. Сам писал подобное, очень эффективный инструмент. Код п**дить не хорошо конечно, но ты скомпилировал в одном месте информацию.
 
  • Нравится
Реакции: Tihon49 и hikiko4ern
D

dob

Почему у меня в json файлах диалогов вместо букв \u043d\u0430 \u041a\u0430\u0440\u0442\u0443 ?
 
C

codefather_ru

Python:
vk_session = vk_api.VkApi(login, password, app_id=APP_ID, api_version=API_VERSION)
vk_session.auth(token_only=True, reauth=True)

попытка входа по логину\паролю дает "vk_api.exceptions.AuthError: API auth error: This application has no right to use messages"

token = '' # логинимся по токену
vk = login_vk(token)
этот вариант кода так же не работает.
а если заменить на логин\пароль - получим vk_api.exceptions.ApiError: [5] User authorization failed: invalid access_token (4).
 

hikiko4ern

One Level
08.11.2018
5
9
BIT
0
попытка входа по логину\паролю дает "vk_api.exceptions.AuthError: API auth error: This application has no right to use messages"


этот вариант кода так же не работает.
а если заменить на логин\пароль - получим vk_api.exceptions.ApiError: [5] User authorization failed: invalid access_token (4).
админы прикрыли доступ к апи сообщений. Как вариант - юзать токен от оф. или прошедших модерацию приложений.

Набросал как-то кривой скрипт, он получает токен, прикидываясь приложением для ведра. Надо лишь логин и пароль вписать в самом коде.
 

Вложения

  • auth.zip
    676 байт · Просмотры: 573
T

taxumicore

админы прикрыли доступ к апи сообщений. Как вариант - юзать токен от оф. или прошедших модерацию приложений.

Набросал как-то кривой скрипт, он получает токен, прикидываясь приложением для ведра. Надо лишь логин и пароль вписать в самом коде.

не компилится
почему?
Код:
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

C:\Users\Administrator>C:\Users\Administrator\Downloads\auth\auth.py
Traceback (most recent call last):
  File "C:\Users\Administrator\Downloads\auth\auth.py", line 2, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

C:\Users\Administrator>
 
T

taxumicore

потому что не установлены зависимости?
не просто так ведь там написано No module named 'requests'
установил модуль request. теперь новая ошибка

Код:
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.

C:\Users\Administrator>C:\Users\Administrator\Downloads\auth\auth.py
Traceback (most recent call last):
  File "C:\Users\Administrator\Downloads\auth\auth.py", line 13, in <module>
    r2 = requests.get(t1['redirect_uri'])
KeyError: 'redirect_uri'

C:\Users\Administrator>
 

DwY

New member
18.11.2020
1
0
BIT
1
Есть у кого-нибудь дампер сообщений, который создает html документ?
Что-то вроде такого?
1605709159337.png
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!