Парсим API HeadHunter с помощью R

Использовать библиотеки R

library(tidyverse)
library(httr2)
library(furrr)

Начнем с HH: получение токена

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

Чтобы это сделать перейдите по ссылке https://dev.hh.ru

Untitled

Там будет раздел «Регистрация приложения». Кликаете на кнопку Добавить приложение

Untitled

Заполняете все поля. Сильно можно не «заморачиваться». В Redirect URI указываете любую ссылку, которая имеет к вам отношение. В моем случае, я указал корпоративный сайт.

Untitled

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

Untitled

Что нам понадобится?

Для его получения нам необходимо:

  1. Скопировать ссылку


где: YOUR_CLIENT_ID необходимо заменить на Client ID, а YOUR_REDIRECT_URI — на Redirect URI.

  1. Переходим по получившейся ссылке и нажимаем Продолжить

Untitled

У вас сгенерируется новая ссылка типа:

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

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

Переходим к R: получение токена

Библиотеки

library(tidyverse)
library(httr2)

Сохраняем переменные

client_id <- 'YOUR_CLIENT_ID'
client_secret <- 'YOUR_CLIENT_SECRET'
code <- 'YOUR_CODE'

Также понадобится POST запрос к hh.ru для получения токена по ссылке https://api.hh.ru/token

#POST запрос к hh.ru для получения токена
oauth_endpoint <- ""
#Запрос токена, используем библиотеку httr2
TOKEN <- request(oauth_endpoint) %>% 
  req_body_form(
    grant_type = "authorization_code",
    client_id = client_id,
    client_secret = client_secret,
    code = code,
    redirect_uri = "https://eco-hotel.ru"
  ) %>% 
  req_perform() %>% 
  resp_body_json()

Здесь стоит отметить, что переменная grant_type должна стоять первой, хотя в документации это явно не указано. Название переменной grant_type это и есть authorization_code . Это НЕ код, который вам нужно получить, а текстовая переменная.

После выполнении запроса вы получите переменную TOKEN. Извлечь сам токен можно при помощи команды

TOKEN_ID <- TOKEN$access_token

Справочник параметров

В документации hh есть раздел «Справочники». Он вам точно понадобится, если вы хотите более тонко настроить парсинг.

Регионы

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

areas_url <- ""
areas <- request(areas_url) %>% 
  req_perform() %>% 
  resp_body_json()

Вы получите список. Со списком работать не очень удобно, поэтому можно сформировать dataframe для индетификации id тех регионов, которые вас интересуют. Для России это можно сдлеать так:

areas_df <- map_dfr(areas[[1]]$areas, ~tibble(id = .x$id, name = .x$name))

Меня интересовало три региона, которые я и выбрал

areas_id <- map(areas[[1]]$areas, ~.x$id) %>% 
  keep(~ . %in% c("1", "2019", "2"))

Все тоже самое мы можем сдлеать и для профессиональных ролей

Профессиональные роли

professional_roles_url <- ""
professional_roles <- request(professional_roles_url) %>%
  req_perform() %>%
  resp_body_json()

Здесь преобразование списка в dataframe меняется, так как нужно было понять к какой категории относится та или иная профессиональная роль:

roles_df <- professional_roles$categories %>%
  map_dfr( ~{
    tibble(
      id_categories = .x$id,
      name_categories = .x$name,
      id_roles = map_chr(.x$roles, "id"),
      name_roles = map_chr(.x$roles, "name")
    )
  }) %>%
  distinct(id_roles, .keep_all = TRUE)

Мой список выглядит так:

roles_id <- list("8", "90", "89", "130", "72",
"74", "94", "40", "113", "87",
"76", "51", "26", "3")

Опыт работы

hh.ru имеет ограничения по количеству запросов в каждой категории:

Это означает, что в одной категории можно просмотреть не более:

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

## Опыт работы ----
exp_id <- list('noExperience', #нет опыта
               'between1And3', #от 1 до 3 лет
               'between3And6', #от 3 до 6 лет
               'moreThan6')    #более 6 летрсонала

