Статья Поиск в ВК фото с геометками с помощью Python. Часть #03.2 - Фильтрация по параметрам

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

03_2.jpg


0000.jpg


Просмотр фото за определенную дату

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

Python:
# ввод даты или нескольких дат пользователем
# для передачи после обработки в функцию чтения информации
def date_change(owner_id):
    if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort')):
        print('\n[+] Нет дат доступных для просмотра')
        user_change(owner_id)
        return
    else:
        files_dates = os.listdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort'))
        print(f'\n[+] Доступные даты:\n{"*"*64}')
        count = 0
        date_box = []
        for file in files_dates:
            date_print = f'{file.removesuffix(".mp").split("-")[2]}.{file.removesuffix(".mp").split("-")[1]}.' \
                         f'{file.removesuffix(".mp").split("-")[0]}'
            if count <= 5:
                print(f'{date_print} | ', end='')
                count += 1
            if count == 5:
                print('')
                count = 0
        print(f'\n{"*" * 64}')

    date_input = input('\n[+] Введите 1-ну или несколько дат через запятую (пример: 10.10.2020, 12.12.2021)\n  - '
                       'для просмотра всех дат введите: all\n  - для возврата к предыдущему меню введите: ex\n  >>> ')
    if date_input.lower() == "all":
        retrieve_all_date(owner_id)
        return
    elif date_input.lower() == "ex":
        user_change(owner_id)
        return
    else:
        date = date_input.split(",")
        date_resive = []
        for item in date:
            try:
                in_resive = f'{item.strip().split(".")[2]}-{item.strip().split(".")[1]}-{item.strip().split(".")[0]}'
                date_resive.append(in_resive)
            except IndexError:
                print('\n[+] Введены неверные даты или пропущена запятая. Попробуйте снова!')
                time.sleep(3)
                date_change(owner_id)
        read_date(owner_id, date_resive)

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

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

Python:
if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort')):
        print('\n[+] Нет дат доступных для просмотра')
        user_change(owner_id)
        return

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

Python:
else:
        files_dates = os.listdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort'))
        print(f'\n[+] Доступные даты:\n{"*"*64}')
        count = 0
        for file in files_dates:
            date_print = f'{file.removesuffix(".mp").split("-")[2]}.{file.removesuffix(".mp").split("-")[1]}.' \
                         f'{file.removesuffix(".mp").split("-")[0]}'
            if count <= 5:
                print(f'{date_print} | ', end='')
                count += 1
            if count == 5:
                print('')
                count = 0
        print(f'\n{"*" * 64}')

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

Затем обрабатываем пользовательский выбор.

Python:
    date_input = input('\n[+] Введите 1-ну или несколько дат через запятую (пример: 10.10.2020, 12.12.2021)\n  - '
                       'для просмотра всех дат введите: all\n  - для возврата к предыдущему меню введите: ex\n  >>> ')
    if date_input.lower() == "all":
        retrieve_all_date(owner_id)
        return
    elif date_input.lower() == "ex":
        user_change(owner_id)
        return
    else:
        date = date_input.split(",")
        date_resive = []

Если пользователь ввел «all», значит, запускаем функцию, которая считывает данные о файлах в наличии и передаст их дальше. Если ввел «ex», выходим в пользовательский выбор. Ну, а в противном случае, обрабатываем введенную пользователем дату. То есть, делим ввод по запятой. Если же он ввел одну дату, на функцию это никак не повлияем, так как ничего разделено не будет. Создаем список, в котором будем складировать даты для отправки. Нам их нужно слегка перевернуть вверх ногами. То есть, если пользователь ввел: 01.01.2020, у нас должно получиться 2020-01-01. Что мы и делаем ниже.

