Работа с данными ВК / подсказка к 4 задачке

4

Доработать приложение из посдсказки, добавить возможность сдвигаться не только вперед по списку но и назад, ну и чтобы наша позиция в списке выводилась, как-то так:

Тут попробуем сделать так называемую пагинацию. Пагинация — это когда вы не выводите пользователю все данные сразу, а делаете это порциями, например, по 10 штук за раз.

Начну с базового приложения:

import requests 
from pprint import pprint
from tkinter import *
from tkinter import ttk
from PIL import Image, ImageTk 


class UI():
    def __init__(self, gui):  
        gui.geometry("400x430")
       
        
gui = Tk()
UI(gui)
gui.mainloop()

Будем снова использовать данные ВК, в этот раз попробуем вытянуть список подписчиков группы.

Для этого имеет метод https://vk.com/dev/groups.getMembers

Проскролим вниз и глянем какие данные нам возвращает этот метод

не очень интересно, то есть получается он возвращает количество участников в поле count и список их идентификаторов.

Но если в fields добавить какие-то дополнительные поля, то увидим уже что-то поинтереснее. Добавлю, например, картинку юзера:

Видно, что тут сразу появляется информация о имени/фамилии ну и нашей картинке в photo_max_orig

Добавим вызов этого метода в init

class UI():
    def __init__(self, gui):  
        gui.geometry("400x430")
        
        proxies = {
            "http": "http://172.27.100.5:4444",
            "https": "http://172.27.100.5:4444",
        }

        r = requests.get("https://api.vk.com/method/groups.getMembers", {
            "access_token": "203a983a203a983a203a983a58204daa522203a203a983a405dff18fea57bccc1c68d62",
            "v": "5.130",
            "group_id": "golos_irnitu", // группа
            "fields": "photo_max_orig", // добавили поле 
            "count": 10, // просим вернуть только 10 записей
            "offset": 0,  // начиная с нулевой
        }, proxies=proxies)
        
        data = r.json()
        pprint(data)

во, чего-то возвращает:

возможно вы заметили, что все имена возвращаются на английском, что там стали русские имена надо добавить параметр lang, вот так:

class UI():
    def __init__(self, gui):  
        gui.geometry("400x430")
        
        proxies = {
            "http": "http://172.27.100.5:4444",
            "https": "http://172.27.100.5:4444",
        }
        
        r = requests.get("https://api.vk.com/method/groups.getMembers", {
            "access_token": "203a983a203a983a203a983a58204daa522203a203a983a405dff18fea57bccc1c68d62",
            "v": "5.130",
            "group_id": "golos_irnitu",
            "fields": "photo_max_orig",
            "offset": 0,
            "count": 10,
            "lang": "ru",  # явно указываю язык
        }, proxies=proxies)
        
        data = r.json()
        pprint(data)

теперь попробуем вывести список студентов на форму:

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

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

class UI():
    def __init__(self, gui):  
        # ...
        
        data = r.json()
        pprint(data)
            
        items = data['response']['items'] # вытаскиваю список студентов из ответа от сервера
        
        # вытаскиваю фамилию и имя у первого студента
        first_name = items[0]['first_name']
        last_name = items[0]['last_name']
        # создаю 1-ый лейбл
        label = Label(text=f"{r['first_name']} {r['last_name']}")
        label.place(x=10, y=10)
        
        # вытаскиваю фамилию и имя у второго студента
        first_name = items[1]['first_name']
        last_name = items[1]['last_name']
        # создаю 2-ой лейбл
        label = Label(text=f"{r['first_name']} {r['last_name']}")
        label.place(x=10, y=40)
        
         # вытаскиваю фамилию и имя у второго студента
        first_name = items[2]['first_name']
        last_name = items[2]['last_name']
        # создаю 3-ий лейбл
        label = Label(text=f"{r['first_name']} {r['last_name']}")
        label.place(x=10, y=70)
        
        # и т.д.

по сути мне 10 раз надо написать 10 примерно одинаковых кусков кода. Чтобы этим не заниматься, можно воспользоваться циклом:

class UI():
    def __init__(self, gui):  
        gui.geometry("400x430")
        
        # ...
        
        data = r.json()
        pprint(data)
        
        items = data['response']['items']   # вытаскиваю список студентов из ответа от сервера
        
        y = 10 # начинаю с координаты y = 10
        for r in items:  # прохожу по списку студентов
            first_name = r['first_name'] # вытаскиваю имя
            last_name = r['last_name'] # фамилию
            label = Label(text=f"{first_name} {last_name}") # создаю лейбл
            label.place(x=10, y=y) # распололгаю его на форме
            y += 30 # увеличиваю значение координаты y на 30, это значение будет использоваться для следующего лейбла

проверяем:

класс! =)

Попробуем теперь добавить кнопку которая выведет нам следующие 10 юзеров

Добавим кнопку:

class UI():
    def __init__(self, gui):  
        gui.geometry("400x360")
        
        # ...
        
        y = 10
        for r in items:
            first_name = r['first_name']
            last_name = r['last_name']
            label = Label(text=f"{first_name} {last_name}")
            label.place(x=10, y=y)
            y += 30
            
        # добавил кнопку
        self.button = Button(text=" >> ", command=self.on_button_click)
        self.button.place(x=10, y=320)

    def on_button_click(self): # добави реакцию на кнопку
        print("Кликнули")

проверяем:

теперь надо понять как сделать чтобы что-то началось листать. Для этого взглянем на наш запрос

таким образом, чтобы вывести очередные 10 записей надо что бы при клике на кнопку сделался запрос с увеличенным значением offset.

Скопипастим код из __init__ в on_button_click

получится так:

