Лабораторная работа №4. Работа с API ВКонтакте

Эта работа посвящена взаимодействию с API ВКонтакте.

Замечание: Если вы не знакомы с термином "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 (в настоящий момент 5.53).

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

https://api.vk.com/method/friends.get?fields=sex&access_token=0394a2ede332c9a13eb82e9b24631604c31df978b4e2f0fbd2c549944f9d79a5bc866455623bd560732ab&v=5.53
Внимание: Токен доступа ненастоящий, поэтому этот запрос работать не будет.

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

$ python access_token.py YOUR_CLIENT_ID -s friends,messages

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

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

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

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

import webbrowser
import argparse


def get_access_token(client_id, scope):
    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)

Задание №1. Требуется написать функцию прогнозирования возраста пользователя по возрасту его друзей.

def get_friends(user_id, fields):
    """ Returns a list of user IDs or detailed information about a user's friends """
    assert isinstance(user_id, int), "user_id must be positive integer"
    assert isinstance(fields, str), "fields must be string"
    assert user_id > 0, "user_id must be positive integer"
    # PUT YOUR CODE HERE
    pass

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

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

$ pip install requests

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

domain = "https://api.vk.com/method"
access_token = # PUT YOUR ACCESS TOKEN HERE
user_id = # PUT USER ID HERE

query_params = {
    'domain' : domain,
    'access_token': access_token,
    'user_id': user_id,
    'fields': 'sex'
}

query = "{domain}/friends.get?access_token={access_token}&user_id={user_id}&fields={fields}&v=5.53".format(**query_params)
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 список словарей с информацией по каждому пользователю.

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

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

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

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

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

def get(url, params={}, timeout=5, max_retries=5, backoff_factor=0.3):
    """ Выполнить GET-запрос

    :param url: адрес, на который необходимо выполнить запрос
    :param params: параметры запроса
    :param timeout: максимальное время ожидания ответа от сервера
    :param max_retries: максимальное число повторных запросов
    :param backoff_factor: коэффициент экспоненциального нарастания задержки
    """
    # PUT YOUR CODE HERE
    pass
>>> get("https://httpbin.org/get")
>>> <Response [200]>

>>> get("https://httpbin.org/delay/2", timeout=1)
ReadTimeout: HTTPSConnectionPool(host='httpbin.org', port=443): Read timed out. (read timeout=1)

>>> get("https://httpbin.org/status/500")
HTTPError: 500 Server Error: INTERNAL SERVER ERROR for url: https://httpbin.org/status/500

>>> get("https://noname.com", timeout=1)
ConnectionError: HTTPSConnectionPool(host='noname.com', port=443): Max retries exceeded with url: /

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

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

def age_predict(user_id):
    """
    >>> age_predict(???)
    ???
    """
    assert isinstance(user_id, int), "user_id must be positive integer"
    assert user_id > 0, "user_id must be positive integer"
    # PUT YOUR CODE HERE
    pass
Замечание: Так как дата рождения пользователя может быть не указана или указаны только день и месяц, то для обработки таких ситуаций вы можете использовать конструкцию try...except, где except будет содержать только pass.

Задание №2: Требуется написать функцию, которая бы выводила график частоты переписки с указанным пользователем.

Давайте начнем с того, что получим всю или часть переписки с указанным пользователем. Для этого вам потребуется реализовать метод API messages.getHistory по аналогии с тем, как вы реализовывали получение списка друзей пользователя:

def messages_get_history(user_id, offset=0, count=20):
    assert isinstance(user_id, int), "user_id must be positive integer"
    assert user_id > 0, "user_id must be positive integer"
    assert isinstance(offset, int), "offset must be positive integer"
    assert offset >= 0, "user_id must be positive integer"
    assert count >= 0, "user_id must be positive integer"
    # PUT YOUR CODE HERE
    pass
Замечание: В реализации функции messages_get_history нужно учитывать, что API ВКонтакте устанавливает ограничение на число запросов в одну секунду (на сегодняшний день это три запроса). Число сообщений, которое вы можете получить за одно запрос - 200.

Далее приведен пример использования функции messages_get_history:

>>> user_id = # PUT USER ID HERE
>>> history = messages_get_history(user_id)
>>> from pprint import pprint as pp
>>> message = history['response']['items'][0]
>>> pp(message)
{'body': 'Это текст сообщения.',
 'date': 1474811631,
 'from_id': USER_ID_HERE,
 'id': 168989,
 'out': 0,
 'read_state': 1,
 'user_id': USER_ID_HERE}

Каждое сообщение содержит следующие поля:

  • body - текст сообщения;
  • date - дата отправки сообщения в формате unixtime;
  • from_id - идентификатор автора сообщения;
  • id - идентификатор сообщения;
  • out - тип сообщения (0 — полученное, 1 — отправленное, не возвращается для пересланных сообщений);
  • read_state - статус сообщения (0 — не прочитано, 1 — прочитано, не возвращается для пересланных сообщений);
  • user_id - идентификатор пользователя, в диалоге с которым находится сообщение.

Нас интересует поле date, которое представляет дату отправки сообщения в формате unixtime. Чтобы изменить формат даты представления можно воспользоваться функцией strftime из модуля datetime:

>>> from datetime import datetime
>>> date = datetime.fromtimestamp(message['date']).strftime("%Y-%m-%d")
'2016-09-25'

Формат представления указывается в виде строки форматирования, например, %Y-%m-%d - год, месяц и день, соответственно.

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

def count_dates_from_messages(messages):
    #PUT YOUR CODE HERE
    pass
Замечание: При большом количестве сообщений вы их можете разбить по неделям или месяцам.

Далее мы воспользуемся сервисом Plot.ly, который предоставляет API для рисования графиков. Запрос содержит информацию о точках, которые нужно отобразить на графике. Вам нужно зарегистрироваться и получить ключ доступа (API KEY). Для более простого взаимодействия с Plot.ly мы воспользуемся готовым модулем (существуют решения и для других языков):

$ pip3 install plotly

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

import plotly
plotly.tools.set_credentials_file(username='YOUR_USER_NAME', api_key='YOUR_API_KEY')

Ниже приведен пример построения графика, где переменная x содержит даты, а y - количество сообщений в этот день:

import plotly.plotly as py
import plotly.graph_objs as go

from datetime import datetime
x = [datetime(year=2016, month=09, day=23),
     datetime(year=2016, month=09, day=24),
     datetime(year=2016, month=09, day=25)]

data = [go.Scatter(x=x,y=[142, 50, 8])]
py.iplot(data)

Созданный график вы можете найти в своем профиле:

Задание №3: Требуется написать функцию get_network(), которая для указанного списка пользователей users_ids строит граф и представляет его либо в виде матрицы смежности (as_edgelist=False), либо в виде списка ребер (as_edgelist=True). В полученном графе необходимо выделить сообщества и визуализировать результат.

def get_network(users_ids, as_edgelist=True):
    """ Building a friend graph for an arbitrary list of users """
    # PUT YOUR CODE HERE
    pass

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

$ pip install python-igraph
$ pip install numpy
$ pip install cairocffi
$ brew install cairo    # Только для MacOS X. Для других ОС см. https://www.cairographics.org/download/
from igraph import Graph, plot
import numpy as np

vertices = [i for i in range(7)]
edges = [
    (0,2),(0,1),(0,3),
    (1,0),(1,2),(1,3),
    (2,0),(2,1),(2,3),(2,4),
    (3,0),(3,1),(3,2),
    (4,5),(4,6),
    (5,4),(5,6),
    (6,4),(6,5)
]

g = Graph(vertex_attrs={"label":vertices},
    edges=edges, directed=False)

N = len(vertices)
visual_style = {}
visual_style["layout"] = g.layout_fruchterman_reingold(
    maxiter=1000,
    area=N**3,
    repulserad=N**3)

plot(g, **visual_style)

g.simplify(multiple=True, loops=True)

communities = g.community_edge_betweenness(directed=False)
clusters = communities.as_clustering()
print(clusters)
Clustering with 7 elements and 2 clusters
[0] 0, 1, 2, 3
[1] 4, 5, 6
pal = igraph.drawing.colors.ClusterColoringPalette(len(clusters))
g.vs['color'] = pal.get_many(clusters.membership)

results matching ""

    No results matching ""