Python:
        for item in date:
            try:
                in_resive = f'{item.strip().split(".")[2]}-{item.strip().split(".")[1]}-{item.strip().split(".")[0]}'
                date_resive.append(in_resive)
            except IndexError:
                print('\n[+] Введены неверные даты или пропущена запятая. Попробуйте снова!')
                time.sleep(3)
                date_change(owner_id)
        read_date(owner_id, date_resive)

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


Получение всех сохраненных дат

Хотя бы данная функция получилась небольшая. Вот функция:

Python:
# чтение имен файлов для получения информации за все даты
# из папки с сохраненными датами
def retrieve_all_date(owner_id):
    if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort')):
        date_change(owner_id)
        return
    date_from_files = os.listdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort'))
    date_list = []
    for date in sorted(date_from_files):
        date_list.append(date.removesuffix('.mp'))
    read_date(owner_id, date_list)

Здесь мы проверяем, существует ли папка с датами. Если да, то сканируем директорию с помощью библиотеки os. Создаем список для дат. Ведь у полученного списка файлов надо удалить расширение, то есть суффикс. А после добавить уже в список. И этот список мы передаем в функцию чтения данных и добавления на карту.


Вычисление средней точки для отображения карты

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

Python:
# вычисление средних координат из полученных от пользователя дат
# для центрирования карты между координатами
def middle_coordinate(owner_id, dates):
    lat_m = 0
    long_m = 0
    coord_temp = []
    for item in sorted(dates):
        try:
            with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort', f'{item}.mp'), 'r',
                      encoding='utf-8') as f:
                data = f.readlines()
                for coord in data:
                    lat_m = lat_m + float(coord.split("|")[0].split(",")[0].replace("(", ""))
                    long_m = long_m + float(coord.split("|")[0].split(",")[1].replace(")", ""))
                lat_m = lat_m / len(data)
                long_m = long_m / len(data)
                coord_temp.append([lat_m, long_m])
                lat_m = 0
                long_m = 0
        except FileNotFoundError:
            print('\n[+] Введены неверные даты или пропущена запятая. Попробуйте снова!')
            time.sleep(3)
            date_change(owner_id)

    for item in coord_temp:
        lat_m = lat_m + item[0]
        long_m = long_m + item[1]
    lat_m = lat_m / len(coord_temp)
    long_m = long_m / len(coord_temp)
    return lat_m, long_m

Функция middle_coordinate(owner_id, dates) принимает на входе id пользователя и полученные даты, для которых нужно произвести расчет. Не знаю, может быть, можно было как-то сделать это проще. Но, к моему великому сожалению, не особо силен в математике, а потому пошел простым путем. Итак, для начала присваиваем переменным широты и долготы значение 0. Создаем временный список для координат. Затем открываем в цикле файлы дат, в которых хранятся нужные координаты. Берем первый файл, считываем значения построчно, затем делим по «|», берем первый элемент, именно тут хранятся у меня координаты. Делим по запятым, и заменяем ненужные нам знаки. После складываем полученные значения, предварительно переведя их во float. Делим на длину строк в файле. И добавляем в список координат. После чего присваиваем широте и долготе 0 и, если файлов несколько идем на следующую итерацию. После того, как все файлы будут прочитаны, обрабатываем список с координатами. Также в цикле складываем значения и делим на длину списка. Это и будут средние значения координат. Если я нигде не ошибся.


Расстановка точек на карте и сохранение данных

Теперь давайте рассмотрим функцию расстановки маркеров на карте по полученным из файлов дат координатам. Для начала полный код функции:

