• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья Поиск в ВК фото с геометками с помощью Python. Часть #03.1 Дополнение

Данная статья является неким логическим продолжением предыдущей темы. И с одной стороны она является плавно вытекающей из того материала, что был рассмотрен в этих статьях: раз, два, три. А речь о том же ВК, но уже не со стороны рандомного поиска геометок, а со стороны целенаправленного поиска по ID пользователя. В принципе, я не открываю здесь никакой Америки, так как данный функционал предусмотрен API ВК. Даже более того, на странице пользователя, если у него есть геометки, можно посмотреть их на карте. Единственное отличие в том, что вам не расскажут там про их координаты и не дадут точного адреса. Давайте посмотрим, как можно сделать что-то подобное на Python.

03_1.jpg


Вообще, на данный материал меня натолкнул пост про VK visualizer. Мне показались интересными те возможности, что предоставляет данный скрипт. Вот только у меня, почему-то, запустить его не получилось. Хотя, все библиотеки нужные для работы установил. Запускал его с правами админа. В общем, помучившись немного, я подумал о том, чтобы попробовать сделать что-то подобное. Не такое же, нет, я в аналитике слабовато. Да и не совсем я понял, что там анализируется. Запустить то не смог. Но, подобное. Тем более что так совпало, нашелся материал из предыдущих частей. Вот и получилось то, что получилось. Тем не менее, данный скрипт является самостоятельным и пересекается с предыдущей темой только лишь направлением поиска данных. Давайте начнем писать код.

0000.jpg




Что потребуется?

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

pip install vk-api geopy ipyleaflet python-docx requests

Давайте по порядку. Первая, это та, с помощью которой мы будем работать с ВК, а именно vk-api. Она позволяет получить данные о фото, пользователе и многое другое.

Следующая библиотека geopy. С ее помощью мы будем получать данные об адресе по координатам, то есть, обратное геокодирование. Данные, в данном случае, берутся из Open Street Maps. И некоторых адресов там нет. Но, это редкость, и в основном, все, что нужно находиться довольно хорошо.

Далее на очереди библиотека ipyleaflet. С ее помощью мы будем отмечать точки на карте, ведь она и предназначена для работы с геоданными. А также, есть в ней интересный модуль, который позволяет связать и визуализировать точки между собой.

Следующая библиотека python-docx, с ним я уже работал в предыдущих статьях. С его помощью мы сохраним данные в формате Word. Да, слегка страшненько, но, вполне себе удобоваримо.

Ну и последняя библиотека requests. Она позволяет делать запросы к сайтам по заданным адресам и получать самые разнообразные данные. В нашем случае мы будем с ее помощью скачивать картинки. Вообще, все, кто хоть немного интересовался парсингом данных, с ней уже знакомы.

Теперь давайте импортируем все это в наш скрипт.

Python:
import json
import os.path
import time
from datetime import datetime as dt

import docx
from docx.shared import Pt
from geopy.exc import GeocoderUnavailable
from geopy.geocoders import Nominatim
from ipyleaflet import AntPath, Map, Marker, LegendControl
from ipywidgets import HTML, Layout
from requests import get
from vk_api import VkApi
from vk_api.exceptions import ApiError

from set import token

Как видите, помимо установленных библиотек я использую и те, что предустановлены уже с Python. И дополнительно импортирую модуль, в котором содержится токен, для использования его в запросах vk_api. Как получить токен я описывал в предыдущих статьях, и останавливаться на этом мы не будем.

Скажу лишь только, что данный скрипт использует в работе ID пользователя. Это те циферки, которые идут после id в адресе страницы. Найти их можно соответственно там. Если же их там не содержится и id пользователя скрыт отображаемым псевдонимом, можно воспользоваться вот этим . Ну и совсем для экстремалов, id пользователя можно посмотреть, открыв исходный код страницы. Там он тоже есть. Продолжаем.


Получение и обработка пользовательского ввода

Давайте начнем по порядку, по логике работы скрипта. И начнем с функции main().

