Приготовим пакетики

packages <- c("httr", "dplyr", "tidyr", "rlist", "stringr")
# install.packages(packages)
library(httr)
library(dplyr)
library(tidyr)
library(rlist)
library(stringr)

Application Programming Interfaces

Давайте разберемся что такое API c помощью картинки.

  1. Обычный парсинг предполагает, что у нас есть какая-то веб-страница, на которой есть информация необходимая нам. Как мы убедимся на следующих занятиях, парсить информацию с таких страниц достаточно сложно.
  2. Сама страница рендерится (создается) из данных, которые лежат в базе данных.

Хотелось бы попросить сервер, в данном случае Vk, получить доступ к их базе данных, чтобы не мучиться с парсингом страницы. Очевидно, что прямой доступ до базы данных никто нам не даст. Для этого и был создан API. Это некоторая “прослойка”, которая позволяет получать данные из баз.

Первое, что нужно сделать перед парсингом, это проверить, есть ли API, которое облегчит вашу жизнь. У больших компаний есть API, например это Vk или Twitter. Важно понимать, что API это не только получение каких-то данных из баз сервера. Это может быть все что угодно. Например, с помощью API Яндекс.Переводчик вы можете перевести текст с одного языка на другой.

Работа с разными API очень схожи, везде есть токены, методы и тому подобное. Поэтому давайте разберём все на одном примере – VkApi.

VkAPI

У Vk есть свое API. Вот его документация.

Токен

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

ВАЖНО! ЕСЛИ У КОГО-ТО ЕСТЬ ВАШ ТОКЕН, ТО ОН ИМЕЕТ ПОЛНЫЙ ДОСТУП К ВАШЕЙ СТРАНИЦЕ. НИКОМУ НЕ ДАВАЙТЕ СВОЙ ТОКЕН.

  1. Создаем свое приложение

Переходим по ссылке и создаем свое приложение.

  1. Id приложения

В настройках вашего приложения есть его id.

  1. Генерируем ссылку

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

id <- "7657737"
path <- paste("https://oauth.vk.com/authorize?client_id=", id, "&display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=friends&response_type=token&v=5.52", sep = "")
cat(path)
https://oauth.vk.com/authorize?client_id=7657737&display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=friends&response_type=token&v=5.52
  1. Получаем токен

Полученную ссылку вводим в браузере. Соглашаемся дать доступ (там будет как раз описано, к чему вы даете доступ). Копируем ссылку, которая получилась в итоге.

"https://oauth.vk.com/blank.html#access_token=388bafec47318fdbbb2bda6ef769adb8df50fda7a0a65e7d99caf4577bac6cedc0c67c16e99e0d605e186&expires_in=86400&user_id=73614108"
## [1] "https://oauth.vk.com/blank.html#access_token=388bafec47318fdbbb2bda6ef769adb8df50fda7a0a65e7d99caf4577bac6cedc0c67c16e99e0d605e186&expires_in=86400&user_id=73614108"

Здесь нужно обратить внимание на следующие вещи:

  1. access_token – это ваш токен
  2. expires_in – время жизни токена (по дефолту 86400 секунд = 1 сутки)
  3. user_id – id человека, которому предоставлен доступ

Запишем токен в отдельную переменную.

token <- "388bafec47318fdbbb2bda6ef769adb8df50fda7a0a65e7d99caf4577bac6cedc0c67c16e99e0d605e186"

Делаем свой первый запрос

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

Здесь нужно вспомнить, что такое HTTP запросы и метод GET, с помощью которого мы будем получать нужные нам данные.

Давайте посмотрим как формируется url, по которому мы и сделаем запрос.

url <- "https://api.vk.com/method/" + название метода + "?" + параметры метода, разделенные &

Давайте попробуем использовать метод friends.get [ссылка] , который возращает список друзей заданного пользователя. Если не указать пользователя (параметр user_id), то он выведет ваших друзей. В параметрах нужно указать версию API (5.52 актуальная), ваш токен и какие-то еще параметры из описания метода.

method <- "friends.get"
version <- "5.52"
fields <- "first_name,last_name"
token <- "388bafec47318fdbbb2bda6ef769adb8df50fda7a0a65e7d99caf4577bac6cedc0c67c16e99e0d605e186"

url <- paste("https://api.vk.com/method/", method, 
             "?v=", version, "&access_token=", token,
             "&fields=", fields,
             sep = "")
cat(url)
https://api.vk.com/method/friends.get?v=5.52&access_token=388bafec47318fdbbb2bda6ef769adb8df50fda7a0a65e7d99caf4577bac6cedc0c67c16e99e0d605e186&fields=first_name,last_name

Полученную ссылку можно вставить в брузере. На выходе получим JSON с информацией о друзьях. Давайте получим эту информацию внутри R с помощью метода GET.