Python:
# чтение данных из сохраненных файлов и размещение точек на карте
# сохранение созданной карты в папке пользователя
def read_date(owner_id, dates):
    m_coordinate = middle_coordinate(owner_id, dates)
    m = Map(center=(m_coordinate[0], m_coordinate[1]), zoom=5, layout=Layout(width='100%', height='900px'))

    latlong_list = []
    for date in sorted(dates):
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort', f'{date}.mp'),
                  'r', encoding='utf-8') as f:
            data_file = f.readlines()
            marker_temp = []
            marker_popup = []
            for item in data_file:
                lat = float(item.split("|")[0].split(",")[0].replace("(", ""))
                long = float(item.split("|")[0].split(",")[1].replace(")", ""))
                marker_temp.append(Marker(location=(lat, long)))
                marker_popup.append(item.split("|")[1])
                latlong_list.append([lat, long])
            for num in range(0, len(marker_temp)):
                marker_temp[num].popup = HTML(marker_popup[num])
                m.add_layer(marker_temp[num])
            marker_temp.clear()
            marker_popup.clear()
    date_path = AntPath(locations=latlong_list, dash_array=[1, 10], delay=1000, color='#ff7000',
                        pulse_color='#9500ff')
    m.add_layer(date_path)

    legend = LegendControl({"Для увеличения: click ЛКМ 2 раза": "#A55",
                            "Для уменьшения: Shift click ЛКМ 2 раза": "#A55",
                            "Для увеличения участка: Shift+выделение": "#A55"}, name="Примечание", position="topright")
    m.add_control(legend)

    m.save(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}_date_points.html'),
           title='Moving an object within a specified date range')

    print(f"\n[+] Данные сохранены в папку: "
          f"{os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}_date_points.html')}\n")
    os.popen(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}_date_points.html'))
    date_change(owner_id)

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

Python:
    m_coordinate = middle_coordinate(owner_id, dates)
    m = Map(center=(m_coordinate[0], m_coordinate[1]), zoom=5, layout=Layout(width='100%', height='900px'))

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

Запускаем цикл по датам. Открываем файл для чтения построчно. Создаем два списка, marker_temp и marker_popup. В первый будем передавать скомпонованные маркеры, а во второй всплывающие подсказки, прочитанные из файла. Запускаем цикл по данному файлу, забираем координаты и помещаем их в созданный список, создаем маркер, в который передаем полученные координаты. А также забираем всплывающую подсказку и передаем с список подсказок.

Python:
    for date in sorted(dates):
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort', f'{date}.mp'),
                  'r', encoding='utf-8') as f:
            data_file = f.readlines()
            marker_temp = []
            marker_popup = []
            for item in data_file:
                lat = float(item.split("|")[0].split(",")[0].replace("(", ""))
                long = float(item.split("|")[0].split(",")[1].replace(")", ""))
                marker_temp.append(Marker(location=(lat, long)))
                marker_popup.append(item.split("|")[1])
                latlong_list.append([lat, long])

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

Python:
            for num in range(0, len(marker_temp)):
                marker_temp[num].popup = HTML(marker_popup[num])
                m.add_layer(marker_temp[num])
            marker_temp.clear()
            marker_popup.clear()

Таким образом, на объект карты будут нанесены все маркеры с координатами и подсказками, которые запросил пользователь. Затем создаем «муравьиный путь». В него нужно передать список списков, установить скорость анимации, цвет «пути» и «муравьев». Муравьи в данном контексте это пульсирующие точки на линии, которые создают иллюзию движения. После чего добавляем это на карту отдельным слоем.

Python:
    date_path = AntPath(locations=latlong_list, dash_array=[1, 10], delay=1000, color='#ff7000',
                        pulse_color='#9500ff')
    m.add_layer(date_path)

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

Python:
    legend = LegendControl({"Для увеличения: click ЛКМ 2 раза": "#A55",
                            "Для уменьшения: Shift click ЛКМ 2 раза": "#A55",
                            "Для увеличения участка: Shift+выделение": "#A55"}, name="Примечание", position="topright")
    m.add_control(legend)

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

Python:
m.save(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}_date_points.html'),
           title='Moving an object within a specified date range')

Название лучше писать на английском. Кириллица тут воспринимается некорректно.

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

Python:
    print(f"\n[+] Данные сохранены в папку: "
          f"{os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}_date_points.html')}\n")
    os.popen(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}_date_points.html'))
    date_change(owner_id)