Python:
# ввод ID пользователя и передача в функцию ввода действий пользователя
def main():
    try:
        owner_id = input('\n[+] Введите ID пользователя\n   - для выхода введите x\n   >>> ')
        if owner_id.lower() == "x":
            exit(0)
        if not owner_id.isdigit():
            main()
        user_change(owner_id)
        return
    except KeyboardInterrupt:
        print('\n[+] Good by!')

Теперь немного пояснений. Здесь запрашивается id пользователя, так как он будет требоваться на всех этапах работы скрипта. А по сути, является основной составляющей, с которой он работает. Кроме дополнительных параметров в запросах. Так же, дадим пользователю возможность выйти из программы с помощью ввода «x». Если же введен id, делается проверка, является ли введенный id цифрами. Если это не так, предлагается попробовать снова. Если все в порядке, то запускаем функцию обработки выбора пользователя, в которую передаем введенный id.

Теперь запросим у пользователя направления, в которых он хочет поработать. Ведь может быть так, что он уже делал запрос по данному пользователю и ему просто нужно посмотреть данные. Вот здесь мы и будем это обрабатывать.

Python:
# запрос и обработка пользовательского ввода
def user_change(owner_id):
    try:
        user_input = input('\n[+] Выберите действие:\n   [1] Получить информацию о наличии фото с геометками\n   '
                           '[2] Посмотреть полученные даты на карте\n   [3] Посмотреть информацию о пользователе\n   '
                           '[4] Вернуться к вводу ID\n   [5] Выход\n   >>> ')
        if user_input == "1":
            get_user_photo_data(owner_id)
            create_dataset(owner_id)
        elif user_input == "2":
            date_change(owner_id)
        elif user_input == "3":
            user_info_save(owner_id)
        elif user_input == "4":
            main()
            return
        elif user_input == "5":
            exit(0)
        else:
            print('\n[+] Неопознанный ввод. Попробуйте снова\n')
            user_change(owner_id)
    except KeyboardInterrupt:
        print('\n[+] Good by!')

Для начала предлагаем несколько вариантов. То есть, доступен поиск по введенному id, просмотр уже полученных данных на карте, просмотр информации о пользователе, а также возврат к вводу id. Ну и конечно же выход из скрипта.

Если пользователь выбрал получение данных запускается функция get_user_photo_data(owner_id), в которую передается id, она получает разные данные, компонует json и сохраняет их. Затем запускается функция сортировки полученных данных по дате. Ведь за одну дату может быть несколько фото. Я назвал ее create_dataset(owner_id). Она сохраняет данные о координатах, а также компонует содержимое всплывающего сообщения. И все это сохраняется в отдельный файл, расширение у которого .mp. По сути, это просто текстовый файл. Просто так удобнее и пользователь лишний раз не будет в него лазить.

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

Выбор просмотра информации о пользователе компонует документ формата Word и открывает его для просмотра.

Ну и дальнейший выбор, это возврат в предыдущее меню и выход из программы.


Получение информации о фото с геоданными

Я назвал данную функцию get_user_photo_data(owner_id). В нее передается
id пользователя. Вот сама функция, а ее описание немного ниже.