Функции, которая преобразует JSON ответ в dataframe

# Функция, которая разворачивает ответ JSON и преобразовывает его в dataframe ----
get_vacancies_inter <- function(vacancies) {
  # Извлекаем элемент 'items' из входящего JSON-объекта и применяем функцию 'map_dfr'
  # для каждой вакансии, объединяя результаты в один dataframe
  vacancies$items %>% 
    map_dfr(~ {
      tibble(
        # Извлекаем и сохраняем идентификатор вакансии
        id = .x$id,
        # Извлекаем и сохраняем название вакансии
        name = .x$name,
        # Извлекаем и сохраняем идентификатор области
        area_id = .x$area$id,
        # Извлекаем и сохраняем название области
        area_name = .x$area$name,
        # Извлекаем и сохраняем минимальную зарплату, если она указана, иначе сохраняем NA
        salary_from = .x$salary$from %||% NA_real_,
        # Извлекаем и сохраняем максимальную зарплату, если она указана, иначе сохраняем NA
        salary_to = .x$salary$to %||% NA_real_,
        # Извлекаем и сохраняем информацию о том, указана ли зарплата до вычета налогов
        salary_gross = .x$salary$gross %||% NA,
        # Извлекаем и сохраняем тип графика работы
        schedule = .x$schedule$name,
        # Преобразуем и сохраняем дату публикации вакансии в формат "YYYY-MM-DD"
        published_at = format(as.Date(.x$published_at, "%Y-%m-%d")),
        # Преобразуем и сохраняем дату создания вакансии в формат "YYYY-MM-DD"
        created_at = format(as.Date(.x$created_at, "%Y-%m-%d")),
        # Извлекаем и сохраняем альтернативный URL вакансии
        alternate_url = .x$alternate_url,
        # Извлекаем и сохраняем название работодателя
        employer = .x$employer$name,
        # Извлекаем и сохраняем идентификаторы профессиональных ролей
        professional_roles_id = map_chr(.x$professional_roles, "id"),
        # Извлекаем и сохраняем названия профессиональных ролей
        professional_roles_name = map_chr(.x$professional_roles, "name"),
        # Извлекаем и сохраняем требуемый опыт работы
        experience = .x$experience$name,
        # Извлекаем и сохраняем тип занятости
        employment = .x$employment$name
      )
    })
}

Это не полный список того, что можно «вытащить» из JSON ответа. В моем случае, данных параметров достаточно.

Функция парсинга API hh.ru

# Функция для получения списка вакансий с фильтрацией по опыту, области и профессиональной роли ----
get_vacancies_result <- function(page, experience = NULL, area = NULL, professional_role = NULL) {
  
  # Выполняем HTTP-запрос к API для получения списка вакансий
  vacancies <- request(vacancies_url) %>%
    # Устанавливаем заголовок авторизации с токеном
    req_headers(
      Authorization = paste("Bearer", TOKEN_ID)
    ) %>%
    # Устанавливаем параметры URL-запроса
    req_url_query(
      per_page = 100,               # Количество вакансий на странице
      only_with_salary = TRUE,      # Только вакансии с указанной зарплатой
      page = page,                  # Номер страницы
      experience = experience,      # Требуемый опыт работы (если указан)
      professional_role = professional_role,  # Профессиональная роль (если указана)
      area = area                   # Область (если указана)
    ) %>%
    # Выполняем запрос
    req_perform() %>%
    # Преобразуем тело ответа в формат JSON
    resp_body_json()
  
  # Если в полученном списке вакансий нет элементов, возвращаем NULL
  if (length(vacancies$items) == 0) {
    return(NULL)
  }
  
  # Преобразуем полученные вакансии в dataframe, используя вспомогательную функцию
  vacancies_df_inter <- get_vacancies_inter(vacancies)
  
  # Возвращаем полученный dataframe
  return(vacancies_df_inter)
}

Создаем параметры для нашей функции get_vacancies_result