Сохранение данных о пользователе в Word и открытие для просмотра

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

Python:
# чтение инофрмации о пользователе из файлов, создание документа word
# и сохранение его на диск в папку пользователя
def user_info_save(owner_id):
    if not os.path.isfile(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_info.json')):
        print('\n[+] Информации о запрашиваемом ID не найдено')
        user_change(owner_id)
        return

    if os.path.isfile(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt')):
        os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'))
    if os.path.isfile(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg')):
        os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'))

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

    user_list = []
    if user[0].get('first_name') is not None or user[0].get('first_name') != "":
        user_list.append(f'Имя: {user[0].get("first_name")}\n')
    if user[0].get('last_name') is not None or user[0].get('last_name') != "":
        user_list.append(f'Фамилия/Отчество: {user[0].get("last_name")}\n')
    if user[0].get('bdate') is not None or user[0].get('bdate') != "":
        user_list.append(f'Дата рождения: {user[0].get("bdate")}\n')
    if user[0].get('country').get('title') is not None or user[0].get('country').get('title') != "":
        user_list.append(f'Страна: {user[0].get("country").get("title")}\n')
    if user[0].get('city').get('title') is not None or user[0].get('city').get('title') != "":
        user_list.append(f'Город: {user[0].get("city").get("title")}\n')
    user_list.append(f'Профиль ВК: https://vk.com/id{owner_id}\n')
    for u_item in user_list:
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'), 'a',
                  encoding='utf-8') as file:
            file.write(u_item)

    if user[0].get('photo_max_orig') is not None or user[0].get('photo_max_orig') != "":
        req = get(url=user[0].get('photo_max_orig')).content
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'), 'wb') as file:
            file.write(req)
    doc = docx.Document()
    style = doc.styles['Normal']
    style.font.name = 'Arial'
    style.font.size = Pt(11)
    doc.add_picture(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'),
                    width=docx.shared.Cm(5))
    os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'))

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

    loc_list = ['\n\nНайденные локации\n\n']
    for loc in loc_info:
        loc_list.append(f"Дата локации: {dt.utcfromtimestamp(loc['date']).strftime('%d.%m.%Y %H:%M:%S')}\n")
        loc_list.append(f"Координаты: {loc['lat']}, {loc['long']}\n")
        loc_list.append(f"Адрес: {loc['address']}\n")
        loc_list.append(f"Ссылка на фото локации: {loc['url']}\n\n")
    for item in loc_list:
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'), 'a',
                  encoding='utf-8') as file:
            file.write(f'{item}')

    with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'), 'r',
              encoding='utf-8') as file:
        temp_txt = file.read()
    doc.add_paragraph(temp_txt)

    doc.save(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}.docx'))
    os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'))
    os.popen(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}.docx'))
    user_change(owner_id)

Я назвал функцию user_info_save(owner_id). На входе она принимает только id пользователя. Перед тем, как читать данные, надо проверить, существуют ли они вообще. Поэтому, проверяем, есть ли файл по запрашиваемому пользователю для обработки. Если нет, сообщаем об этом и выходим в функцию пользовательского выбора.