class UI():
    def __init__(self, gui):  
         # ...

    def on_button_click(self):
        proxies = {
            "http": "http://172.27.100.5:4444",
            "https": "http://172.27.100.5:4444",
        }
        
        r = requests.get("https://api.vk.com/method/groups.getMembers", {
            "access_token": "203a983a203a983a203a983a58204daa522203a203a983a405dff18fea57bccc1c68d62",
            "v": "5.130",
            "group_id": "golos_irnitu",
            "fields": "photo_max_orig",
            "offset": 0,
            "count": 10,
            "lang": "ru",
        }, proxies=proxies)
        
        data = r.json()
        pprint(data)
        
        items = data['response']['items']
        
        y = 10
        for r in items:
            first_name = r['first_name']
            last_name = r['last_name']
            label = Label(text=f"{first_name} {last_name}")
            label.place(x=10, y=y)
            y += 30

заведем переменную в класс в которой будем хранить текущий сдвиг от начала списка

class UI():
    def __init__(self, gui):  
        gui.geometry("400x360")
        self.offset_from_start = 0 # добавил переменную со сдвигом, по началу тут ноль
        
        proxies = {
            "http": "http://172.27.100.5:4444",
            "https": "http://172.27.100.5:4444",
        }
        
        r = requests.get("https://api.vk.com/method/groups.getMembers", {
            "access_token": "203a983a203a983a203a983a58204daa522203a203a983a405dff18fea57bccc1c68d62",
            "v": "5.130",
            "group_id": "golos_irnitu",
            "fields": "photo_max_orig",
            "offset": self.offset_from_start, # <<< тут использую теперь переменную со сдвигом
            "count": 10,
            "lang": "ru",
        }, proxies=proxies)
        
        # ...

    def on_button_click(self):
        proxies = {
            "http": "http://172.27.100.5:4444",
            "https": "http://172.27.100.5:4444",
        }
        
        r = requests.get("https://api.vk.com/method/groups.getMembers", {
            "access_token": "203a983a203a983a203a983a58204daa522203a203a983a405dff18fea57bccc1c68d62",
            "v": "5.130",
            "group_id": "golos_irnitu",
            "fields": "photo_max_orig",
            "offset": self.offset_from_start, # <<< и тут теперь тоже теперь использую переменную со свдигом
            "count": 10,
            "lang": "ru",
        }, proxies=proxies)
        
       # ...

проверим, как работает:

хм, чет ничего не происходит…

А! Ну да! Мы же значение self.offset_from_start не меняем добавим в начало on_button_click пересчет значения:

    def on_button_click(self):
        self.offset_from_start += 10 # увеличим значение на 10
        
        # ну а дальше ничего не меняем 
        proxies = {
            "http": "http://172.27.100.5:4444",
            "https": "http://172.27.100.5:4444",
        }
        
        # ...

тестим:

работает! =)

только там иногда записи как будто налазят на предыдущие:

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

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

    def __init__(self, gui):  
        gui.geometry("400x360")
        self.offset_from_start = 0
        
        #...
        
        items = data['response']['items']
        
        self.labels = [] # добавляем список под лейблы
        
        y = 10
        for r in items:
            first_name = r['first_name']
            last_name = r['last_name']
            label = Label(text=f"{first_name} {last_name}")
            label.place(x=10, y=y)
            y += 30
            self.labels.append(label) # внутри цикла, после создания очередного лейбла заносим его в список
            
        # ...

затем аналогично подкручиваем on_button_click

    def on_button_click(self):
        self.offset_from_start += 10
   
        # ...
        
        items = data['response']['items']
        
        # добавляем новый цикл по лейблам
        for label in self.labels:
            # внутри удаляем каждый лейбл на форме
            label.destroy()
        
        # ну а тут по сути копипаста из __init__
        # то есть очищаем список 
        self.labels = []
        
        y = 10
        for r in items:
            first_name = r['first_name']
            last_name = r['last_name']
            label = Label(text=f"{first_name} {last_name}")
            label.place(x=10, y=y)
            y += 30
            self.labels.append(label) # и добавляем заполнение

тестим:

ну все красота! =)

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

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

class UI():
    # добавил новую функцию, тут почти весь код из on_button_click
    def get_members(self):
        proxies = {
            "http": "http://172.27.100.5:4444",
            "https": "http://172.27.100.5:4444",
        }
        
        r = requests.get("https://api.vk.com/method/groups.getMembers", {
            "access_token": "203a983a203a983a203a983a58204daa522203a203a983a405dff18fea57bccc1c68d62",
            "v": "5.130",
            "group_id": "golos_irnitu",
            "fields": "photo_max_orig",
            "offset": self.offset_from_start,
            "count": 10,
            "lang": "ru",
        }, proxies=proxies)
        
        data = r.json()
        pprint(data)
        
        items = data['response']['items']
        
        for label in self.labels:
            label.destroy()
        
        self.labels = []
        
        y = 10
        for r in items:
            first_name = r['first_name']
            last_name = r['last_name']
            label = Label(text=f"{first_name} {last_name}")
            label.place(x=10, y=y)
            y += 30
            
            self.labels.append(label)

теперь упростим код __init__:

class UI():
    def get_members(self):
        # ...
    
    def __init__(self, gui):  
        gui.geometry("400x360")
        self.offset_from_start = 0
        
        self.labels = [] # добавляем пустой список под labels, без этого работать не будет
        self.get_members() # тут просто все что было связано со стягивание данных заменяем на вызов нашей новой функции
            
        self.button = Button(text=" >> ", command=self.on_button_click)
        self.button.place(x=10, y=320)

    def on_button_click(self):
        self.offset_from_start += 10
   
        self.get_members() # и тут тоже заменяем весь код на вызов новой функции 

тестим:

работает же! =О

Ну все, можно задание теперь пилить =)