# Создание сетки параметров для запросов вакансий ----
params <- expand_grid(
  # Задаем вектор значений для параметра 'page' от 0 до 19 (включительно),
  # что соответствует страницам результатов поиска
  page = 0:19,
  
  # Используем вектор 'roles_id', содержащий идентификаторы профессиональных ролей,
  # для параметра 'professional_role'
  professional_role = roles_id,
  
  # Используем вектор 'exp_id', содержащий идентификаторы опыта работы,
  # для параметра 'experience'
  experience = exp_id,
  
  # Используем вектор 'areas_id', содержащий идентификаторы областей,
  # для параметра 'area'
  area = areas_id
)

Функция expand_grid из пакета tidyr создает все возможные комбинации из заданных значений параметров. Она принимает несколько векторов и возвращает dataframe, где каждая строка представляет одну из возможных комбинаций этих векторов.

В результате, вы должны получить следующее:

# A tibble: 3,360 × 4
    page professional_role experience area     
                        
 1     0            
 2     0            
 3     0            
 4     0            
 5     0            
 6     0            
 7     0            
 8     0            
 9     0            
10     0            
# ℹ 3,350 more rows
# ℹ Use `print(n = ...)` to see more rows

Выполняем запрос к API: используем пакет furrr

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

# Устанавливаем URL API для получения вакансий
vacancies_url <- "https://api.hh.ru/vacancies"

# Устанавливаем токен для авторизации
TOKEN_ID <- "YOUR_TOKEN"

# Настраиваем план параллельного выполнения задач с использованием нескольких сессий
future::plan(multisession)

# Записываем текущее время для измерения времени выполнения
start.time <- Sys.time()

# Используем 'params' для выполнения параллельных запросов к API и обработки результатов
vacancies_df <- params %>%
  # Выполняем функцию 'get_vacancies_result' для каждого набора параметров параллельно
  future_pmap_dfr(get_vacancies_result) %>%
  # Удаляем дубликаты по идентификатору вакансии, сохраняя все остальные столбцы
  distinct(id, .keep_all = TRUE) %>%
  # Добавляем столбец 'region' с использованием функции 'case_when'
  mutate(
    region = case_when(
      area_id == 1 ~ "МСК",     # Если 'area_id' равно 1, устанавливаем регион "МСК"
      area_id == 2 ~ "СПБ",     # Если 'area_id' равно 2, устанавливаем регион "СПБ"
      .default = "МО"           # Для всех остальных значений устанавливаем регион "МО"
    )
  )

# Записываем текущее время для измерения времени выполнения
end.time <- Sys.time()

# Вычисляем время, затраченное на выполнение запросов и обработку данных
time.taken <- end.time - start.time

# Выводим время выполнения
print(time.taken)

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

Данный запрос выполняется ~ Time difference of 4.136553 mins.

Результат, выполнения функции:

# A tibble: 54,370 × 17
   id        name          area_id area_name salary_from salary_to salary_gross schedule
                                                
 1 104336620 Администрато… 1       Москва          68000        NA TRUE         Сменный…
 2 80290406  Администрато… 1       Москва          70000     70000 TRUE         Полный …
 3 104335755 Вечерний адм… 1       Москва          60000     60000 TRUE         Полный …
 4 104335439 Администрато… 1       Москва           3000        NA TRUE         Сменный…
 5 96379378  Администрато… 1       Москва          45000        NA FALSE        Гибкий …
 6 103817094 Дизайнер инт… 1       Москва         100000        NA FALSE        Полный …
 7 103433054 Специалист п… 1       Москва          75000    165000 FALSE        Полный …
 8 103629566 Администрато… 1       Москва          75000    100000 FALSE        Полный …
 9 104052792 Администратор 1       Москва          60000        NA FALSE        Полный …
10 103037149 Администрато… 1       Москва          55000        NA FALSE        Сменный…
# ℹ 54,360 more rows
# ℹ 9 more variables: published_at , created_at , alternate_url ,
#   employer , professional_roles_id , professional_roles_name ,
#   experience , employment , region 
# ℹ Use `print(n = ...)` to see more rows

© Habrahabr.ru