Python:
    if not os.path.isfile(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_info.json')):
        print('\n[+] Информации о запрашиваемом ID не найдено')
        user_change(owner_id)
        return

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

Python:
    if os.path.isfile(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt')):
        os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'))
    if os.path.isfile(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg')):
        os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'))

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

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

Python:
    user_list = []
    if user[0].get('first_name') is not None or user[0].get('first_name') != "":
        user_list.append(f'Имя: {user[0].get("first_name")}\n')
    if user[0].get('last_name') is not None or user[0].get('last_name') != "":
        user_list.append(f'Фамилия/Отчество: {user[0].get("last_name")}\n')
    if user[0].get('bdate') is not None or user[0].get('bdate') != "":
        user_list.append(f'Дата рождения: {user[0].get("bdate")}\n')
    if user[0].get('country').get('title') is not None or user[0].get('country').get('title') != "":
        user_list.append(f'Страна: {user[0].get("country").get("title")}\n')
    if user[0].get('city').get('title') is not None or user[0].get('city').get('title') != "":
        user_list.append(f'Город: {user[0].get("city").get("title")}\n')
    user_list.append(f'Профиль ВК: https://vk.com/id{owner_id}\n')

Затем перебираем список полученных значений и сохраняем по одному значению на строку в текстовый временный файл.

Python:
    for u_item in user_list:
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'), 'a',
                  encoding='utf-8') as file:
            file.write(u_item)

Теперь скачиваем фото пользователя, так как одно из значений в файле, который мы получили, это ссылка на большое фото. Если оно есть, то загружаем его в папку для добавления в документ. Создаем объект документа word, добавляем к нему стили, шрифт и его размер.

Python:
    doc = docx.Document()
    style = doc.styles['Normal']
    style.font.name = 'Arial'
    style.font.size = Pt(11)

Затем добавляем картинку в документ, где параметр width определяет ее размер в сантиметрах. После добавления картинки удаляем ее за ненадобностью.

Python:
    doc.add_picture(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'),
                    width=docx.shared.Cm(5))
    os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'))

После открываем файл, в который мы сохраняли все найденные фото со ссылками и координатами.

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:
        loc_info = json.load(file)

Создаем список локаций, куда в цикле добавляем все найденные нами локации.

Python:
    loc_list = ['\n\nНайденные локации\n\n']
    for loc in loc_info:
        loc_list.append(f"Дата локации: {dt.utcfromtimestamp(loc['date']).strftime('%d.%m.%Y %H:%M:%S')}\n")
        loc_list.append(f"Координаты: {loc['lat']}, {loc['long']}\n")
        loc_list.append(f"Адрес: {loc['address']}\n")
        loc_list.append(f"Ссылка на фото локации: {loc['url']}\n\n")

После обработки файла нужно добавить все данные из списка в текстовый документ. Поэтому запускаем цикл по списку с локациями и добавляем в txt.

Python:
    for item in loc_list:
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'), 'a',
                  encoding='utf-8') as file:
            file.write(f'{item}')

Открываем только что созданный текстовый документ и добавляем его содержимое в word. Таким образом, получается, обойти создание гигантских абзацев между строками.

Python:
    with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'), 'r',
              encoding='utf-8') as file:
        temp_txt = file.read()
    doc.add_paragraph(temp_txt)

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

Python:
    doc.save(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}.docx'))
    os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'))
    os.popen(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}.docx'))
    user_change(owner_id)

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

Python:
# pip install vk-api
# pip install ipyleaflet
# pip install geopy
# pip install python-docx
# pip install requests

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


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


# получение информации о фото, пользователе
# сохранение информации в файлы
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


# создание файлов разбитых по датам с координатами, адресами
# и ссылками для всплывающих подсказок карты
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)


# вычисление средних координат из полученных от пользователя дат
# для центрирования карты между координатами
def middle_coordinate(owner_id, dates):
    lat_m = 0
    long_m = 0
    coord_temp = []
    for item in sorted(dates):
        try:
            with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort', f'{item}.mp'), 'r',
                      encoding='utf-8') as f:
                data = f.readlines()
                for coord in data:
                    lat_m = lat_m + float(coord.split("|")[0].split(",")[0].replace("(", ""))
                    long_m = long_m + float(coord.split("|")[0].split(",")[1].replace(")", ""))
                lat_m = lat_m / len(data)
                long_m = long_m / len(data)
                coord_temp.append([lat_m, long_m])
                lat_m = 0
                long_m = 0
        except FileNotFoundError:
            print('\n[+] Введены неверные даты или пропущена запятая. Попробуйте снова!')
            time.sleep(3)
            date_change(owner_id)

    for item in coord_temp:
        lat_m = lat_m + item[0]
        long_m = long_m + item[1]
    lat_m = lat_m / len(coord_temp)
    long_m = long_m / len(coord_temp)
    return lat_m, long_m