Python:
# получение информации о фото, пользователе
# сохранение информации в файлы
def get_user_photo_data(owner_id):
    session = VkApi(token=token)

    try:
        count = session.get_api().photos.getAll(owner_id=owner_id, access_token=token, offset=0, count=1, photo_sizes=0,
                                                v=5.131)['count']
        if count == 0:
            print('[+] У пользователя нет фото!\n')
            main()
            return

        print(f'[+] Найдено {count} фото')

        count_photo = 0
        photo_dict = []
        for offset in range(0, count, 200):
            photo_all = session.get_api().photos.getAll(owner_id=owner_id, access_token=token, extended=1,
                                                        offset=offset, count=200, need_hidden=1, photo_sizes=0, v=5.131)
            count_photo = count_photo + len(photo_all['items'])
            print(f'\r[+] Загружаю данные о фото: {count_photo}/{count}', end='')
            photo_dict.append(photo_all['items'])

        photo_geo = []
        height = 0
        url = ''
        count = 1
        for i in range(0, len(photo_dict)):
            for photo in photo_dict[i]:
                print(f'\r[+] Выбираю фото с геометками: {count}', end='')
                if photo.get('lat') is not None:
                    for size in photo['sizes']:
                        if size['height'] > height:
                            height = size['height']
                            url = size['url']
                    addr = get_addr((float(photo['lat']), float(photo['long'])))
                    data = {'date': photo['date'], 'lat': photo['lat'], 'long': photo['long'], 'url': url, 'address': addr}
                    photo_geo.append(data)
                    height = 0
                count += 1
        print(f'\n[+] Найдено: {len(photo_geo)} фото с геометками')
        if len(photo_geo) == 0:
            main()
            return

        user_info = session.get_api().users.get(user_id=owner_id, fields='bdate, city, country, connections, '
                                                                         'contacts, last_seen, relatives, relation, '
                                                                         'timezone, photo_max_orig')
        print('[+] Загружаю данные о пользователе')

        if not os.path.isdir(os.path.join(os.getcwd(), 'users')):
            os.mkdir(os.path.join(os.getcwd(), 'users'))
        if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}')):
            os.mkdir(os.path.join(os.getcwd(), 'users', f'{owner_id}'))
        if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json')):
            os.mkdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json'))
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_sort.json'), 'w',
                  encoding='utf-8') as file:
            json.dump(photo_geo, file, indent=4, ensure_ascii=False)
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_info.json'), 'w',
                  encoding='utf-8') as file:
            json.dump(user_info, file, indent=4, ensure_ascii=False)
        print(f"\n[+] Все данные получены и сохранены в папку: "
              f"{os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json')}")
    except ApiError as exc:
        print(f'\n{exc}')
        main()
        return

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

Для начала открываем сессию VK, куда передаем токен для авторизации. После запрашиваем информацию о количестве фото. Для этого запускаем метод photos.getAll, куда передаем id пользователя, токен, смещение, а также количество запрашиваемых фото, которое в данном случае равно 1, так как здесь основной информацией будет количество фото у пользователя. Оно также передается в ответном json.

Затем смотрим, не равняется ли количество фото 0, ведь если это так, выполнять дальнейший код не имеет смысла.

Python:
    session = VkApi(token=token)

    try:
        count = session.get_api().photos.getAll(owner_id=owner_id, access_token=token, offset=0, count=1, photo_sizes=0,
                                                v=5.131)['count']
        if count == 0:
            print('[+] У пользователя нет фото!\n')
            main()
            return

Двигаемся дальше. Выводим сообщение пользователю о количестве найденных фото, объявляем переменную count_photo равную 0, она будет нужна для подсчета загруженных фото и по сути служит для отображения информации пользователю. Создаем список, в который будут сохранятся все полученные json. Запускаем цикл от 0 до количества полученных фото, с шагом 200.

Python:
        print(f'[+] Найдено {count} фото')

        count_photo = 0
        photo_dict = []
        for offset in range(0, count, 200):
            photo_all = session.get_api().photos.getAll(owner_id=owner_id, access_token=token, extended=1,
                                                        offset=offset, count=200, need_hidden=1, photo_sizes=0, v=5.131)
            count_photo = count_photo + len(photo_all['items'])
            print(f'\r[+] Загружаю данные о фото: {count_photo}/{count}', end='')
            photo_dict.append(photo_all['items'])

Здесь есть одна особенность, а именно параметр offset. Вот его нам и нужно будет изменять, так как за один раз ВК позволяет получить информацию только о 200 фото. И если их много больше, тут-то и надо устанавливать параметр offset, то есть смещение. Что мы и будем делать в цикле. Полученный json имеет два основных ключа: count, где передается количество фото пользователя. И items, где уже и хранятся все остальные параметры. Вот по этому ключу и происходит компоновка данных в список.

Переходим к блоку сортировки. Вот его код:

Python:
        photo_geo = []
        height = 0
        url = ''
        count = 1
        for i in range(0, len(photo_dict)):
            for photo in photo_dict[i]:
                print(f'\r[+] Выбираю фото с геометками: {count}', end='')
                if photo.get('lat') is not None:
                    for size in photo['sizes']:
                        if size['height'] > height:
                            height = size['height']
                            url = size['url']
                    addr = get_addr((float(photo['lat']), float(photo['long'])))
                    data = {'date': photo['date'], 'lat': photo['lat'], 'long': photo['long'], 'url': url, 'address': addr}
                    photo_geo.append(data)
                    height = 0
                count += 1
        print(f'\n[+] Найдено: {len(photo_geo)} фото с геометками')
        if len(photo_geo) == 0:
            main()
            return

Создаем список, куда будем добавлять скомпонованные и уже отсортированные данные. Затем переменой height присваиваем значение 0. С ее помощью мы будем сортировать полученные данные, и выбирать фото с наибольшим размером из всех полученных. Также объявляем url. Здесь будет храниться ссылка на самое большое фото.

Запускаем цикл, по скомпонованному ранее списку с json. Проверяем, есть ли в итерируемых данных широта, если нет, значит, эти данные можно пропустить. Если есть, запускаем цикл по данным и выбираем фото самого большого размера. Далее, когда цикл будет завершен, получаем по координатам адрес снимка, после чего компонуем это все в словарь, который сохраняем в список. Объявляем пользователю, сколько найдено фото с геометками. Проверяем длину списка. Если она нулевая, то обрабатывать данные далее нет смысла, так как фото с геометками нет. Поэтому возвращаемся к выбору действие.

Следующий блок, это получение информации о пользователе. Здесь особо мудрить нечего. Получаем с помощью метода users.get данные. В данном методе передаются такие параметры, как id пользователя и дополнительные поля, по которым нужно получить информацию. Все их нужно указать в fields. Данных полей больше и для того, чтобы подробнее ознакомиться с их списком смотрите документацию.

Python:
        user_info = session.get_api().users.get(user_id=owner_id, fields='bdate, city, country, connections, '
                                                                         'contacts, last_seen, relatives, relation, '
                                                                         'timezone, photo_max_orig')
        print('[+] Загружаю данные о пользователе')

В данном случае мы не мудрствуем лукаво, сортировать тут особо нечего, да и нет смысла, а потому все пойдет на сохранение в том виде, в котором прилетело.

Теперь делаем разные проверки на существование нужных для сохранения папок. Если их нет – создаем. После чего сохраняем отсортированные данные о фото, а также отдельным файлом сохраняем данные о пользователе. После чего сообщаем об этом пользователю. И да, весь этот блок заключен в try-except. Так как если вы передадите неверный id, тот, что не существует, скрипт падает в осадок или профиль пользователя окажется приватным. Вот чтобы этого не случилось, и обрабатываем исключение.

Python:
        if not os.path.isdir(os.path.join(os.getcwd(), 'users')):
            os.mkdir(os.path.join(os.getcwd(), 'users'))
        if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}')):
            os.mkdir(os.path.join(os.getcwd(), 'users', f'{owner_id}'))
        if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json')):
            os.mkdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json'))
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_sort.json'), 'w',
                  encoding='utf-8') as file:
            json.dump(photo_geo, file, indent=4, ensure_ascii=False)
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_info.json'), 'w',
                  encoding='utf-8') as file:
            json.dump(user_info, file, indent=4, ensure_ascii=False)
        print(f"\n[+] Все данные получены и сохранены в папку: "
              f"{os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json')}")
    except ApiError as exc:
        print(f'\n{exc}')
        main()
        return

То есть, еще в самом начале, при запросе данных о количестве фото исключение сработает, и работа функции прекратиться. Мы будем перенаправлены в меню ввода id пользователя.


Получение адреса по координатам

В предыдущей функции использовалось обращение к функции получения адреса по координатам. Для этого используется модуль Nominatim библиотеки geopy.

Python:
# получение адреса локации фото
def get_addr(location):
    try:
        geoloc = Nominatim(user_agent="GetLoc")
        locname = geoloc.reverse(location)
        return locname.address
    except GeocoderUnavailable:
        return 'Unknown'

Здесь создается сессия, в которую передается user_agent. Затем, используется обратное геокодирование reverse, которое принимает координаты и возвращает адрес из базы. Также здесь мы обрабатываем исключение, если адреса в базе не окажется. Такое редко, но бывает. И возвращаем все это в функцию получения данных о фото.


