Парсим API HeadHunter с помощью R
Использовать библиотеки R
library(tidyverse)
library(httr2)
library(furrr)
Начнем с HH: получение токена
Вам необходимо зарегистрироваться на сайте hh.ru. После чего Вы сможете подать заявку на регистрацию своего приложения.
Чтобы это сделать перейдите по ссылке https://dev.hh.ru
Там будет раздел «Регистрация приложения». Кликаете на кнопку Добавить приложение
Заполняете все поля. Сильно можно не «заморачиваться». В Redirect URI
указываете любую ссылку, которая имеет к вам отношение. В моем случае, я указал корпоративный сайт.
После заполнения формы, нажимаете на кнопку Добавить.
Примерно через неделю, если со стороны hh не будет к вам вопросов, вам одобрят заявку.
Что нам понадобится?
Для его получения нам необходимо:
Скопировать ссылку
где: YOUR_CLIENT_ID необходимо заменить на Client ID, а YOUR_REDIRECT_URI — на Redirect URI.
Переходим по получившейся ссылке и нажимаем
Продолжить
У вас сгенерируется новая ссылка типа:
Ваш код указан после ?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