response <- GET(url = url)
con <- content(response)
print(class(con))
## [1] "list"

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

str(con, max.level = 3, list.len = 2)
## List of 1
##  $ response:List of 2
##   ..$ count: int 515
##   ..$ items:List of 515
##   .. ..$ :List of 4
##   .. ..$ :List of 4
##   .. .. [list output truncated]

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

con$response$items[[89]]
## $first_name
## [1] "Максим"
## 
## $id
## [1] 50471454
## 
## $last_name
## [1] "Мидюкин"
## 
## $track_code
## [1] "434b09d1QUMt36PEAc-g0TlFPDKoesFanz0M3ar_Xd7O5stTb1AsKCS7k5IJzqDVVbTO7FweoVeIJmmz"

Или на другого друга.

con$response$items[[1]]
## $first_name
## [1] "Александр"
## 
## $id
## [1] 201914
## 
## $last_name
## [1] "Самойлов"
## 
## $track_code
## [1] "3951827eX2n-PSG_P_-zYe-XaK-3Ae119lIO8ww3GR9_tt5SmlQyAvYKH7g_r7hn21XDFlxvmmGEOw"

Обработка данных

Можно увидеть, что поля у всех друзей совпадают, поэтому можно отказаться от list и перейти к data frame. Это можно сделать с помощью функции bind_rows из пакета dplyr, ей нужно лишь передать список списков. А она сама все склеит в один data frame.

df <- con$response$items %>%
  bind_rows()

df %>% head()
## # A tibble: 6 x 5
##   first_name      id last_name track_code                            deactivated
##   <chr>        <int> <chr>     <chr>                                 <chr>      
## 1 Александр   201914 Самойлов  3951827eX2n-PSG_P_-zYe-XaK-3Ae119lIO… <NA>       
## 2 Ксения      347530 Матросова 61ef7392q8KcGSPK2c3kr9oakRoH5cNo_v1Y… <NA>       
## 3 Дмитрий    2028993 Генералов 6e6903284A3BN2i3E1vTVeZPmXQhnWlDHqDj… <NA>       
## 4 Никита     3577476 Чухров    8323f41e2NH57iJQxXx3ALZ2GIxVJ_VylaN4… <NA>       
## 5 Kirill     3739940 Shilov    5d5afa8bwXyrXj2no8JvOKmFCkFCJiKINDiB… <NA>       
## 6 Дмитрий    5778788 Емельянов fcaa92can3yglTk8WgnuCFKxwVlHL0wQwWh3… <NA>

Поле track_code нам не нужно, поэтому давайте избавимся от него.

df <- df %>%
  select(-track_code)

df %>% head()
## # A tibble: 6 x 4
##   first_name      id last_name deactivated
##   <chr>        <int> <chr>     <chr>      
## 1 Александр   201914 Самойлов  <NA>       
## 2 Ксения      347530 Матросова <NA>       
## 3 Дмитрий    2028993 Генералов <NA>       
## 4 Никита     3577476 Чухров    <NA>       
## 5 Kirill     3739940 Shilov    <NA>       
## 6 Дмитрий    5778788 Емельянов <NA>

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

df %>%
  filter(!is.na(deactivated)) %>%
  head()
## # A tibble: 6 x 4
##   first_name       id last_name deactivated
##   <chr>         <int> <chr>     <chr>      
## 1 DELETED    27159331 ""        deleted    
## 2 Ирина      27225061 "Иванова" deleted    
## 3 Илья       37280099 "Дуров"   deleted    
## 4 DELETED    70212394 ""        deleted    
## 5 DELETED    86090729 ""        deleted    
## 6 DELETED    98638665 ""        deleted
df <- df %>%
  filter(is.na(deactivated)) %>%
  select(-deactivated)

df %>%
  head()
## # A tibble: 6 x 3
##   first_name      id last_name
##   <chr>        <int> <chr>    
## 1 Александр   201914 Самойлов 
## 2 Ксения      347530 Матросова
## 3 Дмитрий    2028993 Генералов
## 4 Никита     3577476 Чухров   
## 5 Kirill     3739940 Shilov   
## 6 Дмитрий    5778788 Емельянов

Можно склеить имя и фамилию.

df <- df %>%
  unite("name", last_name, first_name, sep = " ")

df %>%
  head()
## # A tibble: 6 x 2
##   name                    id
##   <chr>                <int>
## 1 Самойлов Александр  201914
## 2 Матросова Ксения    347530
## 3 Генералов Дмитрий  2028993
## 4 Чухров Никита      3577476
## 5 Shilov Kirill      3739940
## 6 Емельянов Дмитрий  5778788

Сейчас это просто data frame моих друзей, но в нем могут храниться и друзья другого человека, поэтому давайте хранить в data frame пару людей.

my_id <- 73614108
my_name <- "Зарманбетов Ахмед"
df <- df %>% 
  mutate(friend_id = my_id,
         friend_name = my_name)