Сортировка полученных данных по датам

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

Python:
# создание файлов разбитых по датам с координатами, адресами
# и ссылками для всплывающих подсказок карты
def create_dataset(owner_id):
    with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_sort.json'), 'r',
              encoding='utf-8') as file:
        sort_photo = json.load(file)

    date_temp = []
    coordinate_map = []
    for photo in sort_photo:
        if photo not in date_temp:
            for item in sort_photo:
                if dt.utcfromtimestamp(item['date']).strftime('%d.%m.%Y') == dt.utcfromtimestamp(photo['date']).\
                        strftime('%d.%m.%Y'):
                    date_temp.append(item)
                    popup = f'<a target="_blank" href="{item["url"]}">' \
                            f'{dt.utcfromtimestamp(item["date"]).strftime("%Y-%m-%d %H:%M:%S")}<br>' \
                            f'{item["lat"], item["long"]}<br>{item["address"]}</a>'
                    coordinate_map.append(f'{item["lat"], item["long"]}|{popup}')

        if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort')):
            os.mkdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort'))
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort',
                               f'{dt.utcfromtimestamp(photo["date"]).strftime("%Y-%m-%d")}.mp'),
                  'w', encoding='utf-8') as file:
            for crd in coordinate_map:
                file.write(f'{str(crd)}\n')

        date_temp = []
        coordinate_map = []
    user_change(owner_id)

Назвал я ее create_dataset(owner_id). В нее также передается id пользователя, и, как я и говорил ранее, с ним ведется работа во всем скрипте. Итак, открываем json с полученными и сохраненными данными в предыдущей функции.

Python:
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_sort.json'), 'r',
              encoding='utf-8') as file:
        sort_photo = json.load(file)

Создаем два списка. В одном будут храниться данные в чистом виде, они будут нужны для проверки, сортировали ли мы их или нет. И второй список, в который будут добавляться координаты и данные для всплывающей подсказки на основании отсортированных значений за определенную дату. Таким образом, если первый список будет содержать полные данные, то во втором будут содержаться только отсортированные.

Запускаем цикл по открытому нами json. Берем первый блок данных. Проверяем, есть ли этот блок в date_temp. Если нет, запускаем второй цикл, где уже получаем дату и сравниваем с той, что содержится в первом блоке данных. Таким образом, происходит выборка данных за определенную дату.

Python:
    date_temp = []
    coordinate_map = []
    for photo in sort_photo:
        if photo not in date_temp:
            for item in sort_photo:
                if dt.utcfromtimestamp(item['date']).strftime('%d.%m.%Y') == dt.utcfromtimestamp(photo['date']).\
                        strftime('%d.%m.%Y'):
                    date_temp.append(item)
                    popup = f'<a target="_blank" href="{item["url"]}">' \
                            f'{dt.utcfromtimestamp(item["date"]).strftime("%Y-%m-%d %H:%M:%S")}<br>' \
                            f'{item["lat"], item["long"]}<br>{item["address"]}</a>'
                    coordinate_map.append(f'{item["lat"], item["long"]}|{popup}')

После того, как сформирован список с данными для сохранения, то есть, в конце первой итерации по основному циклу, создаем папку для сохранения, если ее не существует, и в цикле перебираем собранный список, данные из которого записываем построчно в текстовый документ. Затем очищаем списки для новой итерации.

Python:
        if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort')):
            os.mkdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort'))
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort',
                               f'{dt.utcfromtimestamp(photo["date"]).strftime("%Y-%m-%d")}.mp'),
                  'w', encoding='utf-8') as file:
            for crd in coordinate_map:
                file.write(f'{str(crd)}\n')

        date_temp = []
        coordinate_map = []

В конце функции возвращаемся к пользовательскому выбору.

user_change(owner_id)

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

На этом первую часть дополнения нужно завершить.

Спасибо за внимание. Надеюсь, что данная информация будет вам полезна
 
Последнее редактирование:
  • Нравится
Реакции: And4R
Мы в соцсетях:

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