# чтение данных из сохраненных файлов и размещение точек на карте
# сохранение созданной карты в папке пользователя
def read_date(owner_id, dates):
    m_coordinate = middle_coordinate(owner_id, dates)
    m = Map(center=(m_coordinate[0], m_coordinate[1]), zoom=5, layout=Layout(width='100%', height='900px'))

    latlong_list = []
    for date in sorted(dates):
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort', f'{date}.mp'),
                  'r', encoding='utf-8') as f:
            data_file = f.readlines()
            marker_temp = []
            marker_popup = []
            for item in data_file:
                lat = float(item.split("|")[0].split(",")[0].replace("(", ""))
                long = float(item.split("|")[0].split(",")[1].replace(")", ""))
                marker_temp.append(Marker(location=(lat, long)))
                marker_popup.append(item.split("|")[1])
                latlong_list.append([lat, long])

            for num in range(0, len(marker_temp)):
                marker_temp[num].popup = HTML(marker_popup[num])
                m.add_layer(marker_temp[num])
            marker_temp.clear()
            marker_popup.clear()

    date_path = AntPath(locations=latlong_list, dash_array=[1, 10], delay=1000, color='#ff7000',
                        pulse_color='#9500ff')
    m.add_layer(date_path)

    legend = LegendControl({"Для увеличения: click ЛКМ 2 раза": "#A55",
                            "Для уменьшения: Shift click ЛКМ 2 раза": "#A55",
                            "Для увеличения участка: Shift+выделение": "#A55"}, name="Примечание", position="topright")
    m.add_control(legend)

    m.save(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}_date_points.html'),
           title='Moving an object within a specified date range')

    print(f"\n[+] Данные сохранены в папку: "
          f"{os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}_date_points.html')}\n")
    os.popen(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}_date_points.html'))
    date_change(owner_id)


# чтение имен файлов для получения информации за все даты
# из папки с сохраненными датами
def retrieve_all_date(owner_id):
    if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort')):
        date_change(owner_id)
        return
    date_from_files = os.listdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort'))
    date_list = []
    for date in sorted(date_from_files):
        date_list.append(date.removesuffix('.mp'))
    read_date(owner_id, date_list)