df %>%
  head()
## # A tibble: 6 x 4
##   name                    id friend_id friend_name      
##   <chr>                <int>     <dbl> <chr>            
## 1 Самойлов Александр  201914  73614108 Зарманбетов Ахмед
## 2 Матросова Ксения    347530  73614108 Зарманбетов Ахмед
## 3 Генералов Дмитрий  2028993  73614108 Зарманбетов Ахмед
## 4 Чухров Никита      3577476  73614108 Зарманбетов Ахмед
## 5 Shilov Kirill      3739940  73614108 Зарманбетов Ахмед
## 6 Емельянов Дмитрий  5778788  73614108 Зарманбетов Ахмед

Оптимизация кода

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

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

get_method <- function(method, token, params, version = "5.52"){
  url <- paste('https://api.vk.com/method/', method, sep = '')
  params <- append(params, list(access_token = token, v = version))
  request <- GET(url, query = params)
  info <- content(request)
  return(info)
}

Теперь у нас есть аккуратная функция, давайте попробуем вызвать ее.

params <- list(fields = "first_name,last_name")
con <- get_method(method = "friends.get", token = token, params = params)
str(con, max.level = 3, list.len = 2)
## List of 1
##  $ response:List of 2
##   ..$ count: int 515
##   ..$ items:List of 515
##   .. ..$ :List of 4
##   .. ..$ :List of 4
##   .. .. [list output truncated]

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

Теперь давайте напишем функцию, которая обработает list и получит data frame.

to_df <- function(con, friend_id, friend_name) {
  friends_list <- con$response$items
  friends_list<- list.apply(friends_list, list.flatten) 
  df <- friends_list %>%
    bind_rows() 
  
  if ("deactivated" %in% names(df)) {
    df <- df %>%
      filter(is.na(deactivated))
  }
  
  df <- df %>%
    unite("name", last_name, first_name, sep = " ") %>%
    select(id, name) %>%
    mutate(friend_id = friend_id,
           friend_name = friend_name) 
  return(df)
}

Используем ее.

df <- to_df(con, friend_id = 73614108, friend_name = "Зарманбетов Ахмед")
df %>%
  head()
## # A tibble: 6 x 4
##        id name               friend_id friend_name      
##     <int> <chr>                  <dbl> <chr>            
## 1  201914 Самойлов Александр  73614108 Зарманбетов Ахмед
## 2  347530 Матросова Ксения    73614108 Зарманбетов Ахмед
## 3 2028993 Генералов Дмитрий   73614108 Зарманбетов Ахмед
## 4 3577476 Чухров Никита       73614108 Зарманбетов Ахмед
## 5 3739940 Shilov Kirill       73614108 Зарманбетов Ахмед
## 6 5778788 Емельянов Дмитрий   73614108 Зарманбетов Ахмед

Граф друзей

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

params <- list(fields = "first_name,last_name")
con <- get_method(method = "friends.get", token = token, params = params)
df <- to_df(con, 73614108, "Зарманбетов Ахмед")
df %>% head()
## # A tibble: 6 x 4
##        id name               friend_id friend_name      
##     <int> <chr>                  <dbl> <chr>            
## 1  201914 Самойлов Александр  73614108 Зарманбетов Ахмед
## 2  347530 Матросова Ксения    73614108 Зарманбетов Ахмед
## 3 2028993 Генералов Дмитрий   73614108 Зарманбетов Ахмед
## 4 3577476 Чухров Никита       73614108 Зарманбетов Ахмед
## 5 3739940 Shilov Kirill       73614108 Зарманбетов Ахмед
## 6 5778788 Емельянов Дмитрий   73614108 Зарманбетов Ахмед
my_friends <- df$id
names(my_friends) <- df$name
my_friends[1]
## Самойлов Александр 
##             201914
names(my_friends[1])
## [1] "Самойлов Александр"
for (i in 1:length(my_friends)) {
  Sys.sleep(0.25)
  params <- list(user_id = my_friends[i], fields = "name")
  con <- get_method(method = "friends.get", token = token, params = params)
  friend_df <- to_df(con, my_friends[i], names(my_friends[i])) %>%
    filter(id %in% my_friends)
  df <- df %>% 
    union(friend_df)
}
library(igraph)
graph_vk <- graph.edgelist(as.matrix(df %>% select(name, friend_name)), directed = FALSE)
V(graph_vk)$color = ifelse(V(graph_vk)$name == 'Зарманбетов Ахмед', 'red', 'orange')
plot(graph_vk)

size <- 10000
png('vk_graph.png', width = size, height = size)
plot(graph_vk, vertex.label.cex = 2, vertex.size = 3, vertex.label = str_replace(V(graph_vk)$name, " ", "\n "))
dev.off()
## quartz_off_screen 
##                 2