Перейти к содержанию

Работа с API ВКонтакте

Эта работа посвящена основам анализа социальных сетей на примере взаимодействия с API ВКонтакте.

Note

Если вы не знакомы с термином «API», то рекомендую прочитать статью «What is an API? In English, please».

Регистрация приложения и получение токена доступа

Чтобы начать работать с API от вас требуется зарегистрировать новое приложение. Для этого зайдите на форму создания нового Standalone приложения https://vk.com/editapp?act=create и следуйте инструкциям. Вашему приложению будет назначен идентификатор, который потребуется для выполнения работы.

Запросы к API ВКонтакте имеют следующий формат (из документации):

https://api.vk.com/method/METHOD_NAME?PARAMETERS&access_token=ACCESS_TOKEN&v=V

где:

  • METHOD_NAME - это название метода API, к которому Вы хотите обратиться (например, получить список друзей).
  • PARAMETERS - входные параметры соответствующего метода API, последовательность пар name=value, объединенных амперсандом & (например, количество друзей).
  • ACCESSS_TOKEN - ключ доступа.
  • V - используемая версия API.

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

https://api.vk.com/method/friends.get?fields=sex&access_token=0394a2ede332c9a13eb82e9b24631604c31df978b4e2f0fbd2c549944f9d79a5bc866455623bd560732ab&v=5.126

Так как токен доступа (access_token) ненастоящий, то этот запрос работать не будет. Чтобы получить токен доступа вы можете воспользоваться написанным для вас скриптом access_token.py следующим образом:

$ python access_token.py YOUR_CLIENT_ID -s friends

где вместо YOUR_CLIENT_ID необходимо подставить идентификатор вашего приложения.

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

Note

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

Далее приведено содержимое файла access_token.py:

import webbrowser
import argparse


def get_access_token(client_id: int, scope: int) -> None:
    assert isinstance(client_id, int), 'clinet_id must be positive integer'
    assert isinstance(scope, str), 'scope must be string'
    assert client_id > 0, 'clinet_id must be positive integer'
    url = """\
    https://oauth.vk.com/authorize?client_id={client_id}&\
    redirect_uri=https://oauth.vk.com/blank.hmtl&\
    scope={scope}&\
    &response_type=token&\
    display=page\
    """.replace(" ", "").format(client_id=client_id, scope=scope)
    webbrowser.open_new_tab(url)


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("client_id", help="Application Id", type=int)
    parser.add_argument("-s", dest="scope", help="Permissions bit mask", type=str, default="", required=False)
    args = parser.parse_args()
    get_access_token(args.client_id, args.scope)

Полученный токен доступа вставьте в поле access_token файле vkapi/config.py:

VK_CONFIG = {
    "domain": "https://api.vk.com/method",
    "access_token": "",
    "version": "5.124",
}

Прогнозирования возраста

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

Для выполнения запросов к API мы будем использовать библиотеку requests (для выполнения запросов в асинхронном режиме можно использовать библиотеку httpx):

(cs102) $ python -m pip install requests

Список пользователей можно получить с помощью метода friends.get. Ниже приведен пример обращения к этому методу для получения списка всех друзей указанного пользователя:

domain = VK_CONFIG["domain"]
access_token = VK_CONFIG["acceess_token"]
v = VK_CONFIG["version"]
user_id = # PUT USER ID HERE
fields = 'sex'

query = f"{domain}/friends.get?access_token={access_token}&user_id={user_id}&fields={fields}&v={v}"
response = requests.get(query)

Функция requests.get выполняет GET запрос и возвращает объект Response, который представляет собой ответ сервера на посланный нами запрос.

Объект Response имеет множество атрибутов:

>>> response.<tab>
response.apparent_encoding      response.history                response.raise_for_status
response.close                  response.is_permanent_redirect  response.raw
response.connection             response.is_redirect            response.reason
response.content                response.iter_content           response.request
response.cookies                response.iter_lines             response.status_code
response.elapsed                response.json                   response.text
response.encoding               response.links                  response.url
response.headers                response.ok