# чтение инофрмации о пользователе из файлов, создание документа word
# и сохранение его на диск в папку пользователя
def user_info_save(owner_id):
    if not os.path.isfile(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_info.json')):
        print('\n[+] Информации о запрашиваемом ID не найдено')
        user_change(owner_id)
        return

    if os.path.isfile(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt')):
        os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'))
    if os.path.isfile(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg')):
        os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'))

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

    user_list = []
    if user[0].get('first_name') is not None or user[0].get('first_name') != "":
        user_list.append(f'Имя: {user[0].get("first_name")}\n')
    if user[0].get('last_name') is not None or user[0].get('last_name') != "":
        user_list.append(f'Фамилия/Отчество: {user[0].get("last_name")}\n')
    if user[0].get('bdate') is not None or user[0].get('bdate') != "":
        user_list.append(f'Дата рождения: {user[0].get("bdate")}\n')
    if user[0].get('country').get('title') is not None or user[0].get('country').get('title') != "":
        user_list.append(f'Страна: {user[0].get("country").get("title")}\n')
    if user[0].get('city').get('title') is not None or user[0].get('city').get('title') != "":
        user_list.append(f'Город: {user[0].get("city").get("title")}\n')
    user_list.append(f'Профиль ВК: https://vk.com/id{owner_id}\n')

    for u_item in user_list:
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'), 'a',
                  encoding='utf-8') as file:
            file.write(u_item)

    if user[0].get('photo_max_orig') is not None or user[0].get('photo_max_orig') != "":
        req = get(url=user[0].get('photo_max_orig')).content
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'), 'wb') as file:
            file.write(req)

    doc = docx.Document()
    style = doc.styles['Normal']
    style.font.name = 'Arial'
    style.font.size = Pt(11)

    doc.add_picture(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'),
                    width=docx.shared.Cm(5))
    os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}.jpg'))

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

    loc_list = ['\n\nНайденные локации\n\n']
    for loc in loc_info:
        loc_list.append(f"Дата локации: {dt.utcfromtimestamp(loc['date']).strftime('%d.%m.%Y %H:%M:%S')}\n")
        loc_list.append(f"Координаты: {loc['lat']}, {loc['long']}\n")
        loc_list.append(f"Адрес: {loc['address']}\n")
        loc_list.append(f"Ссылка на фото локации: {loc['url']}\n\n")

    for item in loc_list:
        with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'), 'a',
                  encoding='utf-8') as file:
            file.write(f'{item}')

    with open(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'), 'r',
              encoding='utf-8') as file:
        temp_txt = file.read()
    doc.add_paragraph(temp_txt)

    doc.save(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}.docx'))
    os.remove(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_json', f'{owner_id}_user.txt'))
    os.popen(os.path.join(os.getcwd(), 'users', f'{owner_id}', f'{owner_id}.docx'))
    user_change(owner_id)


# ввод даты или нескольких дат пользователем
# для передачи после обработки в функцию чтения информации
def date_change(owner_id):
    if not os.path.isdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort')):
        print('\n[+] Нет дат доступных для просмотра')
        user_change(owner_id)
        return
    else:
        files_dates = os.listdir(os.path.join(os.getcwd(), 'users', f'{owner_id}', 'data_sort'))
        print(f'\n[+] Доступные даты:\n{"*" * 64}')
        count = 0
        for file in files_dates:
            date_print = f'{file.removesuffix(".mp").split("-")[2]}.{file.removesuffix(".mp").split("-")[1]}.' \
                         f'{file.removesuffix(".mp").split("-")[0]}'
            if count <= 5:
                print(f'{date_print} | ', end='')
                count += 1
            if count == 5:
                print('')
                count = 0
        print(f'\n{"*" * 64}')

    date_input = input('\n[+] Введите 1-ну или несколько дат через запятую (пример: 10.10.2020, 12.12.2021)\n  - '
                       'для просмотра всех дат введите: all\n  - для возврата к предыдущему меню введите: ex\n  >>> ')
    if date_input.lower() == "all":
        retrieve_all_date(owner_id)
        return
    elif date_input.lower() == "ex":
        user_change(owner_id)
        return
    else:
        date = date_input.split(",")
        date_resive = []

        for item in date:
            try:
                in_resive = f'{item.strip().split(".")[2]}-{item.strip().split(".")[1]}-{item.strip().split(".")[0]}'
                date_resive.append(in_resive)
            except IndexError:
                print('\n[+] Введены неверные даты или пропущена запятая. Попробуйте снова!')
                time.sleep(3)
                date_change(owner_id)
        read_date(owner_id, date_resive)


# запрос и обработка пользовательского ввода
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 пользователя и передача в функцию ввода действий пользователя
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!')


if __name__ == "__main__":
    main()


Выводы за время написания скрипта

К чему я пришел? Тут все просто. В том виде, в котором существует данный скрипт, он вполне работоспособен. Но вот для масштабирования совершенно не пригоден. И если бы я начал писать все с начала, то для хранения данных пользователей и данных о фото использовал бы уже базу данных. Хотя бы sqlite.

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

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

Результаты работы программы. На скриншоте показаны даты, по которым отсортированы полученные фото с геометками.

screenshot1.png

Метки на карте с координатами, датой и адресом, а также линия, которая может показать передвижение пользователя в течение времени.

screenshot2.png

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

screenshot3.png

Что ж, на этом, пожалуй, все.

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