Нас будет интересовать только метод response.json, который возвращает JSON объект:

>>> response.json()
{'response': {'count': 136,
              'items': [{'first_name': 'Drake',
                         'id': 1234567,
                         'last_name': 'Wayne',
                         'online': 0,
                         'sex': 1},
                        {'first_name': 'Gracie'
                         'id': 7654321,
                         'last_name': 'Habbard',
                         'online': 0,
                         'sex': 0},
                         ...
>>> response.json()['response']['count']
136
>>> response.json()['response']['items'][0]['first_name']
'Drake'

Поле count содержит число записей, а items список словарей с информацией по каждому пользователю.

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

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

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

Note

Описание алгоритма с примерами можно найти в статье Exponential Backoff или как «не завалить сервер». Почитать про обработку исключений при работе с библиотекой requests можно тут.

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

class Session(requests.Session):
    """
    Сессия.

    :param base_url: Базовый адрес, на который будут выполняться запросы.
    :param timeout: Максимальное время ожидания ответа от сервера.
    :param max_retries: Максимальное число повторных запросов.
    :param backoff_factor: Коэффициент экспоненциального нарастания задержки.
    """

    def __init__(
        self,
        base_url: str,
        timeout: float = 5.0,
        max_retries: int = 3,
        backoff_factor: float = 0.3,
    ) -> None:
        pass

    def get(self, url, **kwargs: tp.Any) -> requests.Response:
        pass

    def post(self, url, data=None, json=None, **kwargs: tp.Any) -> requests.Response:
        pass

Note

Описание лучших практик при использовании модуля requests можно найти тут.

Примеры использования:

>>> s = Session(base_url="https://httpbin.org", timeout=3)
>>> s.get("get")
<Response [200]>
>>> s.get("delay/2")
<Response [200]>
>>> s.get("delay/2", timeout=1)
ReadTimeoutError: HTTPSConnectionPool(host='httpbin.org', port=443): Read timed out. (read timeout=1)
>>> s.get("status/500")
RetryError: HTTPSConnectionPool(host='httpbin.org', port=443): Max retries exceeded with url: /status/500 (Caused by ResponseError('too many 500 error responses'))

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

@dataclasses.dataclass(frozen=True)
class FriendsResponse:
    """
    Ответ на вызов метода `friends.get`.

    :param count: Количество пользователей.
    :param items: Список идентификаторов друзей пользователя или список пользователей.
    """
    count: int
    items: tp.Union[tp.List[int], tp.List[tp.Dict[str, tp.Any]]]


def get_friends(
    user_id: int, count: int = 5000, offset: int = 0, fields: tp.Optional[tp.List[str]] = None
) -> FriendsResponse:
    """
    Получить список идентификаторов друзей пользователя или расширенную информацию
    о друзьях пользователя (при использовании параметра fields).

    :param user_id: Идентификатор пользователя, список друзей для которого нужно получить.
    :param count: Количество друзей, которое нужно вернуть.
    :param offset: Смещение, необходимое для выборки определенного подмножества друзей.
    :param fields: Список полей, которые нужно получить для каждого пользователя.
    :return: Объект класса FriendsResponse.
    """
    pass

Теперь мы можем написать функцию age_predict для «наивного» прогнозирования возраста пользователя с идентификатором user_id (под «наивным» прогнозированием подразумевается вычисление среднего арифметического или медианы):

def age_predict(user_id: int) -> tp.Optional[float]:
    """
    Наивный прогноз возраста пользователя по возрасту его друзей.

    Возраст считается как медиана среди возраста всех друзей пользователя.

    :param user_id: Идентификатор пользователя.
    :return: Медианный возраст пользователя.
    """
    pass

Hint

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

Построение социального графа

Одной из задач при анализе социальных сетей является построение и анализ социального графа, то есть графа, «узлы которого представлены социальными объектами, такими как пользовательские профили с различными атрибутами, сообщества, медиаконтента и так далее, а рёбра — социальными связями между ними». Мы построим одну из разновидностей социального графа - эгоцентричный граф или граф друзей. Обычно под эгоцентричным графом понимают граф, в котором устанавливаются связи между друзьями некоторого пользователя. Для этого вам потребуется делать запросы к методу friends.getMutual, который позволяет получить список общих друзей между парой пользователей:

class MutualFriends(tp.TypedDict):
    id: int
    common_friends: tp.List[int]
    common_count: int


def get_mutual(
    source_uid: tp.Optional[int] = None,
    target_uid: tp.Optional[int] = None,
    target_uids: tp.Optional[tp.List[int]] = None,
    order: str = "",
    count: tp.Optional[int] = None,
    offset: int = 0,
    progress=None,
) -> tp.Union[tp.List[int], tp.List[MutualFriends]]:
    """
    Получить список идентификаторов общих друзей между парой пользователей.

    :param source_uid: Идентификатор пользователя, чьи друзья пересекаются с друзьями пользователя с идентификатором target_uid.
    :param target_uid: Идентификатор пользователя, с которым необходимо искать общих друзей.
    :param target_uids: Cписок идентификаторов пользователей, с которыми необходимо искать общих друзей.
    :param order: Порядок, в котором нужно вернуть список общих друзей.
    :param count: Количество общих друзей, которое нужно вернуть.
    :param offset: Смещение, необходимое для выборки определенного подмножества общих друзей.
    :param progress: Callback для отображения прогресса.
    """
    pass

Одним из ограничений метода friends.getMutual является ограничение на максимальное количество идентификаторов пользователей, с которыми необходимо искать общих друзей, то есть target_uids должен содержать не более 100 идентификаторов. Ваша реализация не должна ограничивать размер этого списка, но учитывать, что к методам API ВК с ключом доступа пользователя или сервисным ключом доступа можно обращаться не чаще 3-х раз в секунду:

>>> from tqdm import tqdm
>>> friends_response = get_friends(user_id=817934, fields=["nickname"])
>>> active_users = [user["id"] for user in friends_response.items if not user.get("deactivated")]
>>> len(active_users)
136
>>> mutual_friends = get_mutual(source_id=817934, target_uids=active_users, progress=tqdm)
100%|██████████████████████████████████████████████████| 2/2 [00:00<00:00, 13.37it/s]

В приведенном примере мы оставляем только активных пользователей, фильтруя их по полю deactivated, чтобы не получить ошибку User was deleted or banned, а затем получаем список общих пользователей за два запроса.

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

def ego_network(
    user_id: tp.Optional[int] = None, friends: tp.Optional[tp.List[int]] = None
) -> tp.List[tp.Tuple[int, int]]:
    """
    Построить эгоцентричный граф друзей.

    :param user_id: Идентификатор пользователя, для которого строится граф друзей.
    :param friends: Идентификаторы друзей, между которыми устанавливаются связи.
    """
    pass

Пример использования:

>>> net = ego_network(user_id=817934)
>>> net[:5]
[(4181, 4206848), (4181, 403284), (4734, 262371), (4734, 251463), (4734, 740914)]
>>> plot_ego_network(net)

Очевидно, что в полученном графе есть сообщества (в качестве примеров сообществ можно привести: школьных и университетских друзей, коллег по работе, членов семьи и т.д.). Поиск сообществ на графе (community detection) является хорошо изученной задачей, а ряд наиболее известных алгоритмов реализован в библиотеке community:

>>> net = ego_network(user_id=817934)
>>> plot_communities(net)

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

>>> communities = get_communities(net)
>>> describe_communities(communities, friends_response.items, fields=["first_name", "last_name"])
   cluster  first_name    last_name
0        0       Антон      Антонов
1        0     Алексей       Кнорре
2        0    Владимир  Волохонский
...

Тематическое моделирование

Тематическое моделирование (topic modeling) одно из современных приложений машинного обучения к анализу текстов, активно развивающееся с конца 90-х годов. Тематическая модель (topic model) коллекции текстовых документов определяет, к каким темам относится каждый документ и какие слова (термины) образуют каждую тему.

Данные для построения тематической модели мы будем собирать со стен различных групп, используя метод wall.get. Аналогично методу friends.getMutual он не позволяет получить более 100 записей за один запрос, таким образом, мы имеем ограничение в 300 постов за секунду. Поэтому для более эффективного сбора сообщений со стен групп мы будем использовать метод execute - «универсальный метод, который позволяет запускать последовательность других методов, сохраняя и фильтруя промежуточные результаты». Параметром в метод execute является код алгоритма в VKScript, который позволяет выполнить до 25 обращений к методам API. Таким образом, мы получим возможность выполнять до 75 обращений в секунду, то есть, 7500 постов. К сожалению есть ограничения на размер тела ответа и если посты слишком большие, то высока вероятность получить ошибку Runtime error occurred during code invocation: response size is too big.

Ниже приведен пример всех необходимых конструкций (дополнительно про VKScript можно почитать тут):

var число = 1.234;
var массив_элементов = [];

if (условие) {
    список выражений;
}

while (условие) {
    тело цикла;
}

var doc = API.метод({параметры});
var значения = doc.ключ;

Пример запроса с использованием execute:

code = """return API.wall.get({
    "owner_id": "",
    "domain": "itmoru",
    "offset": 0,
    "count": 1,
    "filter": "owner",
    "extended": 0,
    "fields": "",
    "v": "5.126"
});"""

response = requests.post(
    url="https://api.vk.com/method/execute",
        data={
            "code": code,
            "access_token": "",  # PUT YOUR ACCESS TOKEN HERE
            "v": "5.126"
        }
)

Пример ответа:

>>> response.json()
{'response': {'count': 7601, 'items': [{'id': 42847, 'from_id': -94, 'owner_id': -94, 'date': 1572798277, 'marked_as_ads': 0, 'post_type': 'post', 'text': "ЗОЛОТО! 🏆\n\nКлим Гаврилов, студент 2 курса ИТМО, выиграл первые автоспортивные Олимпийские игры в классе Туринг! Поздравляем, это было очень круто 💪🏻\n\nПобеда Клима....

Напишите функцию get_wall_execute, которая собирает записи со стены указанного сообщества:

import pandas as pd
import requests
import textwrap

from pandas.io.json import json_normalize
from string import Template
from tqdm import tqdm


def get_wall_execute(
    owner_id: str = "",
    domain: str = "",
    offset: int = 0,
    count: int = 10,
    filter: str = "owner",
    extended: str = "",
    fields: str = "",
    v: str = "5.126",
    progress=None,
) -> pd.DataFrame:
    """
    Возвращает список записей со стены пользователя или сообщества.

    @see: https://vk.com/dev/wall.get 

    :param owner_id: Идентификатор пользователя или сообщества, со стены которого необходимо получить записи.
    :param domain: Короткий адрес пользователя или сообщества.
    :param offset: Смещение, необходимое для выборки определенного подмножества записей.
    :param count: Количество записей, которое необходимо получить (0 - все записи).
    :param filter: Определяет, какие типы записей на стене необходимо получить.
    :param extended: 1 — в ответе будут возвращены дополнительные поля profiles и groups, содержащие информацию о пользователях и сообществах.
    :param fields: Список дополнительных полей для профилей и сообществ, которые необходимо вернуть.
    :param v: Версия API.
    :param progress: Callback для отображения прогресса.
    """
    # PUT YOUR CODE HERE
    pass

Текст необходимо подготовить к построению тематической модели, а именно, удалить знаки пунктуации, ссылки, эмодзи, стоп-слова, провести нормализацию.

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

(cs102) $ python -m pip install pymorphy2

В качестве тематической модели мы будем использовать модель латентного размещения Дирихле (LDA, Latent Dirichlet Allocation), реализацию которой можно найти в библиотеке gensim.

(cs102) $ python -m pip install gensim

Последнее обновление: 25 июня 2020 г.

Комментарии